From cfe8d07e2901ba9bf3a8eeb9f6207812c83d8a29 Mon Sep 17 00:00:00 2001 From: Jean Carraro Date: Thu, 2 Apr 2026 19:06:16 -0300 Subject: [PATCH 01/49] Add 5-minute prediction mode and refactor shared display - Add complete 5m assistant (index5m.js, config5m.js) with shorter timeframe indicators: EMA(3/8) crossover, momentum (ROC + acceleration), and Order Flow Imbalance (OFI) from Binance trade stream - Extract shared terminal rendering into display.js (ANSI colors, kv(), renderScreen(), colorPriceLine()) used by both 15m and 5m modes - Add 5m-tuned engines: probability5m (OFI + momentum + EMA cross primary signals, quadratic time decay) and edge5m (lower thresholds, shorter phases) - Add binanceWsOfi.js for real-time order flow data collection - Refactor index.js to use shared display module - Add npm run start:5m script - Add CLAUDE.md with full architecture documentation Co-Authored-By: Claude Opus 4.6 --- CLAUDE.md | 82 ++++++ package.json | 3 +- src/config5m.js | 27 ++ src/data/binanceWsOfi.js | 130 +++++++++ src/display.js | 235 ++++++++++++++++ src/engines/edge5m.js | 33 +++ src/engines/probability5m.js | 84 ++++++ src/index.js | 320 ++++++--------------- src/index5m.js | 527 +++++++++++++++++++++++++++++++++++ src/indicators/emaCross.js | 41 +++ src/indicators/momentum.js | 75 +++++ src/indicators/orderFlow.js | 35 +++ 12 files changed, 1354 insertions(+), 238 deletions(-) create mode 100644 CLAUDE.md create mode 100644 src/config5m.js create mode 100644 src/data/binanceWsOfi.js create mode 100644 src/display.js create mode 100644 src/engines/edge5m.js create mode 100644 src/engines/probability5m.js create mode 100644 src/index5m.js create mode 100644 src/indicators/emaCross.js create mode 100644 src/indicators/momentum.js create mode 100644 src/indicators/orderFlow.js diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 00000000..7239c087 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,82 @@ +# CLAUDE.md + +This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. + +## Commands + +```bash +npm install # install dependencies +npm start # run 15m assistant (node src/index.js) +npm run start:5m # run 5m assistant (node src/index5m.js) +``` + +No test runner or linter is configured. The project uses ES modules (`"type": "module"` in package.json). + +## Architecture + +This is a single-process real-time console assistant for Polymarket BTC 15-minute prediction markets. It polls every 1 second and redraws a static terminal screen using ANSI escape codes + `readline`. + +### Data layer (`src/data/`) + +- **binance.js / binanceWs.js** — Binance REST (klines, last price) and WebSocket trade stream for live spot price. +- **polymarket.js** — Gamma API + CLOB API: fetches the active 15m market, outcome token IDs, CLOB prices, and order books. +- **polymarketLiveWs.js** — Polymarket live WebSocket (`wss://ws-live-data.polymarket.com`); primary source for the Chainlink BTC/USD price shown on Polymarket UI. +- **chainlink.js / chainlinkWs.js** — Fallback: reads Chainlink BTC/USD aggregator on Polygon via HTTP RPC or WSS RPC using ethers v6. + +Price source priority: `polymarketLiveWs` → `chainlinkWs` → `chainlink` HTTP fetch. + +### Shared display (`src/display.js`) + +All terminal rendering helpers (ANSI colors, `kv()`, `renderScreen()`, `colorPriceLine()`, etc.) shared by both 15m and 5m modes. + +### Indicators (`src/indicators/`) + +Pure functions operating on arrays of OHLCV candles (Binance 1m klines): +- **heikenAshi.js** — Heiken Ashi candles + consecutive same-color count. +- **rsi.js** — RSI, SMA helper, slope of last N values. +- **macd.js** — MACD line, signal, histogram, histogram delta. *(15m only)* +- **vwap.js** — Session VWAP (scalar) and VWAP series (per-candle). +- **orderFlow.js** — Order Flow Imbalance scoring from real-time trade data. *(5m only)* +- **emaCross.js** — Fast EMA(3)/EMA(8) crossover, replaces MACD for short timeframes. *(5m only)* +- **momentum.js** — Rate of change (1m/3m), acceleration, volume surge. *(5m only)* + +### Engines (`src/engines/`) + +- **regime.js** — Classifies market as `TREND_UP`, `TREND_DOWN`, `RANGE`, or `CHOP` based on price vs VWAP, VWAP slope, VWAP cross count, and volume. *(15m only)* +- **probability.js** — `scoreDirection`: additive scoring model (up/down integer scores) from VWAP, RSI, MACD, Heiken Ashi, failed VWAP reclaim; normalizes to 0–1. `applyTimeAwareness`: decays signal toward 50% as remaining time shrinks. *(15m)* +- **probability5m.js** — `scoreDirection5m`: primary signals are order flow + momentum + EMA cross; secondary: RSI(5), HA (relaxed), short VWAP. `applyTimeAwareness5m`: quadratic decay (exponent 0.6). *(5m)* +- **edge.js** — Compares model probability vs Polymarket market price to compute edge. `decide` uses phase-dependent thresholds (EARLY/MID/LATE) to emit `ENTER` or `NO_TRADE`. *(15m)* +- **edge5m.js** — Re-exports `computeEdge`; `decide5m` uses 5m-tuned phases (EARLY >3m / MID >1.5m / LATE) and lower thresholds. *(5m)* + +### Main loops + +- **index.js** (15m) — Starts three WebSocket streams (Binance trades, Polymarket live, Chainlink), loops fetching klines + Polymarket snapshot, computes TA indicators, renders terminal screen, logs to `./logs/signals.csv`. +- **index5m.js** (5m) — Same structure but uses `binanceWsOfi.js` (order flow stream), 5m-specific indicators/engines, shorter VWAP window, logs to `./logs/signals_5m.csv`. + +### Configuration (`src/config.js`, `src/config5m.js`) + +All tunable parameters (poll interval, TA periods, Polymarket series IDs, RPC URLs) live here and are read from environment variables with defaults. `config5m.js` extends the base config with 5m-tuned values (RSI period 5, VWAP window 10 candles, EMA 3/8). + +### Proxy (`src/net/proxy.js`) + +Called once at startup via `applyGlobalProxyFromEnv()`. Reads `HTTPS_PROXY`/`HTTP_PROXY`/`ALL_PROXY` and patches Node's global `fetch` dispatcher (via `undici`) and WebSocket connections to route through HTTP or SOCKS5 proxies. + +## Key environment variables + +| Variable | Default | Purpose | +|---|---|---| +| `POLYGON_RPC_URL` | `https://polygon-rpc.com` | Chainlink fallback HTTP RPC | +| `POLYGON_RPC_URLS` | — | Comma-separated list of fallback RPCs | +| `POLYGON_WSS_URLS` | — | WSS RPCs for real-time Chainlink fallback | +| `POLYMARKET_AUTO_SELECT_LATEST` | `true` | Auto-pick latest 15m market | +| `POLYMARKET_SLUG` | — | Pin a specific market slug | +| `POLYMARKET_5M_SERIES_ID` | (falls back to 15m series) | Series ID for 5m markets | +| `POLYMARKET_5M_SERIES_SLUG` | `btc-up-or-down-5m` | Series slug for 5m markets | +| `HTTPS_PROXY` / `ALL_PROXY` | — | Proxy for all outbound connections | + +## Output + +- Terminal screen refreshed every second via `readline.cursorTo` + `clearScreenDown`. +- `./logs/signals.csv` — one row per poll tick (15m mode) with regime, signal, model probabilities, market prices, edge, and recommendation. +- `./logs/signals_5m.csv` — one row per poll tick (5m mode) with OFI, momentum, EMA cross, RSI, model probs, edge, and recommendation. +- `./logs/polymarket_market_.json` — raw Polymarket market JSON dumped once per new market slug. diff --git a/package.json b/package.json index 796676ef..388fc24b 100644 --- a/package.json +++ b/package.json @@ -4,7 +4,8 @@ "type": "module", "private": true, "scripts": { - "start": "node src/index.js" + "start": "node src/index.js", + "start:5m": "node src/index5m.js" }, "dependencies": { "ethers": "^6.11.1", diff --git a/src/config5m.js b/src/config5m.js new file mode 100644 index 00000000..55534e61 --- /dev/null +++ b/src/config5m.js @@ -0,0 +1,27 @@ +import { CONFIG as BASE } from "./config.js"; + +export const CONFIG = { + ...BASE, + + candleWindowMinutes: 5, + + // Shorter RSI for 5m — 5-period on 1m candles = 5 minutes of data + rsiPeriod: 5, + rsiMaPeriod: 5, + + // VWAP slope lookback (in minutes / candles) + vwapSlopeLookbackMinutes: 3, + // Only use last N candles for VWAP in 5m mode + vwapCandleWindow: 10, + + // EMA crossover periods + emaCrossFast: 3, + emaCrossSlow: 8, + + polymarket: { + ...BASE.polymarket, + // 5m series — will need to be set via env vars + seriesId: process.env.POLYMARKET_5M_SERIES_ID || BASE.polymarket.seriesId, + seriesSlug: process.env.POLYMARKET_5M_SERIES_SLUG || "btc-up-or-down-5m" + } +}; diff --git a/src/data/binanceWsOfi.js b/src/data/binanceWsOfi.js new file mode 100644 index 00000000..1c5ea569 --- /dev/null +++ b/src/data/binanceWsOfi.js @@ -0,0 +1,130 @@ +// Enhanced Binance WebSocket trade stream that tracks Order Flow Imbalance (OFI). +// Accumulates buy vs sell volume in rolling time windows (30s, 1m, 2m). + +import WebSocket from "ws"; +import { CONFIG } from "../config.js"; +import { wsAgentForUrl } from "../net/proxy.js"; + +function toNumber(x) { + const n = Number(x); + return Number.isFinite(n) ? n : null; +} + +function buildWsUrl(symbol) { + const s = String(symbol || "").toLowerCase(); + return `wss://stream.binance.com:9443/ws/${s}@trade`; +} + +export function startBinanceOfiStream({ symbol = CONFIG.symbol } = {}) { + let ws = null; + let closed = false; + let reconnectMs = 500; + let lastPrice = null; + let lastTs = null; + + // Ring buffer of recent trades: { price, qty, isBuyerMaker, ts } + const trades = []; + const MAX_BUFFER_AGE_MS = 150_000; // keep 2.5 min of trades + + function pruneOld() { + const cutoff = Date.now() - MAX_BUFFER_AGE_MS; + while (trades.length > 0 && trades[0].ts < cutoff) { + trades.shift(); + } + } + + function computeOfi(windowMs) { + const cutoff = Date.now() - windowMs; + let buyVol = 0; + let sellVol = 0; + for (let i = trades.length - 1; i >= 0; i--) { + const t = trades[i]; + if (t.ts < cutoff) break; + // isBuyerMaker=true means the buyer placed a limit order and the seller + // hit it with a market order → the trade is a sell (taker sells). + if (t.isBuyerMaker) { + sellVol += t.qty; + } else { + buyVol += t.qty; + } + } + const total = buyVol + sellVol; + return { + buyVol, + sellVol, + total, + ofi: total > 0 ? (buyVol - sellVol) / total : 0 + }; + } + + const connect = () => { + if (closed) return; + + const url = buildWsUrl(symbol); + ws = new WebSocket(url, { agent: wsAgentForUrl(url) }); + + ws.on("open", () => { + reconnectMs = 500; + }); + + ws.on("message", (buf) => { + try { + const msg = JSON.parse(buf.toString()); + const p = toNumber(msg.p); + const q = toNumber(msg.q); + if (p === null || q === null) return; + + lastPrice = p; + lastTs = Date.now(); + + trades.push({ + price: p, + qty: q * p, // store in USD terms for volume comparisons + isBuyerMaker: msg.m === true, + ts: lastTs + }); + + // Prune periodically (every ~500 trades) + if (trades.length % 500 === 0) pruneOld(); + } catch { + return; + } + }); + + const scheduleReconnect = () => { + if (closed) return; + try { ws?.terminate(); } catch { /* ignore */ } + ws = null; + const wait = reconnectMs; + reconnectMs = Math.min(10_000, Math.floor(reconnectMs * 1.5)); + setTimeout(connect, wait); + }; + + ws.on("close", scheduleReconnect); + ws.on("error", scheduleReconnect); + }; + + connect(); + + return { + getLast() { + return { price: lastPrice, ts: lastTs }; + }, + getOfi() { + pruneOld(); + return { + ofi30s: computeOfi(30_000), + ofi1m: computeOfi(60_000), + ofi2m: computeOfi(120_000) + }; + }, + getTradeCount() { + return trades.length; + }, + close() { + closed = true; + try { ws?.close(); } catch { /* ignore */ } + ws = null; + } + }; +} diff --git a/src/display.js b/src/display.js new file mode 100644 index 00000000..325f5693 --- /dev/null +++ b/src/display.js @@ -0,0 +1,235 @@ +// Shared display/rendering utilities used by both 15m and 5m modes. + +export const ANSI = { + reset: "\x1b[0m", + red: "\x1b[31m", + green: "\x1b[32m", + yellow: "\x1b[33m", + lightRed: "\x1b[91m", + gray: "\x1b[90m", + white: "\x1b[97m", + dim: "\x1b[2m" +}; + +export function screenWidth() { + const w = Number(process.stdout?.columns); + return Number.isFinite(w) && w >= 40 ? w : 80; +} + +export function sepLine(ch = "\u2500") { + const w = screenWidth(); + return `${ANSI.white}${ch.repeat(w)}${ANSI.reset}`; +} + +export function renderScreen(text) { + try { + const lines = text.split("\n"); + const output = "\x1b[H" + lines.map(l => l + "\x1b[K").join("\n") + "\x1b[J"; + process.stdout.write(output); + } catch { + // ignore + } +} + +export function stripAnsi(s) { + return String(s).replace(/\x1b\[[0-9;]*m/g, ""); +} + +export function padLabel(label, width) { + const visible = stripAnsi(label).length; + if (visible >= width) return label; + return label + " ".repeat(width - visible); +} + +export function centerText(text, width) { + const visible = stripAnsi(text).length; + if (visible >= width) return text; + const left = Math.floor((width - visible) / 2); + const right = width - visible - left; + return " ".repeat(left) + text + " ".repeat(right); +} + +export const LABEL_W = 16; + +export function kv(label, value) { + const l = padLabel(String(label), LABEL_W); + return `${l}${value}`; +} + +export function section(title) { + return `${ANSI.white}${title}${ANSI.reset}`; +} + +export function colorPriceLine({ label, price, prevPrice, decimals = 0, prefix = "" }) { + if (price === null || price === undefined) { + return `${label}: ${ANSI.gray}-${ANSI.reset}`; + } + + const p = Number(price); + const prev = prevPrice === null || prevPrice === undefined ? null : Number(prevPrice); + + let color = ANSI.reset; + let arrow = ""; + if (prev !== null && Number.isFinite(prev) && Number.isFinite(p) && p !== prev) { + if (p > prev) { + color = ANSI.green; + arrow = " \u2191"; + } else { + color = ANSI.red; + arrow = " \u2193"; + } + } + + const formatted = `${prefix}${formatNumberDisplay(p, decimals)}`; + return `${label}: ${color}${formatted}${arrow}${ANSI.reset}`; +} + +export function formatNumberDisplay(x, digits = 0) { + if (x === null || x === undefined || Number.isNaN(x)) return "-"; + return new Intl.NumberFormat("en-US", { + minimumFractionDigits: digits, + maximumFractionDigits: digits + }).format(x); +} + +export function formatSignedDelta(delta, base) { + if (delta === null || base === null || base === 0) return `${ANSI.gray}-${ANSI.reset}`; + const sign = delta > 0 ? "+" : delta < 0 ? "-" : ""; + const pct = (Math.abs(delta) / Math.abs(base)) * 100; + return `${sign}$${Math.abs(delta).toFixed(2)}, ${sign}${pct.toFixed(2)}%`; +} + +export function colorByNarrative(text, narrative) { + if (narrative === "LONG") return `${ANSI.green}${text}${ANSI.reset}`; + if (narrative === "SHORT") return `${ANSI.red}${text}${ANSI.reset}`; + return `${ANSI.gray}${text}${ANSI.reset}`; +} + +export function formatNarrativeValue(label, value, narrative) { + return `${label}: ${colorByNarrative(value, narrative)}`; +} + +export function narrativeFromSign(x) { + if (x === null || x === undefined || !Number.isFinite(Number(x)) || Number(x) === 0) return "NEUTRAL"; + return Number(x) > 0 ? "LONG" : "SHORT"; +} + +export function narrativeFromRsi(rsi) { + if (rsi === null || rsi === undefined || !Number.isFinite(Number(rsi))) return "NEUTRAL"; + const v = Number(rsi); + if (v >= 55) return "LONG"; + if (v <= 45) return "SHORT"; + return "NEUTRAL"; +} + +export function narrativeFromSlope(slope) { + if (slope === null || slope === undefined || !Number.isFinite(Number(slope)) || Number(slope) === 0) return "NEUTRAL"; + return Number(slope) > 0 ? "LONG" : "SHORT"; +} + +export function formatProbPct(p, digits = 0) { + if (p === null || p === undefined || !Number.isFinite(Number(p))) return "-"; + return `${(Number(p) * 100).toFixed(digits)}%`; +} + +export function fmtEtTime(now = new Date()) { + try { + return new Intl.DateTimeFormat("en-US", { + timeZone: "America/New_York", + hour: "2-digit", + minute: "2-digit", + second: "2-digit", + hour12: false + }).format(now); + } catch { + return "-"; + } +} + +export function getBtcSession(now = new Date()) { + const h = now.getUTCHours(); + const inAsia = h >= 0 && h < 8; + const inEurope = h >= 7 && h < 16; + const inUs = h >= 13 && h < 22; + + if (inEurope && inUs) return "Europe/US overlap"; + if (inAsia && inEurope) return "Asia/Europe overlap"; + if (inAsia) return "Asia"; + if (inEurope) return "Europe"; + if (inUs) return "US"; + return "Off-hours"; +} + +export function fmtTimeLeft(mins) { + const totalSeconds = Math.max(0, Math.floor(mins * 60)); + const m = Math.floor(totalSeconds / 60); + const s = totalSeconds % 60; + return `${String(m).padStart(2, "0")}:${String(s).padStart(2, "0")}`; +} + +export function parsePriceToBeat(market) { + const text = String(market?.question ?? market?.title ?? ""); + if (!text) return null; + const m = text.match(/price\s*to\s*beat[^\d$]*\$?\s*([0-9][0-9,]*(?:\.[0-9]+)?)/i); + if (!m) return null; + const raw = m[1].replace(/,/g, ""); + const n = Number(raw); + return Number.isFinite(n) ? n : null; +} + +export function safeFileSlug(x) { + return String(x ?? "") + .toLowerCase() + .replace(/[^a-z0-9_-]+/g, "-") + .replace(/-+/g, "-") + .replace(/(^-|-$)/g, "") + .slice(0, 120); +} + +export function extractNumericFromMarket(market) { + const directKeys = [ + "priceToBeat", "price_to_beat", "strikePrice", "strike_price", + "strike", "threshold", "thresholdPrice", "threshold_price", + "targetPrice", "target_price", "referencePrice", "reference_price" + ]; + + for (const k of directKeys) { + const v = market?.[k]; + const n = typeof v === "string" ? Number(v) : typeof v === "number" ? v : NaN; + if (Number.isFinite(n)) return n; + } + + const seen = new Set(); + const stack = [{ obj: market, depth: 0 }]; + + while (stack.length) { + const { obj, depth } = stack.pop(); + if (!obj || typeof obj !== "object") continue; + if (seen.has(obj) || depth > 6) continue; + seen.add(obj); + + const entries = Array.isArray(obj) ? obj.entries() : Object.entries(obj); + for (const [key, value] of entries) { + const k = String(key).toLowerCase(); + if (value && typeof value === "object") { + stack.push({ obj: value, depth: depth + 1 }); + continue; + } + + if (!/(price|strike|threshold|target|beat)/i.test(k)) continue; + + const n = typeof value === "string" ? Number(value) : typeof value === "number" ? value : NaN; + if (!Number.isFinite(n)) continue; + + if (n > 1000 && n < 2_000_000) return n; + } + } + + return null; +} + +export function priceToBeatFromPolymarketMarket(market) { + const n = extractNumericFromMarket(market); + if (n !== null) return n; + return parsePriceToBeat(market); +} diff --git a/src/engines/edge5m.js b/src/engines/edge5m.js new file mode 100644 index 00000000..bed9936b --- /dev/null +++ b/src/engines/edge5m.js @@ -0,0 +1,33 @@ +// Edge detection and decision engine for 5m mode. +// Reuses computeEdge from edge.js — only the decision thresholds change. + +import { clamp } from "../utils.js"; + +export { computeEdge } from "./edge.js"; + +export function decide5m({ remainingMinutes, edgeUp, edgeDown, modelUp = null, modelDown = null }) { + // Phases tuned for 5-minute window + const phase = remainingMinutes > 3 ? "EARLY" : remainingMinutes > 1.5 ? "MID" : "LATE"; + + const threshold = phase === "EARLY" ? 0.04 : phase === "MID" ? 0.08 : 0.15; + const minProb = phase === "EARLY" ? 0.54 : phase === "MID" ? 0.58 : 0.62; + + if (edgeUp === null || edgeDown === null) { + return { action: "NO_TRADE", side: null, phase, reason: "missing_market_data" }; + } + + const bestSide = edgeUp > edgeDown ? "UP" : "DOWN"; + const bestEdge = bestSide === "UP" ? edgeUp : edgeDown; + const bestModel = bestSide === "UP" ? modelUp : modelDown; + + if (bestEdge < threshold) { + return { action: "NO_TRADE", side: null, phase, reason: `edge_below_${threshold}` }; + } + + if (bestModel !== null && bestModel < minProb) { + return { action: "NO_TRADE", side: null, phase, reason: `prob_below_${minProb}` }; + } + + const strength = bestEdge >= 0.2 ? "STRONG" : bestEdge >= 0.1 ? "GOOD" : "OPTIONAL"; + return { action: "ENTER", side: bestSide, phase, strength, edge: bestEdge }; +} diff --git a/src/engines/probability5m.js b/src/engines/probability5m.js new file mode 100644 index 00000000..be0c7493 --- /dev/null +++ b/src/engines/probability5m.js @@ -0,0 +1,84 @@ +// Probability engine for 5m mode. +// Primary signals: order flow, momentum, fast EMA crossover. +// Secondary: RSI(5), Heiken Ashi (relaxed), short VWAP. + +import { clamp } from "../utils.js"; + +export function scoreDirection5m({ + orderFlow, + momentumScore, + emaCross, + rsi, + rsiSlope, + heikenColor, + heikenCount, + price, + vwap, + vwapSlope +}) { + let up = 1; + let down = 1; + + // Order flow imbalance — primary signal (weight: up to 8 points) + if (orderFlow) { + up += orderFlow.up; + down += orderFlow.down; + } + + // Momentum — primary signal (weight: up to 5 points) + if (momentumScore) { + up += momentumScore.up; + down += momentumScore.down; + } + + // EMA crossover — secondary signal (weight: up to 3 points) + if (emaCross) { + if (emaCross.bullish) up += 1; + else down += 1; + + if (emaCross.expanding && emaCross.bullish) up += 1; + if (emaCross.expanding && !emaCross.bullish) down += 1; + + if (emaCross.crossover === "BULLISH") up += 1; + if (emaCross.crossover === "BEARISH") down += 1; + } + + // RSI — lighter weight with shorter period (weight: up to 2 points) + if (rsi !== null && rsiSlope !== null) { + if (rsi > 55 && rsiSlope > 0) up += 1; + if (rsi < 45 && rsiSlope < 0) down += 1; + + // Extreme RSI for reversal detection + if (rsi > 70) up += 1; + if (rsi < 30) down += 1; + } + + // Heiken Ashi — relaxed: only need 1 consecutive (weight: up to 1 point) + if (heikenColor) { + if (heikenColor === "green" && heikenCount >= 1) up += 1; + if (heikenColor === "red" && heikenCount >= 1) down += 1; + } + + // Short VWAP — lightweight (weight: up to 2 points) + if (price !== null && vwap !== null) { + if (price > vwap) up += 1; + if (price < vwap) down += 1; + } + + if (vwapSlope !== null) { + if (vwapSlope > 0) up += 1; + if (vwapSlope < 0) down += 1; + } + + const rawUp = up / (up + down); + return { upScore: up, downScore: down, rawUp }; +} + +export function applyTimeAwareness5m(rawUp, remainingMinutes, windowMinutes = 5) { + // Quadratic decay — preserves signal longer in early phase, + // decays aggressively only in final minute. + const ratio = clamp(remainingMinutes / windowMinutes, 0, 1); + const timeDecay = Math.pow(ratio, 0.6); + const adjustedUp = clamp(0.5 + (rawUp - 0.5) * timeDecay, 0, 1); + return { timeDecay, adjustedUp, adjustedDown: 1 - adjustedUp }; +} diff --git a/src/index.js b/src/index.js index 4c8b8a9d..3309ed2a 100644 --- a/src/index.js +++ b/src/index.js @@ -23,8 +23,14 @@ import { appendCsvRow, formatNumber, formatPct, getCandleWindowTiming, sleep } f import { startBinanceTradeStream } from "./data/binanceWs.js"; import fs from "node:fs"; import path from "node:path"; -import readline from "node:readline"; import { applyGlobalProxyFromEnv } from "./net/proxy.js"; +import { + ANSI, screenWidth, sepLine, renderScreen, centerText, + kv, colorPriceLine, formatSignedDelta, + colorByNarrative, formatNarrativeValue, narrativeFromSign, + narrativeFromSlope, formatProbPct, fmtEtTime, getBtcSession, fmtTimeLeft, + safeFileSlug +} from "./display.js"; function countVwapCrosses(closes, vwapSeries, lookback) { if (closes.length < lookback || vwapSeries.length < lookback) return null; @@ -40,242 +46,8 @@ function countVwapCrosses(closes, vwapSeries, lookback) { applyGlobalProxyFromEnv(); -function fmtTimeLeft(mins) { - const totalSeconds = Math.max(0, Math.floor(mins * 60)); - const m = Math.floor(totalSeconds / 60); - const s = totalSeconds % 60; - return `${String(m).padStart(2, "0")}:${String(s).padStart(2, "0")}`; -} - -const ANSI = { - reset: "\x1b[0m", - red: "\x1b[31m", - green: "\x1b[32m", - yellow: "\x1b[33m", - lightRed: "\x1b[91m", - gray: "\x1b[90m", - white: "\x1b[97m", - dim: "\x1b[2m" -}; - -function screenWidth() { - const w = Number(process.stdout?.columns); - return Number.isFinite(w) && w >= 40 ? w : 80; -} - -function sepLine(ch = "─") { - const w = screenWidth(); - return `${ANSI.white}${ch.repeat(w)}${ANSI.reset}`; -} - -function renderScreen(text) { - try { - readline.cursorTo(process.stdout, 0, 0); - readline.clearScreenDown(process.stdout); - } catch { - // ignore - } - process.stdout.write(text); -} - -function stripAnsi(s) { - return String(s).replace(/\x1b\[[0-9;]*m/g, ""); -} - -function padLabel(label, width) { - const visible = stripAnsi(label).length; - if (visible >= width) return label; - return label + " ".repeat(width - visible); -} - -function centerText(text, width) { - const visible = stripAnsi(text).length; - if (visible >= width) return text; - const left = Math.floor((width - visible) / 2); - const right = width - visible - left; - return " ".repeat(left) + text + " ".repeat(right); -} - -const LABEL_W = 16; -function kv(label, value) { - const l = padLabel(String(label), LABEL_W); - return `${l}${value}`; -} - -function section(title) { - return `${ANSI.white}${title}${ANSI.reset}`; -} - -function colorPriceLine({ label, price, prevPrice, decimals = 0, prefix = "" }) { - if (price === null || price === undefined) { - return `${label}: ${ANSI.gray}-${ANSI.reset}`; - } - - const p = Number(price); - const prev = prevPrice === null || prevPrice === undefined ? null : Number(prevPrice); - - let color = ANSI.reset; - let arrow = ""; - if (prev !== null && Number.isFinite(prev) && Number.isFinite(p) && p !== prev) { - if (p > prev) { - color = ANSI.green; - arrow = " ↑"; - } else { - color = ANSI.red; - arrow = " ↓"; - } - } - - const formatted = `${prefix}${formatNumber(p, decimals)}`; - return `${label}: ${color}${formatted}${arrow}${ANSI.reset}`; -} - -function formatSignedDelta(delta, base) { - if (delta === null || base === null || base === 0) return `${ANSI.gray}-${ANSI.reset}`; - const sign = delta > 0 ? "+" : delta < 0 ? "-" : ""; - const pct = (Math.abs(delta) / Math.abs(base)) * 100; - return `${sign}$${Math.abs(delta).toFixed(2)}, ${sign}${pct.toFixed(2)}%`; -} - -function colorByNarrative(text, narrative) { - if (narrative === "LONG") return `${ANSI.green}${text}${ANSI.reset}`; - if (narrative === "SHORT") return `${ANSI.red}${text}${ANSI.reset}`; - return `${ANSI.gray}${text}${ANSI.reset}`; -} - -function formatNarrativeValue(label, value, narrative) { - return `${label}: ${colorByNarrative(value, narrative)}`; -} - -function narrativeFromSign(x) { - if (x === null || x === undefined || !Number.isFinite(Number(x)) || Number(x) === 0) return "NEUTRAL"; - return Number(x) > 0 ? "LONG" : "SHORT"; -} - -function narrativeFromRsi(rsi) { - if (rsi === null || rsi === undefined || !Number.isFinite(Number(rsi))) return "NEUTRAL"; - const v = Number(rsi); - if (v >= 55) return "LONG"; - if (v <= 45) return "SHORT"; - return "NEUTRAL"; -} - -function narrativeFromSlope(slope) { - if (slope === null || slope === undefined || !Number.isFinite(Number(slope)) || Number(slope) === 0) return "NEUTRAL"; - return Number(slope) > 0 ? "LONG" : "SHORT"; -} - -function formatProbPct(p, digits = 0) { - if (p === null || p === undefined || !Number.isFinite(Number(p))) return "-"; - return `${(Number(p) * 100).toFixed(digits)}%`; -} - -function fmtEtTime(now = new Date()) { - try { - return new Intl.DateTimeFormat("en-US", { - timeZone: "America/New_York", - hour: "2-digit", - minute: "2-digit", - second: "2-digit", - hour12: false - }).format(now); - } catch { - return "-"; - } -} - -function getBtcSession(now = new Date()) { - const h = now.getUTCHours(); - const inAsia = h >= 0 && h < 8; - const inEurope = h >= 7 && h < 16; - const inUs = h >= 13 && h < 22; - - if (inEurope && inUs) return "Europe/US overlap"; - if (inAsia && inEurope) return "Asia/Europe overlap"; - if (inAsia) return "Asia"; - if (inEurope) return "Europe"; - if (inUs) return "US"; - return "Off-hours"; -} - -function parsePriceToBeat(market) { - const text = String(market?.question ?? market?.title ?? ""); - if (!text) return null; - const m = text.match(/price\s*to\s*beat[^\d$]*\$?\s*([0-9][0-9,]*(?:\.[0-9]+)?)/i); - if (!m) return null; - const raw = m[1].replace(/,/g, ""); - const n = Number(raw); - return Number.isFinite(n) ? n : null; -} - const dumpedMarkets = new Set(); -function safeFileSlug(x) { - return String(x ?? "") - .toLowerCase() - .replace(/[^a-z0-9_-]+/g, "-") - .replace(/-+/g, "-") - .replace(/(^-|-$)/g, "") - .slice(0, 120); -} - -function extractNumericFromMarket(market) { - const directKeys = [ - "priceToBeat", - "price_to_beat", - "strikePrice", - "strike_price", - "strike", - "threshold", - "thresholdPrice", - "threshold_price", - "targetPrice", - "target_price", - "referencePrice", - "reference_price" - ]; - - for (const k of directKeys) { - const v = market?.[k]; - const n = typeof v === "string" ? Number(v) : typeof v === "number" ? v : NaN; - if (Number.isFinite(n)) return n; - } - - const seen = new Set(); - const stack = [{ obj: market, depth: 0 }]; - - while (stack.length) { - const { obj, depth } = stack.pop(); - if (!obj || typeof obj !== "object") continue; - if (seen.has(obj) || depth > 6) continue; - seen.add(obj); - - const entries = Array.isArray(obj) ? obj.entries() : Object.entries(obj); - for (const [key, value] of entries) { - const k = String(key).toLowerCase(); - if (value && typeof value === "object") { - stack.push({ obj: value, depth: depth + 1 }); - continue; - } - - if (!/(price|strike|threshold|target|beat)/i.test(k)) continue; - - const n = typeof value === "string" ? Number(value) : typeof value === "number" ? value : NaN; - if (!Number.isFinite(n)) continue; - - if (n > 1000 && n < 2_000_000) return n; - } - } - - return null; -} - -function priceToBeatFromPolymarketMarket(market) { - const n = extractNumericFromMarket(market); - if (n !== null) return n; - return parsePriceToBeat(market); -} - const marketCache = { market: null, fetchedAtMs: 0 @@ -404,6 +176,11 @@ async function main() { let prevCurrentPrice = null; let priceToBeatState = { slug: null, value: null, setAtMs: null }; + // Trade outcome tracking (per-market) + let tradeState = { slug: null, side: null, entryMarketPrice: null, priceToBeat: null, lastChainlinkPrice: null, hasSignal: false }; + let runningStats = { wins: 0, losses: 0, totalPnl: 0 }; + let recentOutcomes = []; // { slug, side, won, pnl, ts }[] + const header = [ "timestamp", "entry_minute", @@ -416,7 +193,9 @@ async function main() { "mkt_down", "edge_up", "edge_down", - "recommendation" + "recommendation", + "outcome", + "pnl" ]; while (true) { @@ -595,6 +374,46 @@ async function main() { } const priceToBeat = priceToBeatState.slug === marketSlug ? priceToBeatState.value : null; + + // --- Trade outcome tracking --- + // Detect market settlement: slug changed from a known previous market + if (tradeState.slug !== null && tradeState.slug !== "" && marketSlug !== tradeState.slug) { + if (tradeState.hasSignal && tradeState.priceToBeat !== null && tradeState.lastChainlinkPrice !== null) { + const winner = tradeState.lastChainlinkPrice > tradeState.priceToBeat ? "UP" : "DOWN"; + const won = tradeState.side === winner; + const ep = tradeState.entryMarketPrice ?? 0.5; + const pnl = won ? (1 / ep) - 1 : -1; + if (won) runningStats.wins += 1; + else runningStats.losses += 1; + runningStats.totalPnl += pnl; + recentOutcomes.unshift({ slug: tradeState.slug, side: tradeState.side, won, pnl, ts: new Date().toISOString() }); + if (recentOutcomes.length > 10) recentOutcomes.pop(); + // Write settlement row to CSV + appendCsvRow("./logs/signals.csv", header, [ + new Date().toISOString(), "SETTLED", "0", tradeState.slug, + `${tradeState.side}:${won ? "WIN" : "LOSS"}`, "", "", "", "", "", "", + `${won ? "WIN" : "LOSS"}:${tradeState.side}`, + won ? "WIN" : "LOSS", + pnl.toFixed(4) + ]); + } + // Reset for new market + tradeState = { slug: marketSlug, side: null, entryMarketPrice: null, priceToBeat: null, lastChainlinkPrice: currentPrice, hasSignal: false }; + } else if (tradeState.slug === null || tradeState.slug === "") { + tradeState.slug = marketSlug; + } + + // Record first ENTER signal for this market + if (!tradeState.hasSignal && rec.action === "ENTER" && marketSlug) { + tradeState.side = rec.side; + tradeState.entryMarketPrice = rec.side === "UP" ? marketUp : marketDown; + tradeState.hasSignal = true; + } + + // Always keep last known price and price-to-beat updated + if (currentPrice !== null) tradeState.lastChainlinkPrice = currentPrice; + if (priceToBeat !== null) tradeState.priceToBeat = priceToBeat; + const currentPriceBaseLine = colorPriceLine({ label: "CURRENT PRICE", price: currentPrice, @@ -667,6 +486,12 @@ async function main() { : ANSI.reset) : ANSI.reset; + const recColor = rec.action === "ENTER" ? ANSI.green : ANSI.gray; + const recLabel = rec.action === "ENTER" + ? `► ${rec.side === "UP" ? "BUY UP" : "BUY DOWN"} [${rec.phase} · ${rec.strength}]` + : `NO TRADE [${rec.phase}]`; + const recLine = `${recColor}${recLabel}${ANSI.reset}`; + const lines = [ titleLine, marketLine, @@ -681,6 +506,8 @@ async function main() { kv("Delta 1/3:", deltaLine.split(": ")[1] ?? deltaLine), kv("VWAP:", vwapLine.split(": ")[1] ?? vwapLine), "", + kv("Recommendation:", recLine), + "", sepLine(), "", kv("POLYMARKET:", polyHeaderValue), @@ -698,6 +525,23 @@ async function main() { kv("ET | Session:", `${ANSI.white}${fmtEtTime(new Date())}${ANSI.reset} | ${ANSI.white}${getBtcSession(new Date())}${ANSI.reset}`), "", sepLine(), + "", + (() => { + const total = runningStats.wins + runningStats.losses; + const winRateStr = total > 0 ? `${((runningStats.wins / total) * 100).toFixed(0)}%` : "-"; + const pnlColor = runningStats.totalPnl > 0 ? ANSI.green : runningStats.totalPnl < 0 ? ANSI.red : ANSI.gray; + const pnlSign = runningStats.totalPnl > 0 ? "+" : ""; + const statsLine = `${ANSI.white}TRADE HISTORY${ANSI.reset} W:${ANSI.green}${runningStats.wins}${ANSI.reset} L:${ANSI.red}${runningStats.losses}${ANSI.reset} Win Rate:${ANSI.white}${winRateStr}${ANSI.reset} P&L:${pnlColor}${pnlSign}${runningStats.totalPnl.toFixed(2)} USDC${ANSI.reset}`; + return statsLine; + })(), + ...recentOutcomes.slice(0, 5).map((o, i) => { + const color = o.won ? ANSI.green : ANSI.red; + const label = o.won ? "WIN" : "LOSS"; + const pnlSign = o.pnl > 0 ? "+" : ""; + return `${ANSI.gray} ${i + 1}.${ANSI.reset} ${color}${label}${ANSI.reset} ${ANSI.dim}${o.side}${ANSI.reset} ${color}${pnlSign}${o.pnl.toFixed(2)} USDC${ANSI.reset} ${ANSI.gray}${o.slug.slice(0, 40)}${ANSI.reset}`; + }), + "", + sepLine(), centerText(`${ANSI.dim}${ANSI.gray}created by @krajekis${ANSI.reset}`, screenWidth()) ].filter((x) => x !== null); @@ -718,7 +562,9 @@ async function main() { marketDown, edge.edgeUp, edge.edgeDown, - rec.action === "ENTER" ? `${rec.side}:${rec.phase}:${rec.strength}` : "NO_TRADE" + rec.action === "ENTER" ? `${rec.side}:${rec.phase}:${rec.strength}` : "NO_TRADE", + "", // outcome — preenchido na row SETTLED + "" // pnl — preenchido na row SETTLED ]); } catch (err) { console.log("────────────────────────────"); diff --git a/src/index5m.js b/src/index5m.js new file mode 100644 index 00000000..5135edd9 --- /dev/null +++ b/src/index5m.js @@ -0,0 +1,527 @@ +import { CONFIG } from "./config5m.js"; +import { fetchKlines, fetchLastPrice } from "./data/binance.js"; +import { fetchChainlinkBtcUsd } from "./data/chainlink.js"; +import { startChainlinkPriceStream } from "./data/chainlinkWs.js"; +import { startPolymarketChainlinkPriceStream } from "./data/polymarketLiveWs.js"; +import { + fetchMarketBySlug, + fetchLiveEventsBySeriesId, + flattenEventMarkets, + pickLatestLiveMarket, + fetchClobPrice, + fetchOrderBook, + summarizeOrderBook +} from "./data/polymarket.js"; +import { startBinanceOfiStream } from "./data/binanceWsOfi.js"; +import { computeSessionVwap, computeVwapSeries } from "./indicators/vwap.js"; +import { computeRsi, slopeLast } from "./indicators/rsi.js"; +import { computeHeikenAshi, countConsecutive } from "./indicators/heikenAshi.js"; +import { computeEmaCross } from "./indicators/emaCross.js"; +import { scoreOrderFlow } from "./indicators/orderFlow.js"; +import { computeMomentum, scoreMomentum } from "./indicators/momentum.js"; +import { scoreDirection5m, applyTimeAwareness5m } from "./engines/probability5m.js"; +import { computeEdge, decide5m } from "./engines/edge5m.js"; +import { appendCsvRow, formatNumber, formatPct, getCandleWindowTiming, sleep } from "./utils.js"; +import fs from "node:fs"; +import path from "node:path"; +import { applyGlobalProxyFromEnv } from "./net/proxy.js"; +import { + ANSI, screenWidth, sepLine, renderScreen, centerText, + kv, colorPriceLine, formatSignedDelta, + colorByNarrative, formatNarrativeValue, narrativeFromSign, + narrativeFromSlope, formatProbPct, fmtEtTime, getBtcSession, fmtTimeLeft, + safeFileSlug, priceToBeatFromPolymarketMarket +} from "./display.js"; + +applyGlobalProxyFromEnv(); + +const dumpedMarkets = new Set(); + +const marketCache = { + market: null, + fetchedAtMs: 0 +}; + +async function resolveCurrentMarket() { + if (CONFIG.polymarket.marketSlug) { + return await fetchMarketBySlug(CONFIG.polymarket.marketSlug); + } + + if (!CONFIG.polymarket.autoSelectLatest) return null; + + const now = Date.now(); + if (marketCache.market && now - marketCache.fetchedAtMs < CONFIG.pollIntervalMs) { + return marketCache.market; + } + + const events = await fetchLiveEventsBySeriesId({ seriesId: CONFIG.polymarket.seriesId, limit: 25 }); + const markets = flattenEventMarkets(events); + const picked = pickLatestLiveMarket(markets); + + marketCache.market = picked; + marketCache.fetchedAtMs = now; + return picked; +} + +async function fetchPolymarketSnapshot() { + const market = await resolveCurrentMarket(); + + if (!market) return { ok: false, reason: "market_not_found" }; + + const outcomes = Array.isArray(market.outcomes) ? market.outcomes : (typeof market.outcomes === "string" ? JSON.parse(market.outcomes) : []); + const outcomePrices = Array.isArray(market.outcomePrices) + ? market.outcomePrices + : (typeof market.outcomePrices === "string" ? JSON.parse(market.outcomePrices) : []); + + const clobTokenIds = Array.isArray(market.clobTokenIds) + ? market.clobTokenIds + : (typeof market.clobTokenIds === "string" ? JSON.parse(market.clobTokenIds) : []); + + let upTokenId = null; + let downTokenId = null; + for (let i = 0; i < outcomes.length; i += 1) { + const label = String(outcomes[i]); + const tokenId = clobTokenIds[i] ? String(clobTokenIds[i]) : null; + if (!tokenId) continue; + + if (label.toLowerCase() === CONFIG.polymarket.upOutcomeLabel.toLowerCase()) upTokenId = tokenId; + if (label.toLowerCase() === CONFIG.polymarket.downOutcomeLabel.toLowerCase()) downTokenId = tokenId; + } + + const upIndex = outcomes.findIndex((x) => String(x).toLowerCase() === CONFIG.polymarket.upOutcomeLabel.toLowerCase()); + const downIndex = outcomes.findIndex((x) => String(x).toLowerCase() === CONFIG.polymarket.downOutcomeLabel.toLowerCase()); + + const gammaYes = upIndex >= 0 ? Number(outcomePrices[upIndex]) : null; + const gammaNo = downIndex >= 0 ? Number(outcomePrices[downIndex]) : null; + + if (!upTokenId || !downTokenId) { + return { ok: false, reason: "missing_token_ids", market, outcomes, clobTokenIds, outcomePrices }; + } + + let upBuy = null; + let downBuy = null; + let upBookSummary = { bestBid: null, bestAsk: null, spread: null, bidLiquidity: null, askLiquidity: null }; + let downBookSummary = { bestBid: null, bestAsk: null, spread: null, bidLiquidity: null, askLiquidity: null }; + + try { + const [yesBuy, noBuy, upBook, downBook] = await Promise.all([ + fetchClobPrice({ tokenId: upTokenId, side: "buy" }), + fetchClobPrice({ tokenId: downTokenId, side: "buy" }), + fetchOrderBook({ tokenId: upTokenId }), + fetchOrderBook({ tokenId: downTokenId }) + ]); + + upBuy = yesBuy; + downBuy = noBuy; + upBookSummary = summarizeOrderBook(upBook); + downBookSummary = summarizeOrderBook(downBook); + } catch { + upBuy = null; + downBuy = null; + upBookSummary = { + bestBid: Number(market.bestBid) || null, + bestAsk: Number(market.bestAsk) || null, + spread: Number(market.spread) || null, + bidLiquidity: null, + askLiquidity: null + }; + downBookSummary = { + bestBid: null, + bestAsk: null, + spread: Number(market.spread) || null, + bidLiquidity: null, + askLiquidity: null + }; + } + + return { + ok: true, + market, + tokens: { upTokenId, downTokenId }, + prices: { + up: upBuy ?? gammaYes, + down: downBuy ?? gammaNo + }, + orderbook: { + up: upBookSummary, + down: downBookSummary + } + }; +} + +function ofiLabel(ofi) { + if (!ofi || ofi.total === 0) return "-"; + const pct = (ofi.ofi * 100).toFixed(0); + const sign = ofi.ofi > 0 ? "+" : ""; + return `${sign}${pct}%`; +} + +function ofiNarrative(ofi) { + if (!ofi || ofi.total === 0) return "NEUTRAL"; + if (ofi.ofi > 0.05) return "LONG"; + if (ofi.ofi < -0.05) return "SHORT"; + return "NEUTRAL"; +} + +async function main() { + const ofiStream = startBinanceOfiStream({ symbol: CONFIG.symbol }); + const polymarketLiveStream = startPolymarketChainlinkPriceStream({}); + const chainlinkStream = startChainlinkPriceStream({}); + + let prevSpotPrice = null; + let prevCurrentPrice = null; + let priceToBeatState = { slug: null, value: null, setAtMs: null }; + + let tradeState = { slug: null, side: null, entryMarketPrice: null, priceToBeat: null, lastChainlinkPrice: null, hasSignal: false }; + let runningStats = { wins: 0, losses: 0, totalPnl: 0 }; + let recentOutcomes = []; + + const header = [ + "timestamp", "entry_minute", "time_left_min", "ofi_30s", "ofi_1m", "ofi_2m", + "roc1", "roc3", "ema_cross", "rsi", "signal", + "model_up", "model_down", "mkt_up", "mkt_down", + "edge_up", "edge_down", "recommendation", "outcome", "pnl" + ]; + + while (true) { + const timing = getCandleWindowTiming(CONFIG.candleWindowMinutes); + + const wsTick = ofiStream.getLast(); + const wsPrice = wsTick?.price ?? null; + const ofiData = ofiStream.getOfi(); + + const polymarketWsTick = polymarketLiveStream.getLast(); + const polymarketWsPrice = polymarketWsTick?.price ?? null; + const chainlinkWsTick = chainlinkStream.getLast(); + const chainlinkWsPrice = chainlinkWsTick?.price ?? null; + + try { + const chainlinkPromise = polymarketWsPrice !== null + ? Promise.resolve({ price: polymarketWsPrice, updatedAt: polymarketWsTick?.updatedAt ?? null, source: "polymarket_ws" }) + : chainlinkWsPrice !== null + ? Promise.resolve({ price: chainlinkWsPrice, updatedAt: chainlinkWsTick?.updatedAt ?? null, source: "chainlink_ws" }) + : fetchChainlinkBtcUsd(); + + const [klines1m, lastPrice, chainlink, poly] = await Promise.all([ + fetchKlines({ interval: "1m", limit: 60 }), + fetchLastPrice(), + chainlinkPromise, + fetchPolymarketSnapshot() + ]); + + const settlementMs = poly.ok && poly.market?.endDate ? new Date(poly.market.endDate).getTime() : null; + const settlementLeftMin = settlementMs ? (settlementMs - Date.now()) / 60_000 : null; + const timeLeftMin = settlementLeftMin ?? timing.remainingMinutes; + + // Use only last N candles for short VWAP + const vwapCandles = klines1m.slice(-CONFIG.vwapCandleWindow); + const allCloses = klines1m.map(c => c.close); + const vwapCloses = vwapCandles.map(c => c.close); + + const vwap = computeSessionVwap(vwapCandles); + const vwapSeries = computeVwapSeries(vwapCandles); + const vwapNow = vwapSeries[vwapSeries.length - 1]; + + const lookback = CONFIG.vwapSlopeLookbackMinutes; + const vwapSlope = vwapSeries.length >= lookback ? (vwapNow - vwapSeries[vwapSeries.length - lookback]) / lookback : null; + const vwapDist = vwapNow ? (lastPrice - vwapNow) / vwapNow : null; + + // RSI with shorter period + const rsiNow = computeRsi(allCloses, CONFIG.rsiPeriod); + const rsiSeries = []; + for (let i = 0; i < allCloses.length; i++) { + const sub = allCloses.slice(0, i + 1); + const r = computeRsi(sub, CONFIG.rsiPeriod); + if (r !== null) rsiSeries.push(r); + } + const rsiSlope = slopeLast(rsiSeries, 3); + + // EMA crossover + const emaCross = computeEmaCross(allCloses, CONFIG.emaCrossFast, CONFIG.emaCrossSlow); + + // Heiken Ashi on recent candles + const ha = computeHeikenAshi(klines1m.slice(-10)); + const consec = countConsecutive(ha); + + // Momentum + const momentum = computeMomentum(klines1m); + const momentumScore = scoreMomentum(momentum); + + // Order flow + const orderFlowScore = scoreOrderFlow(ofiData); + + // Score direction (5m model) + const scored = scoreDirection5m({ + orderFlow: orderFlowScore, + momentumScore, + emaCross, + rsi: rsiNow, + rsiSlope, + heikenColor: consec.color, + heikenCount: consec.count, + price: lastPrice, + vwap: vwapNow, + vwapSlope + }); + + const timeAware = applyTimeAwareness5m(scored.rawUp, timeLeftMin, CONFIG.candleWindowMinutes); + + const marketUp = poly.ok ? poly.prices.up : null; + const marketDown = poly.ok ? poly.prices.down : null; + const edge = computeEdge({ modelUp: timeAware.adjustedUp, modelDown: timeAware.adjustedDown, marketYes: marketUp, marketNo: marketDown }); + + const rec = decide5m({ remainingMinutes: timeLeftMin, edgeUp: edge.edgeUp, edgeDown: edge.edgeDown, modelUp: timeAware.adjustedUp, modelDown: timeAware.adjustedDown }); + + // --- Display --- + const lastCandle = klines1m.length ? klines1m[klines1m.length - 1] : null; + const lastClose = lastCandle?.close ?? null; + const close1mAgo = klines1m.length >= 2 ? klines1m[klines1m.length - 2]?.close ?? null : null; + const close3mAgo = klines1m.length >= 4 ? klines1m[klines1m.length - 4]?.close ?? null : null; + const delta1m = lastClose !== null && close1mAgo !== null ? lastClose - close1mAgo : null; + const delta3m = lastClose !== null && close3mAgo !== null ? lastClose - close3mAgo : null; + + const pLong = timeAware?.adjustedUp ?? null; + const pShort = timeAware?.adjustedDown ?? null; + const predictValue = `${ANSI.green}LONG${ANSI.reset} ${ANSI.green}${formatProbPct(pLong, 0)}${ANSI.reset} / ${ANSI.red}SHORT${ANSI.reset} ${ANSI.red}${formatProbPct(pShort, 0)}${ANSI.reset}`; + + const marketUpStr = `${marketUp ?? "-"}${marketUp == null ? "" : "\u00A2"}`; + const marketDownStr = `${marketDown ?? "-"}${marketDown == null ? "" : "\u00A2"}`; + const polyHeaderValue = `${ANSI.green}\u2191 UP${ANSI.reset} ${marketUpStr} | ${ANSI.red}\u2193 DOWN${ANSI.reset} ${marketDownStr}`; + + // OFI display + const ofi30Narrative = ofiNarrative(ofiData.ofi30s); + const ofi1Narrative = ofiNarrative(ofiData.ofi1m); + const ofi2Narrative = ofiNarrative(ofiData.ofi2m); + const ofiValue = `30s:${colorByNarrative(ofiLabel(ofiData.ofi30s), ofi30Narrative)} | 1m:${colorByNarrative(ofiLabel(ofiData.ofi1m), ofi1Narrative)} | 2m:${colorByNarrative(ofiLabel(ofiData.ofi2m), ofi2Narrative)}`; + + // EMA cross display + const emaLabel = emaCross === null ? "-" : emaCross.crossover !== "NONE" + ? `${emaCross.crossover} (${emaCross.expanding ? "expanding" : "flat"})` + : emaCross.bullish + ? `bullish${emaCross.expanding ? " (expanding)" : ""}` + : `bearish${emaCross.expanding ? " (expanding)" : ""}`; + const emaNarrative = emaCross === null ? "NEUTRAL" : emaCross.bullish ? "LONG" : "SHORT"; + + // Momentum display + const momLabel = momentum === null ? "-" : (() => { + const r1 = momentum.roc1 !== null ? `${(momentum.roc1 * 100).toFixed(3)}%` : "-"; + const r3 = momentum.roc3 !== null ? `${(momentum.roc3 * 100).toFixed(3)}%` : "-"; + const acc = momentum.accel !== null ? (momentum.accel > 0 ? " \u2191accel" : momentum.accel < 0 ? " \u2193decel" : "") : ""; + return `1m:${r1} | 3m:${r3}${acc}`; + })(); + const momNarrative = momentum?.roc1 != null ? narrativeFromSign(momentum.roc1) : "NEUTRAL"; + + // RSI + const rsiArrow = rsiSlope !== null && rsiSlope < 0 ? "\u2193" : rsiSlope !== null && rsiSlope > 0 ? "\u2191" : "-"; + const rsiValue = `${formatNumber(rsiNow, 1)} ${rsiArrow}`; + const rsiNarrative = narrativeFromSlope(rsiSlope); + + // HA + const heikenValue = `${consec.color ?? "-"} x${consec.count}`; + const haNarrative = (consec.color ?? "").toLowerCase() === "green" ? "LONG" : (consec.color ?? "").toLowerCase() === "red" ? "SHORT" : "NEUTRAL"; + + // VWAP + const vwapSlopeLabel = vwapSlope === null ? "-" : vwapSlope > 0 ? "UP" : vwapSlope < 0 ? "DOWN" : "FLAT"; + const vwapValue = `${formatNumber(vwapNow, 0)} (${formatPct(vwapDist, 2)}) | slope: ${vwapSlopeLabel}`; + const vwapNarrative = narrativeFromSign(vwapDist); + + // Delta + const delta1Narrative = narrativeFromSign(delta1m); + const delta3Narrative = narrativeFromSign(delta3m); + const deltaValue = `${colorByNarrative(formatSignedDelta(delta1m, lastClose), delta1Narrative)} | ${colorByNarrative(formatSignedDelta(delta3m, lastClose), delta3Narrative)}`; + + const signal = rec.action === "ENTER" ? (rec.side === "UP" ? "BUY UP" : "BUY DOWN") : "NO TRADE"; + + const recColor = rec.action === "ENTER" ? ANSI.green : ANSI.gray; + const recLabel = rec.action === "ENTER" + ? `\u25BA ${rec.side === "UP" ? "BUY UP" : "BUY DOWN"} [${rec.phase} \u00B7 ${rec.strength}]` + : `NO TRADE [${rec.phase}]`; + const recLine = `${recColor}${recLabel}${ANSI.reset}`; + + const spotPrice = wsPrice ?? lastPrice; + const currentPrice = chainlink?.price ?? null; + const marketSlug = poly.ok ? String(poly.market?.slug ?? "") : ""; + const marketStartMs = poly.ok && poly.market?.eventStartTime ? new Date(poly.market.eventStartTime).getTime() : null; + + if (marketSlug && priceToBeatState.slug !== marketSlug) { + priceToBeatState = { slug: marketSlug, value: null, setAtMs: null }; + } + if (priceToBeatState.slug && priceToBeatState.value === null && currentPrice !== null) { + const nowMs = Date.now(); + const okToLatch = marketStartMs === null ? true : nowMs >= marketStartMs; + if (okToLatch) { + priceToBeatState = { slug: priceToBeatState.slug, value: Number(currentPrice), setAtMs: nowMs }; + } + } + const priceToBeat = priceToBeatState.slug === marketSlug ? priceToBeatState.value : null; + + // Trade outcome tracking + if (tradeState.slug !== null && tradeState.slug !== "" && marketSlug !== tradeState.slug) { + if (tradeState.hasSignal && tradeState.priceToBeat !== null && tradeState.lastChainlinkPrice !== null) { + const winner = tradeState.lastChainlinkPrice > tradeState.priceToBeat ? "UP" : "DOWN"; + const won = tradeState.side === winner; + const ep = tradeState.entryMarketPrice ?? 0.5; + const pnl = won ? (1 / ep) - 1 : -1; + if (won) runningStats.wins += 1; + else runningStats.losses += 1; + runningStats.totalPnl += pnl; + recentOutcomes.unshift({ slug: tradeState.slug, side: tradeState.side, won, pnl, ts: new Date().toISOString() }); + if (recentOutcomes.length > 10) recentOutcomes.pop(); + appendCsvRow("./logs/signals_5m.csv", header, [ + new Date().toISOString(), "SETTLED", "0", "", "", "", "", "", "", "", "", + `${tradeState.side}:${won ? "WIN" : "LOSS"}`, "", "", "", "", "", + `${won ? "WIN" : "LOSS"}:${tradeState.side}`, + won ? "WIN" : "LOSS", + pnl.toFixed(4) + ]); + } + tradeState = { slug: marketSlug, side: null, entryMarketPrice: null, priceToBeat: null, lastChainlinkPrice: currentPrice, hasSignal: false }; + } else if (tradeState.slug === null || tradeState.slug === "") { + tradeState.slug = marketSlug; + } + + if (!tradeState.hasSignal && rec.action === "ENTER" && marketSlug) { + tradeState.side = rec.side; + tradeState.entryMarketPrice = rec.side === "UP" ? marketUp : marketDown; + tradeState.hasSignal = true; + } + if (currentPrice !== null) tradeState.lastChainlinkPrice = currentPrice; + if (priceToBeat !== null) tradeState.priceToBeat = priceToBeat; + + // Price to beat display + const currentPriceBaseLine = colorPriceLine({ label: "CURRENT PRICE", price: currentPrice, prevPrice: prevCurrentPrice, decimals: 2, prefix: "$" }); + const ptbDelta = (currentPrice !== null && priceToBeat !== null && Number.isFinite(currentPrice) && Number.isFinite(priceToBeat)) + ? currentPrice - priceToBeat : null; + const ptbDeltaColor = ptbDelta === null ? ANSI.gray : ptbDelta > 0 ? ANSI.green : ptbDelta < 0 ? ANSI.red : ANSI.gray; + const ptbDeltaText = ptbDelta === null + ? `${ANSI.gray}-${ANSI.reset}` + : `${ptbDeltaColor}${ptbDelta > 0 ? "+" : ptbDelta < 0 ? "-" : ""}$${Math.abs(ptbDelta).toFixed(2)}${ANSI.reset}`; + const currentPriceValue = currentPriceBaseLine.split(": ")[1] ?? currentPriceBaseLine; + const currentPriceLine = kv("CURRENT PRICE:", `${currentPriceValue} (${ptbDeltaText})`); + + if (poly.ok && poly.market && priceToBeatState.value === null) { + const slug = safeFileSlug(poly.market.slug || poly.market.id || "market"); + if (slug && !dumpedMarkets.has(slug)) { + dumpedMarkets.add(slug); + try { + fs.mkdirSync("./logs", { recursive: true }); + fs.writeFileSync(path.join("./logs", `polymarket_market_${slug}.json`), JSON.stringify(poly.market, null, 2), "utf8"); + } catch { /* ignore */ } + } + } + + const binanceSpotBaseLine = colorPriceLine({ label: "BTC (Binance)", price: spotPrice, prevPrice: prevSpotPrice, decimals: 0, prefix: "$" }); + const diffLine = (spotPrice !== null && currentPrice !== null && Number.isFinite(spotPrice) && Number.isFinite(currentPrice) && currentPrice !== 0) + ? (() => { + const diffUsd = spotPrice - currentPrice; + const diffPct = (diffUsd / currentPrice) * 100; + const sign = diffUsd > 0 ? "+" : diffUsd < 0 ? "-" : ""; + return ` (${sign}$${Math.abs(diffUsd).toFixed(2)}, ${sign}${Math.abs(diffPct).toFixed(2)}%)`; + })() + : ""; + const binanceSpotValue = (binanceSpotBaseLine + diffLine).split(": ")[1] ?? (binanceSpotBaseLine + diffLine); + const binanceSpotKvLine = kv("BTC (Binance):", binanceSpotValue); + + const titleLine = poly.ok ? `${poly.market?.question ?? "-"}` : "-"; + const marketLine = kv("Market:", poly.ok ? (poly.market?.slug ?? "-") : "-"); + + const timeColor = timeLeftMin >= 3 ? ANSI.green : timeLeftMin >= 1.5 ? ANSI.yellow : ANSI.red; + + const polyTimeLeftColor = settlementLeftMin !== null + ? (settlementLeftMin >= 3 ? ANSI.green : settlementLeftMin >= 1.5 ? ANSI.yellow : ANSI.red) + : ANSI.reset; + + const liquidity = poly.ok ? (Number(poly.market?.liquidityNum) || Number(poly.market?.liquidity) || null) : null; + + const lines = [ + `${ANSI.yellow}[5m MODE]${ANSI.reset} ${titleLine}`, + marketLine, + kv("Time left:", `${timeColor}${fmtTimeLeft(timeLeftMin)}${ANSI.reset}`), + "", + sepLine(), + "", + kv("ORDER FLOW:", ofiValue), + kv("Momentum:", formatNarrativeValue("", momLabel, momNarrative).slice(2)), + kv("EMA Cross:", formatNarrativeValue("", emaLabel, emaNarrative).slice(2)), + kv("RSI:", formatNarrativeValue("", rsiValue, rsiNarrative).slice(2)), + kv("Heiken Ashi:", formatNarrativeValue("", heikenValue, haNarrative).slice(2)), + kv("VWAP:", formatNarrativeValue("", vwapValue, vwapNarrative).slice(2)), + kv("Delta 1/3:", deltaValue), + "", + kv("TA Predict:", predictValue), + kv("Recommendation:", recLine), + "", + sepLine(), + "", + kv("POLYMARKET:", polyHeaderValue), + liquidity !== null ? kv("Liquidity:", formatNumber(liquidity, 0)) : null, + settlementLeftMin !== null ? kv("Time left:", `${polyTimeLeftColor}${fmtTimeLeft(settlementLeftMin)}${ANSI.reset}`) : null, + priceToBeat !== null ? kv("PRICE TO BEAT: ", `$${formatNumber(priceToBeat, 0)}`) : kv("PRICE TO BEAT: ", `${ANSI.gray}-${ANSI.reset}`), + currentPriceLine, + "", + sepLine(), + "", + binanceSpotKvLine, + "", + sepLine(), + "", + kv("ET | Session:", `${ANSI.white}${fmtEtTime(new Date())}${ANSI.reset} | ${ANSI.white}${getBtcSession(new Date())}${ANSI.reset}`), + "", + sepLine(), + "", + (() => { + const total = runningStats.wins + runningStats.losses; + const winRateStr = total > 0 ? `${((runningStats.wins / total) * 100).toFixed(0)}%` : "-"; + const pnlColor = runningStats.totalPnl > 0 ? ANSI.green : runningStats.totalPnl < 0 ? ANSI.red : ANSI.gray; + const pnlSign = runningStats.totalPnl > 0 ? "+" : ""; + return `${ANSI.white}TRADE HISTORY${ANSI.reset} W:${ANSI.green}${runningStats.wins}${ANSI.reset} L:${ANSI.red}${runningStats.losses}${ANSI.reset} Win Rate:${ANSI.white}${winRateStr}${ANSI.reset} P&L:${pnlColor}${pnlSign}${runningStats.totalPnl.toFixed(2)} USDC${ANSI.reset}`; + })(), + ...recentOutcomes.slice(0, 5).map((o, i) => { + const color = o.won ? ANSI.green : ANSI.red; + const label = o.won ? "WIN" : "LOSS"; + const pnlSign = o.pnl > 0 ? "+" : ""; + return `${ANSI.gray} ${i + 1}.${ANSI.reset} ${color}${label}${ANSI.reset} ${ANSI.dim}${o.side}${ANSI.reset} ${color}${pnlSign}${o.pnl.toFixed(2)} USDC${ANSI.reset} ${ANSI.gray}${o.slug.slice(0, 40)}${ANSI.reset}`; + }), + "", + sepLine(), + centerText(`${ANSI.dim}${ANSI.gray}created by @krajekis${ANSI.reset}`, screenWidth()) + ].filter(x => x !== null); + + renderScreen(lines.join("\n") + "\n"); + + prevSpotPrice = spotPrice ?? prevSpotPrice; + prevCurrentPrice = currentPrice ?? prevCurrentPrice; + + appendCsvRow("./logs/signals_5m.csv", header, [ + new Date().toISOString(), + timing.elapsedMinutes.toFixed(3), + timeLeftMin.toFixed(3), + ofiData.ofi30s?.ofi?.toFixed(3) ?? "", + ofiData.ofi1m?.ofi?.toFixed(3) ?? "", + ofiData.ofi2m?.ofi?.toFixed(3) ?? "", + momentum?.roc1?.toFixed(6) ?? "", + momentum?.roc3?.toFixed(6) ?? "", + emaCross?.crossover ?? "", + rsiNow?.toFixed(1) ?? "", + signal, + timeAware.adjustedUp, + timeAware.adjustedDown, + marketUp, + marketDown, + edge.edgeUp, + edge.edgeDown, + rec.action === "ENTER" ? `${rec.side}:${rec.phase}:${rec.strength}` : "NO_TRADE", + "", + "" + ]); + } catch (err) { + console.log("\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"); + console.log(`Error: ${err?.message ?? String(err)}`); + console.log("\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"); + } + + await sleep(CONFIG.pollIntervalMs); + } +} + +main(); diff --git a/src/indicators/emaCross.js b/src/indicators/emaCross.js new file mode 100644 index 00000000..511441c4 --- /dev/null +++ b/src/indicators/emaCross.js @@ -0,0 +1,41 @@ +// Fast EMA crossover for 5m mode. +// Replaces MACD(12,26,9) which is too slow for a 5-minute window. + +function ema(values, period) { + if (!Array.isArray(values) || values.length < period) return null; + const k = 2 / (period + 1); + let prev = values[0]; + for (let i = 1; i < values.length; i++) { + prev = values[i] * k + prev * (1 - k); + } + return prev; +} + +export function computeEmaCross(closes, fastPeriod = 3, slowPeriod = 8) { + const fast = ema(closes, fastPeriod); + const slow = ema(closes, slowPeriod); + if (fast === null || slow === null) return null; + + // Previous values for crossover detection + const prevFast = closes.length > 1 ? ema(closes.slice(0, -1), fastPeriod) : null; + const prevSlow = closes.length > 1 ? ema(closes.slice(0, -1), slowPeriod) : null; + + const diff = fast - slow; + const prevDiff = (prevFast !== null && prevSlow !== null) ? prevFast - prevSlow : null; + + let crossover = "NONE"; + if (prevDiff !== null) { + if (prevDiff <= 0 && diff > 0) crossover = "BULLISH"; + else if (prevDiff >= 0 && diff < 0) crossover = "BEARISH"; + } + + return { + fast, + slow, + diff, + prevDiff, + crossover, + bullish: diff > 0, + expanding: prevDiff !== null ? Math.abs(diff) > Math.abs(prevDiff) : false + }; +} diff --git a/src/indicators/momentum.js b/src/indicators/momentum.js new file mode 100644 index 00000000..9d098ded --- /dev/null +++ b/src/indicators/momentum.js @@ -0,0 +1,75 @@ +// Price momentum indicators for 5m mode. +// Rate of change and acceleration over short windows. + +export function computeMomentum(candles) { + if (!Array.isArray(candles) || candles.length < 2) return null; + + const last = candles[candles.length - 1]; + const closes = candles.map(c => c.close); + + // Rate of change over last 1 candle (1m) + const roc1 = candles.length >= 2 + ? (closes[closes.length - 1] - closes[closes.length - 2]) / closes[closes.length - 2] + : null; + + // Rate of change over last 3 candles (3m) + const roc3 = candles.length >= 4 + ? (closes[closes.length - 1] - closes[closes.length - 4]) / closes[closes.length - 4] + : null; + + // Acceleration: change in roc1 over last 2 ticks + let accel = null; + if (candles.length >= 3) { + const prevRoc1 = (closes[closes.length - 2] - closes[closes.length - 3]) / closes[closes.length - 3]; + accel = roc1 - prevRoc1; + } + + // Volume surge: last candle vs average of previous 5 + let volumeSurge = null; + if (candles.length >= 6) { + const avgVol = candles.slice(-6, -1).reduce((a, c) => a + c.volume, 0) / 5; + volumeSurge = avgVol > 0 ? last.volume / avgVol : null; + } + + return { + roc1, + roc3, + accel, + volumeSurge, + lastClose: last.close + }; +} + +export function scoreMomentum(momentum) { + if (!momentum) return { up: 0, down: 0 }; + + let up = 0; + let down = 0; + + // Rate of change signals + if (momentum.roc1 !== null) { + if (momentum.roc1 > 0.0003) up += 1; + if (momentum.roc1 < -0.0003) down += 1; + if (momentum.roc1 > 0.001) up += 1; + if (momentum.roc1 < -0.001) down += 1; + } + + if (momentum.roc3 !== null) { + if (momentum.roc3 > 0.0005) up += 1; + if (momentum.roc3 < -0.0005) down += 1; + } + + // Acceleration — momentum building + if (momentum.accel !== null) { + if (momentum.accel > 0.0001 && momentum.roc1 > 0) up += 1; + if (momentum.accel < -0.0001 && momentum.roc1 < 0) down += 1; + } + + // Volume surge confirms direction + if (momentum.volumeSurge !== null && momentum.volumeSurge > 1.5) { + if (momentum.roc1 !== null && momentum.roc1 > 0) up += 1; + if (momentum.roc1 !== null && momentum.roc1 < 0) down += 1; + } + + return { up, down }; +} diff --git a/src/indicators/orderFlow.js b/src/indicators/orderFlow.js new file mode 100644 index 00000000..26a17984 --- /dev/null +++ b/src/indicators/orderFlow.js @@ -0,0 +1,35 @@ +// Order Flow Imbalance scoring for 5m mode. +// Converts raw OFI data (from binanceWsOfi) into directional signals. + +export function scoreOrderFlow({ ofi30s, ofi1m, ofi2m }) { + let up = 0; + let down = 0; + + // 30s window — most reactive, captures immediate momentum + if (ofi30s && ofi30s.total > 0) { + if (ofi30s.ofi > 0.15) up += 2; + else if (ofi30s.ofi > 0.05) up += 1; + if (ofi30s.ofi < -0.15) down += 2; + else if (ofi30s.ofi < -0.05) down += 1; + } + + // 1m window — confirms direction + if (ofi1m && ofi1m.total > 0) { + if (ofi1m.ofi > 0.10) up += 2; + else if (ofi1m.ofi > 0.03) up += 1; + if (ofi1m.ofi < -0.10) down += 2; + else if (ofi1m.ofi < -0.03) down += 1; + } + + // 2m window — structural pressure + if (ofi2m && ofi2m.total > 0) { + if (ofi2m.ofi > 0.08) up += 1; + if (ofi2m.ofi < -0.08) down += 1; + } + + // Alignment bonus: all three windows agree + if (ofi30s?.ofi > 0.05 && ofi1m?.ofi > 0.03 && ofi2m?.ofi > 0.03) up += 1; + if (ofi30s?.ofi < -0.05 && ofi1m?.ofi < -0.03 && ofi2m?.ofi < -0.03) down += 1; + + return { up, down }; +} From 57536852455a64ff40f1b24548158e67c3c73ce2 Mon Sep 17 00:00:00 2001 From: Jean Carraro Date: Fri, 3 Apr 2026 10:14:48 -0300 Subject: [PATCH 02/49] 5m: show market interval, detect next market, fix title visibility MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - display.js: enter alternate screen buffer on first render so title always appears at row 1; truncate output to terminal height to prevent scroll overflow; add fmtEtHHMM() helper (HH:MM without seconds) - index5m.js: show "Interval: HH:MM → HH:MM ET" below the market slug; detect when the active market hasn't started yet and label it [PRÓXIMO MERCADO] in yellow, replace "Time left" with "Inicia em:" showing time until start Co-Authored-By: Claude Sonnet 4.6 --- CLAUDE.md | 4 ++-- src/display.js | 33 ++++++++++++++++++++++++++++++++- src/index5m.js | 20 ++++++++++++++++---- 3 files changed, 50 insertions(+), 7 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index 7239c087..b6e49c41 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -27,7 +27,7 @@ Price source priority: `polymarketLiveWs` → `chainlinkWs` → `chainlink` HTTP ### Shared display (`src/display.js`) -All terminal rendering helpers (ANSI colors, `kv()`, `renderScreen()`, `colorPriceLine()`, etc.) shared by both 15m and 5m modes. +All terminal rendering helpers (ANSI colors, `kv()`, `renderScreen()`, `colorPriceLine()`, `fmtEtTime()`, `fmtEtHHMM()`, etc.) shared by both 15m and 5m modes. `renderScreen()` enters the terminal alternate screen buffer on first call (so the title always appears at row 1) and truncates output to `process.stdout.rows - 1` lines to prevent scroll overflow. Restores the normal screen on exit/SIGINT/SIGTERM. ### Indicators (`src/indicators/`) @@ -76,7 +76,7 @@ Called once at startup via `applyGlobalProxyFromEnv()`. Reads `HTTPS_PROXY`/`HTT ## Output -- Terminal screen refreshed every second via `readline.cursorTo` + `clearScreenDown`. +- Terminal screen refreshed every second via ANSI escape codes (`\x1b[H` + per-line `\x1b[K` + `\x1b[J`), rendered inside an alternate screen buffer. - `./logs/signals.csv` — one row per poll tick (15m mode) with regime, signal, model probabilities, market prices, edge, and recommendation. - `./logs/signals_5m.csv` — one row per poll tick (5m mode) with OFI, momentum, EMA cross, RSI, model probs, edge, and recommendation. - `./logs/polymarket_market_.json` — raw Polymarket market JSON dumped once per new market slug. diff --git a/src/display.js b/src/display.js index 325f5693..d5aa98f1 100644 --- a/src/display.js +++ b/src/display.js @@ -21,9 +21,26 @@ export function sepLine(ch = "\u2500") { return `${ANSI.white}${ch.repeat(w)}${ANSI.reset}`; } +let _screenInitialized = false; + +function initAlternateScreen() { + process.stdout.write("\x1b[?1049h\x1b[H"); + const restoreScreen = () => { + try { process.stdout.write("\x1b[?1049l"); } catch { /* ignore */ } + }; + process.once("exit", restoreScreen); + process.once("SIGINT", () => { restoreScreen(); process.exit(0); }); + process.once("SIGTERM", () => { restoreScreen(); process.exit(0); }); +} + export function renderScreen(text) { try { - const lines = text.split("\n"); + if (!_screenInitialized) { + initAlternateScreen(); + _screenInitialized = true; + } + const maxRows = (process.stdout.rows ?? 24) - 1; + const lines = text.split("\n").slice(0, maxRows); const output = "\x1b[H" + lines.map(l => l + "\x1b[K").join("\n") + "\x1b[J"; process.stdout.write(output); } catch { @@ -146,6 +163,20 @@ export function fmtEtTime(now = new Date()) { } } +export function fmtEtHHMM(dateOrMs) { + try { + const d = typeof dateOrMs === "number" ? new Date(dateOrMs) : dateOrMs; + return new Intl.DateTimeFormat("en-US", { + timeZone: "America/New_York", + hour: "2-digit", + minute: "2-digit", + hour12: false + }).format(d); + } catch { + return "-"; + } +} + export function getBtcSession(now = new Date()) { const h = now.getUTCHours(); const inAsia = h >= 0 && h < 8; diff --git a/src/index5m.js b/src/index5m.js index 5135edd9..969a1e70 100644 --- a/src/index5m.js +++ b/src/index5m.js @@ -29,7 +29,7 @@ import { ANSI, screenWidth, sepLine, renderScreen, centerText, kv, colorPriceLine, formatSignedDelta, colorByNarrative, formatNarrativeValue, narrativeFromSign, - narrativeFromSlope, formatProbPct, fmtEtTime, getBtcSession, fmtTimeLeft, + narrativeFromSlope, formatProbPct, fmtEtTime, fmtEtHHMM, getBtcSession, fmtTimeLeft, safeFileSlug, priceToBeatFromPolymarketMarket } from "./display.js"; @@ -422,10 +422,19 @@ async function main() { const binanceSpotValue = (binanceSpotBaseLine + diffLine).split(": ")[1] ?? (binanceSpotBaseLine + diffLine); const binanceSpotKvLine = kv("BTC (Binance):", binanceSpotValue); + const isNextMarket = marketStartMs !== null && marketStartMs > Date.now(); + const modeTag = isNextMarket ? `${ANSI.yellow}[5m MODE]${ANSI.reset} ${ANSI.yellow}[PRÓXIMO MERCADO]${ANSI.reset}` : `${ANSI.yellow}[5m MODE]${ANSI.reset}`; const titleLine = poly.ok ? `${poly.market?.question ?? "-"}` : "-"; const marketLine = kv("Market:", poly.ok ? (poly.market?.slug ?? "-") : "-"); + const intervalLabel = isNextMarket ? "Próx. intervalo:" : "Interval:"; + const intervalLine = (marketStartMs !== null && settlementMs !== null) + ? kv(intervalLabel, `${isNextMarket ? ANSI.yellow : ""}${fmtEtHHMM(marketStartMs)} → ${fmtEtHHMM(settlementMs)} ET${isNextMarket ? ANSI.reset : ""}`) + : null; - const timeColor = timeLeftMin >= 3 ? ANSI.green : timeLeftMin >= 1.5 ? ANSI.yellow : ANSI.red; + const timeColor = isNextMarket + ? ANSI.yellow + : timeLeftMin >= 3 ? ANSI.green : timeLeftMin >= 1.5 ? ANSI.yellow : ANSI.red; + const startsInMin = isNextMarket && marketStartMs !== null ? (marketStartMs - Date.now()) / 60_000 : null; const polyTimeLeftColor = settlementLeftMin !== null ? (settlementLeftMin >= 3 ? ANSI.green : settlementLeftMin >= 1.5 ? ANSI.yellow : ANSI.red) @@ -434,9 +443,12 @@ async function main() { const liquidity = poly.ok ? (Number(poly.market?.liquidityNum) || Number(poly.market?.liquidity) || null) : null; const lines = [ - `${ANSI.yellow}[5m MODE]${ANSI.reset} ${titleLine}`, + `${modeTag} ${titleLine}`, marketLine, - kv("Time left:", `${timeColor}${fmtTimeLeft(timeLeftMin)}${ANSI.reset}`), + intervalLine, + isNextMarket && startsInMin !== null + ? kv("Inicia em:", `${ANSI.yellow}${fmtTimeLeft(startsInMin)}${ANSI.reset}`) + : kv("Time left:", `${timeColor}${fmtTimeLeft(timeLeftMin)}${ANSI.reset}`), "", sepLine(), "", From 796b09418812566589c9dd5bd448c39ac6d7b326 Mon Sep 17 00:00:00 2001 From: Jean Carraro Date: Fri, 3 Apr 2026 15:03:44 -0300 Subject: [PATCH 03/49] Add Polymarket trading integration + env file support Trading (src/trading/): - client.js: ClobClient init with L1+L2 auth via @polymarket/clob-client - orders.js: buyMarketOrder / sellMarketOrder wrappers - position.js: in-memory position state, ROI, chain balance sync - Keypress [B]/[S]/[Q] in both 15m and 5m loops when POLYMARKET_PRIVATE_KEY is set - Position + ROI section in TUI display Config / env: - Hardcode series IDs: 15m=10192, 5m=10684 (not overridable) - node --env-file=.env in npm start / start:5m - .env.example with all configurable variables - .gitignore: allow .env.example, keep .env ignored Co-Authored-By: Claude Sonnet 4.6 --- .env.example | 57 ++++ .gitignore | 1 + CLAUDE.md | 14 + package-lock.json | 583 ++++++++++++++++++++++++++++++++++++++++ package.json | 5 +- src/config.js | 11 +- src/config5m.js | 5 +- src/display.js | 57 ++++ src/index.js | 84 +++++- src/index5m.js | 85 +++++- src/trading/client.js | 46 ++++ src/trading/orders.js | 27 ++ src/trading/position.js | 75 ++++++ 13 files changed, 1041 insertions(+), 9 deletions(-) create mode 100644 .env.example create mode 100644 src/trading/client.js create mode 100644 src/trading/orders.js create mode 100644 src/trading/position.js diff --git a/.env.example b/.env.example new file mode 100644 index 00000000..995fb60b --- /dev/null +++ b/.env.example @@ -0,0 +1,57 @@ +# ───────────────────────────────────────────── +# Polymarket – seleção de mercado +# ───────────────────────────────────────────── + +# Fixar um mercado específico pelo slug (sobrescreve a seleção automática) +# POLYMARKET_SLUG=btc-updown-15m-1234567890 + +# Desativar a seleção automática do mercado mais recente (padrão: true) +# POLYMARKET_AUTO_SELECT_LATEST=true + +# Labels dos outcomes (padrão: Up / Down) +# POLYMARKET_UP_LABEL=Up +# POLYMARKET_DOWN_LABEL=Down + +# URL do WebSocket de dados ao vivo da Polymarket +# POLYMARKET_LIVE_WS_URL=wss://ws-live-data.polymarket.com + +# ───────────────────────────────────────────── +# Trading real (opcional – deixar vazio = modo leitura) +# ───────────────────────────────────────────── + +# Chave privada da carteira Polygon (habilita ordens reais) +# POLYMARKET_PRIVATE_KEY=0x... + +# Endereço do perfil Polymarket (necessário se a conta foi criada pelo site/email) +# POLYMARKET_FUNDER=0x... + +# Tipo de assinatura: 0=EOA (carteira direta), 1=POLY_PROXY (login por email), 2=GNOSIS_SAFE +# POLYMARKET_SIGNATURE_TYPE=0 + +# Valor em USDC por trade (padrão: 5) +# POLYMARKET_TRADE_AMOUNT=5 + +# ───────────────────────────────────────────── +# Chainlink / Polygon RPC +# ───────────────────────────────────────────── + +# RPC HTTP principal do Polygon (fallback de preço Chainlink) +# POLYGON_RPC_URL=https://polygon-rpc.com + +# Lista de RPCs HTTP alternativos separados por vírgula +# POLYGON_RPC_URLS=https://polygon-rpc.com,https://rpc.ankr.com/polygon + +# RPCs WebSocket para preço Chainlink em tempo real +# POLYGON_WSS_URLS=wss://polygon-bor-rpc.publicnode.com + +# Endereço do agregador BTC/USD da Chainlink no Polygon +# CHAINLINK_BTC_USD_AGGREGATOR=0xc907E116054Ad103354f2D350FD2514433D57F6f + +# ───────────────────────────────────────────── +# Proxy (opcional) +# ───────────────────────────────────────────── + +# Proxy HTTP/HTTPS para todas as conexões de saída +# HTTPS_PROXY=http://user:pass@host:port +# HTTP_PROXY=http://user:pass@host:port +# ALL_PROXY=socks5://user:pass@host:port diff --git a/.gitignore b/.gitignore index f8776465..c8e21f71 100644 --- a/.gitignore +++ b/.gitignore @@ -2,6 +2,7 @@ node_modules/ logs/ .env .env.* +!.env.example .DS_Store npm-debug.log* yarn-debug.log* diff --git a/CLAUDE.md b/CLAUDE.md index b6e49c41..bd6ff144 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -57,6 +57,16 @@ Pure functions operating on arrays of OHLCV candles (Binance 1m klines): All tunable parameters (poll interval, TA periods, Polymarket series IDs, RPC URLs) live here and are read from environment variables with defaults. `config5m.js` extends the base config with 5m-tuned values (RSI period 5, VWAP window 10 candles, EMA 3/8). +### Trading (`src/trading/`) + +Optional live-trading integration using `@polymarket/clob-client` SDK. Enabled when `POLYMARKET_PRIVATE_KEY` is set; otherwise the app runs in read-only mode. + +- **client.js** — Initializes `ClobClient` with L1 (EIP-712) + L2 (HMAC) auth. Derives API credentials on first run via `createOrDeriveApiKey()`. Caches the client singleton. +- **orders.js** — `buyMarketOrder()` and `sellMarketOrder()` wrappers around `client.createAndPostMarketOrder()`. Returns `{ ok, order }` or `{ ok: false, error }`. +- **position.js** — In-memory position state: `recordBuy()`, `recordSell()`, `getPosition()`, `computeROI()`, `resetIfMarketChanged()`. Also `fetchPositionBalance()` to sync shares from chain via `getBalanceAllowance()`. + +Both main loops listen for keypresses when trading is enabled: **[B]** buy the recommended side, **[S]** sell 100% of position, **[Q]** quit. Actions are queued and processed inside the main loop where market data is available. + ### Proxy (`src/net/proxy.js`) Called once at startup via `applyGlobalProxyFromEnv()`. Reads `HTTPS_PROXY`/`HTTP_PROXY`/`ALL_PROXY` and patches Node's global `fetch` dispatcher (via `undici`) and WebSocket connections to route through HTTP or SOCKS5 proxies. @@ -73,6 +83,10 @@ Called once at startup via `applyGlobalProxyFromEnv()`. Reads `HTTPS_PROXY`/`HTT | `POLYMARKET_5M_SERIES_ID` | (falls back to 15m series) | Series ID for 5m markets | | `POLYMARKET_5M_SERIES_SLUG` | `btc-up-or-down-5m` | Series slug for 5m markets | | `HTTPS_PROXY` / `ALL_PROXY` | — | Proxy for all outbound connections | +| `POLYMARKET_PRIVATE_KEY` | — | Polygon wallet private key (enables trading) | +| `POLYMARKET_FUNDER` | (derived from key) | Polymarket profile address (for proxy wallets) | +| `POLYMARKET_SIGNATURE_TYPE` | `0` | `0`=EOA, `1`=POLY_PROXY, `2`=GNOSIS_SAFE | +| `POLYMARKET_TRADE_AMOUNT` | `5` | USDC amount per trade | ## Output diff --git a/package-lock.json b/package-lock.json index bad5fcd5..34f5d3fd 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,6 +8,7 @@ "name": "polyassistent", "version": "0.1.0", "dependencies": { + "@polymarket/clob-client": "^5.8.1", "ethers": "^6.11.1", "https-proxy-agent": "^7.0.6", "socks-proxy-agent": "^8.0.5", @@ -21,6 +22,18 @@ "integrity": "sha512-96Z2IP3mYmF1Xg2cDm8f1gWGf/HUVedQ3FMifV4kG/PQ4yEP51xDtRAEfhVNt5f/uzpNkZHwWQuUcu6D6K+Ekw==", "license": "MIT" }, + "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", @@ -45,6 +58,105 @@ "url": "https://paulmillr.com/funding/" } }, + "node_modules/@polymarket/builder-signing-sdk": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@polymarket/builder-signing-sdk/-/builder-signing-sdk-1.0.0.tgz", + "integrity": "sha512-LNHc8Ox+2ITM6+VIEH5LH3SVh9ScE2mOUAJI789YfyNqhF4n1cNulk35Hb6IZfy1xtNfcEfNotanPLa/U8dCaw==", + "license": "MIT", + "dependencies": { + "axios": "^1.12.2" + } + }, + "node_modules/@polymarket/clob-client": { + "version": "5.8.1", + "resolved": "https://registry.npmjs.org/@polymarket/clob-client/-/clob-client-5.8.1.tgz", + "integrity": "sha512-beKC4KPc9dYyFV9wFhKiGoA16q5xWz4PjkYcnEs3BzpsOKjGQGRlZQ7KQD4HQKUd3HC5RLQT0lqWHrFyaI+2fQ==", + "license": "MIT", + "dependencies": { + "@polymarket/builder-signing-sdk": "^1.0.0", + "axios": "^1.0.0", + "browser-or-node": "^2.1.1", + "viem": "^2.46.3" + }, + "engines": { + "node": ">=20.10" + } + }, + "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/node": { "version": "22.7.5", "resolved": "https://registry.npmjs.org/@types/node/-/node-22.7.5.tgz", @@ -54,6 +166,27 @@ "undici-types": "~6.19.2" } }, + "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/aes-js": { "version": "4.0.0-beta.5", "resolved": "https://registry.npmjs.org/aes-js/-/aes-js-4.0.0-beta.5.tgz", @@ -69,6 +202,54 @@ "node": ">= 14" } }, + "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/browser-or-node": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/browser-or-node/-/browser-or-node-2.1.1.tgz", + "integrity": "sha512-8CVjaLJGuSKMVTxJ2DpBl5XnlNDiT4cQFeuCJJrvJmts9YrTZDizTX7PjC2s6W4x+MBGZeEY6dGMrF04/6Hgqg==", + "license": "MIT" + }, + "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/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/debug": { "version": "4.4.3", "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", @@ -86,6 +267,74 @@ } } }, + "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/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/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/ethers": { "version": "6.16.0", "resolved": "https://registry.npmjs.org/ethers/-/ethers-6.16.0.tgz", @@ -135,6 +384,145 @@ } } }, + "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/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/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/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/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/https-proxy-agent": { "version": "7.0.6", "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz", @@ -157,12 +545,129 @@ "node": ">= 12" } }, + "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/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/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/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/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/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/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/smart-buffer": { "version": "4.2.0", "resolved": "https://registry.npmjs.org/smart-buffer/-/smart-buffer-4.2.0.tgz", @@ -222,6 +727,84 @@ "integrity": "sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==", "license": "MIT" }, + "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/ws": { "version": "8.19.0", "resolved": "https://registry.npmjs.org/ws/-/ws-8.19.0.tgz", diff --git a/package.json b/package.json index 388fc24b..5139278f 100644 --- a/package.json +++ b/package.json @@ -4,10 +4,11 @@ "type": "module", "private": true, "scripts": { - "start": "node src/index.js", - "start:5m": "node src/index5m.js" + "start": "node --env-file=.env src/index.js", + "start:5m": "node --env-file=.env src/index5m.js" }, "dependencies": { + "@polymarket/clob-client": "^5.8.1", "ethers": "^6.11.1", "https-proxy-agent": "^7.0.6", "socks-proxy-agent": "^8.0.5", diff --git a/src/config.js b/src/config.js index 0b105979..00dc9300 100644 --- a/src/config.js +++ b/src/config.js @@ -17,14 +17,21 @@ export const CONFIG = { polymarket: { marketSlug: process.env.POLYMARKET_SLUG || "", - seriesId: process.env.POLYMARKET_SERIES_ID || "10192", - seriesSlug: process.env.POLYMARKET_SERIES_SLUG || "btc-up-or-down-15m", + seriesId: "10192", + seriesSlug: "btc-up-or-down-15m", autoSelectLatest: (process.env.POLYMARKET_AUTO_SELECT_LATEST || "true").toLowerCase() === "true", liveDataWsUrl: process.env.POLYMARKET_LIVE_WS_URL || "wss://ws-live-data.polymarket.com", upOutcomeLabel: process.env.POLYMARKET_UP_LABEL || "Up", downOutcomeLabel: process.env.POLYMARKET_DOWN_LABEL || "Down" }, + trading: { + privateKey: process.env.POLYMARKET_PRIVATE_KEY || "", + funder: process.env.POLYMARKET_FUNDER || "", + signatureType: Number(process.env.POLYMARKET_SIGNATURE_TYPE || "0"), + tradeAmount: Number(process.env.POLYMARKET_TRADE_AMOUNT || "5"), + }, + chainlink: { polygonRpcUrls: (process.env.POLYGON_RPC_URLS || "").split(",").map((s) => s.trim()).filter(Boolean), polygonRpcUrl: process.env.POLYGON_RPC_URL || "https://polygon-rpc.com", diff --git a/src/config5m.js b/src/config5m.js index 55534e61..469af824 100644 --- a/src/config5m.js +++ b/src/config5m.js @@ -20,8 +20,7 @@ export const CONFIG = { polymarket: { ...BASE.polymarket, - // 5m series — will need to be set via env vars - seriesId: process.env.POLYMARKET_5M_SERIES_ID || BASE.polymarket.seriesId, - seriesSlug: process.env.POLYMARKET_5M_SERIES_SLUG || "btc-up-or-down-5m" + seriesId: "10684", + seriesSlug: "btc-up-or-down-5m" } }; diff --git a/src/display.js b/src/display.js index d5aa98f1..4a7fedcf 100644 --- a/src/display.js +++ b/src/display.js @@ -264,3 +264,60 @@ export function priceToBeatFromPolymarketMarket(market) { if (n !== null) return n; return parsePriceToBeat(market); } + +// --- Trading display helpers --- + +let _statusMsg = { text: "", expiresAt: 0 }; + +export function setStatusMessage(text, durationMs = 3000) { + _statusMsg = { text, expiresAt: Date.now() + durationMs }; +} + +export function getStatusLine() { + if (_statusMsg.expiresAt > Date.now() && _statusMsg.text) { + return `${ANSI.yellow}${_statusMsg.text}${ANSI.reset}`; + } + return null; +} + +export function formatPositionLines({ position, currentMarketPrice, tradingEnabled }) { + if (!tradingEnabled) return []; + + const lines = []; + lines.push(sepLine()); + lines.push(""); + + const statusLine = getStatusLine(); + if (statusLine) { + lines.push(statusLine); + lines.push(""); + } + + if (!position.active) { + lines.push(kv("POSITION:", `${ANSI.gray}Nenhuma posição aberta${ANSI.reset}`)); + } else { + const sideColor = position.side === "UP" ? ANSI.green : ANSI.red; + const sideLabel = position.side === "UP" ? "↑ UP" : "↓ DOWN"; + const sharesStr = position.shares.toFixed(2); + const entryStr = (position.entryPrice * 100).toFixed(1) + "¢"; + + const roi = currentMarketPrice != null + ? (() => { + const currentValue = position.shares * currentMarketPrice; + const pnlUsdc = currentValue - position.invested; + const roiPct = (pnlUsdc / position.invested) * 100; + const roiColor = pnlUsdc >= 0 ? ANSI.green : ANSI.red; + const sign = pnlUsdc >= 0 ? "+" : ""; + return `${roiColor}${sign}${roiPct.toFixed(1)}%${ANSI.reset} | P&L: ${roiColor}${sign}$${pnlUsdc.toFixed(2)}${ANSI.reset} | Val: $${currentValue.toFixed(2)}`; + })() + : `${ANSI.gray}-${ANSI.reset}`; + + lines.push(kv("POSITION:", `${sideColor}${sideLabel}${ANSI.reset} @ ${entryStr} | ${sharesStr} shares | $${position.invested.toFixed(2)}`)); + lines.push(kv("ROI:", roi)); + } + + lines.push(""); + lines.push(` ${ANSI.white}[B]${ANSI.reset} Comprar ${ANSI.white}[S]${ANSI.reset} Vender ${ANSI.white}[Q]${ANSI.reset} Sair`); + + return lines; +} diff --git a/src/index.js b/src/index.js index 3309ed2a..efa658ad 100644 --- a/src/index.js +++ b/src/index.js @@ -29,8 +29,11 @@ import { kv, colorPriceLine, formatSignedDelta, colorByNarrative, formatNarrativeValue, narrativeFromSign, narrativeFromSlope, formatProbPct, fmtEtTime, getBtcSession, fmtTimeLeft, - safeFileSlug + safeFileSlug, setStatusMessage, formatPositionLines } from "./display.js"; +import { initTradingClient } from "./trading/client.js"; +import { buyMarketOrder, sellMarketOrder } from "./trading/orders.js"; +import { getPosition, recordBuy, recordSell, resetIfMarketChanged, fetchPositionBalance } from "./trading/position.js"; function countVwapCrosses(closes, vwapSeries, lookback) { if (closes.length < lookback || vwapSeries.length < lookback) return null; @@ -172,6 +175,27 @@ async function main() { const polymarketLiveStream = startPolymarketChainlinkPriceStream({}); const chainlinkStream = startChainlinkPriceStream({}); + // --- Trading setup --- + let trading = { client: null, tradingEnabled: false, tradeAmount: 0 }; + try { + trading = await initTradingClient(CONFIG); + } catch { /* trading stays disabled */ } + + const actionQueue = []; + let lastPoly = null; + let lastRec = null; + + if (trading.tradingEnabled) { + process.stdin.setRawMode(true); + process.stdin.resume(); + process.stdin.on("data", (key) => { + const ch = key.toString().toLowerCase(); + if (ch === "b") actionQueue.push({ type: "buy" }); + else if (ch === "s") actionQueue.push({ type: "sell" }); + else if (ch === "q" || key[0] === 0x03) process.exit(0); + }); + } + let prevSpotPrice = null; let prevCurrentPrice = null; let priceToBeatState = { slug: null, value: null, setAtMs: null }; @@ -293,6 +317,56 @@ async function main() { const rec = decide({ remainingMinutes: timeLeftMin, edgeUp: edge.edgeUp, edgeDown: edge.edgeDown, modelUp: timeAware.adjustedUp, modelDown: timeAware.adjustedDown }); + // --- Trading actions --- + lastPoly = poly; + lastRec = rec; + const marketSlugNow = poly.ok ? String(poly.market?.slug ?? "") : ""; + resetIfMarketChanged(marketSlugNow); + + while (actionQueue.length && trading.tradingEnabled && poly.ok) { + const action = actionQueue.shift(); + if (action.type === "buy") { + const pos = getPosition(); + if (pos.active) { + setStatusMessage("Já existe posição aberta"); + } else { + const side = rec.action === "ENTER" ? rec.side : (timeAware.adjustedUp >= timeAware.adjustedDown ? "UP" : "DOWN"); + const tokenId = side === "UP" ? poly.tokens.upTokenId : poly.tokens.downTokenId; + const mktPrice = side === "UP" ? marketUp : marketDown; + const priceNum = mktPrice != null ? mktPrice / 100 : 0.5; + setStatusMessage(`Comprando ${side}...`); + const result = await buyMarketOrder({ client: trading.client, tokenId, amount: trading.tradeAmount }); + if (result.ok) { + const shares = trading.tradeAmount / priceNum; + recordBuy({ side, tokenId, shares, entryPrice: priceNum, invested: trading.tradeAmount, marketSlug: marketSlugNow, orderId: result.order?.orderID }); + const balance = await fetchPositionBalance(trading.client, tokenId); + if (balance > 0) recordBuy({ side, tokenId, shares: balance, entryPrice: priceNum, invested: trading.tradeAmount, marketSlug: marketSlugNow, orderId: result.order?.orderID }); + setStatusMessage(`COMPROU ${side} @ ${(priceNum * 100).toFixed(1)}¢ | $${trading.tradeAmount}`); + } else { + setStatusMessage(`Erro: ${result.error}`); + } + } + } else if (action.type === "sell") { + const pos = getPosition(); + if (!pos.active) { + setStatusMessage("Nenhuma posição para vender"); + } else { + setStatusMessage(`Vendendo ${pos.side}...`); + const result = await sellMarketOrder({ client: trading.client, tokenId: pos.tokenId, amount: pos.shares }); + if (result.ok) { + const mktPrice = pos.side === "UP" ? marketUp : marketDown; + const priceNum = mktPrice != null ? mktPrice / 100 : 0.5; + const pnl = (pos.shares * priceNum) - pos.invested; + const sign = pnl >= 0 ? "+" : ""; + setStatusMessage(`VENDEU ${pos.side} | P&L: ${sign}$${pnl.toFixed(2)}`); + recordSell(); + } else { + setStatusMessage(`Erro: ${result.error}`); + } + } + } + } + const vwapSlopeLabel = vwapSlope === null ? "-" : vwapSlope > 0 ? "UP" : vwapSlope < 0 ? "DOWN" : "FLAT"; const macdLabel = macd === null @@ -540,6 +614,14 @@ async function main() { const pnlSign = o.pnl > 0 ? "+" : ""; return `${ANSI.gray} ${i + 1}.${ANSI.reset} ${color}${label}${ANSI.reset} ${ANSI.dim}${o.side}${ANSI.reset} ${color}${pnlSign}${o.pnl.toFixed(2)} USDC${ANSI.reset} ${ANSI.gray}${o.slug.slice(0, 40)}${ANSI.reset}`; }), + ...(() => { + const pos = getPosition(); + const posPrice = pos.active + ? (pos.side === "UP" ? marketUp : marketDown) + : null; + const currentMktPrice = posPrice != null ? posPrice / 100 : null; + return formatPositionLines({ position: pos, currentMarketPrice: currentMktPrice, tradingEnabled: trading.tradingEnabled }); + })(), "", sepLine(), centerText(`${ANSI.dim}${ANSI.gray}created by @krajekis${ANSI.reset}`, screenWidth()) diff --git a/src/index5m.js b/src/index5m.js index 969a1e70..29a06b12 100644 --- a/src/index5m.js +++ b/src/index5m.js @@ -30,8 +30,12 @@ import { kv, colorPriceLine, formatSignedDelta, colorByNarrative, formatNarrativeValue, narrativeFromSign, narrativeFromSlope, formatProbPct, fmtEtTime, fmtEtHHMM, getBtcSession, fmtTimeLeft, - safeFileSlug, priceToBeatFromPolymarketMarket + safeFileSlug, priceToBeatFromPolymarketMarket, + setStatusMessage, formatPositionLines } from "./display.js"; +import { initTradingClient } from "./trading/client.js"; +import { buyMarketOrder, sellMarketOrder } from "./trading/orders.js"; +import { getPosition, recordBuy, recordSell, computeROI, resetIfMarketChanged, fetchPositionBalance } from "./trading/position.js"; applyGlobalProxyFromEnv(); @@ -168,6 +172,27 @@ async function main() { const polymarketLiveStream = startPolymarketChainlinkPriceStream({}); const chainlinkStream = startChainlinkPriceStream({}); + // --- Trading setup --- + let trading = { client: null, tradingEnabled: false, tradeAmount: 0 }; + try { + trading = await initTradingClient(CONFIG); + } catch { /* trading stays disabled */ } + + const actionQueue = []; + let lastPoly = null; + let lastRec = null; + + if (trading.tradingEnabled) { + process.stdin.setRawMode(true); + process.stdin.resume(); + process.stdin.on("data", (key) => { + const ch = key.toString().toLowerCase(); + if (ch === "b") actionQueue.push({ type: "buy" }); + else if (ch === "s") actionQueue.push({ type: "sell" }); + else if (ch === "q" || key[0] === 0x03) process.exit(0); + }); + } + let prevSpotPrice = null; let prevCurrentPrice = null; let priceToBeatState = { slug: null, value: null, setAtMs: null }; @@ -272,6 +297,56 @@ async function main() { const rec = decide5m({ remainingMinutes: timeLeftMin, edgeUp: edge.edgeUp, edgeDown: edge.edgeDown, modelUp: timeAware.adjustedUp, modelDown: timeAware.adjustedDown }); + // --- Trading actions --- + lastPoly = poly; + lastRec = rec; + const marketSlugNow = poly.ok ? String(poly.market?.slug ?? "") : ""; + resetIfMarketChanged(marketSlugNow); + + while (actionQueue.length && trading.tradingEnabled && poly.ok) { + const action = actionQueue.shift(); + if (action.type === "buy") { + const pos = getPosition(); + if (pos.active) { + setStatusMessage("Já existe posição aberta"); + } else { + const side = rec.action === "ENTER" ? rec.side : (timeAware.adjustedUp >= timeAware.adjustedDown ? "UP" : "DOWN"); + const tokenId = side === "UP" ? poly.tokens.upTokenId : poly.tokens.downTokenId; + const mktPrice = side === "UP" ? marketUp : marketDown; + const priceNum = mktPrice != null ? mktPrice / 100 : 0.5; + setStatusMessage(`Comprando ${side}...`); + const result = await buyMarketOrder({ client: trading.client, tokenId, amount: trading.tradeAmount }); + if (result.ok) { + const shares = trading.tradeAmount / priceNum; + recordBuy({ side, tokenId, shares, entryPrice: priceNum, invested: trading.tradeAmount, marketSlug: marketSlugNow, orderId: result.order?.orderID }); + const balance = await fetchPositionBalance(trading.client, tokenId); + if (balance > 0) recordBuy({ side, tokenId, shares: balance, entryPrice: priceNum, invested: trading.tradeAmount, marketSlug: marketSlugNow, orderId: result.order?.orderID }); + setStatusMessage(`COMPROU ${side} @ ${(priceNum * 100).toFixed(1)}¢ | $${trading.tradeAmount}`); + } else { + setStatusMessage(`Erro: ${result.error}`); + } + } + } else if (action.type === "sell") { + const pos = getPosition(); + if (!pos.active) { + setStatusMessage("Nenhuma posição para vender"); + } else { + setStatusMessage(`Vendendo ${pos.side}...`); + const result = await sellMarketOrder({ client: trading.client, tokenId: pos.tokenId, amount: pos.shares }); + if (result.ok) { + const mktPrice = pos.side === "UP" ? marketUp : marketDown; + const priceNum = mktPrice != null ? mktPrice / 100 : 0.5; + const pnl = (pos.shares * priceNum) - pos.invested; + const sign = pnl >= 0 ? "+" : ""; + setStatusMessage(`VENDEU ${pos.side} | P&L: ${sign}$${pnl.toFixed(2)}`); + recordSell(); + } else { + setStatusMessage(`Erro: ${result.error}`); + } + } + } + } + // --- Display --- const lastCandle = klines1m.length ? klines1m[klines1m.length - 1] : null; const lastClose = lastCandle?.close ?? null; @@ -494,6 +569,14 @@ async function main() { const pnlSign = o.pnl > 0 ? "+" : ""; return `${ANSI.gray} ${i + 1}.${ANSI.reset} ${color}${label}${ANSI.reset} ${ANSI.dim}${o.side}${ANSI.reset} ${color}${pnlSign}${o.pnl.toFixed(2)} USDC${ANSI.reset} ${ANSI.gray}${o.slug.slice(0, 40)}${ANSI.reset}`; }), + ...(() => { + const pos = getPosition(); + const posPrice = pos.active + ? (pos.side === "UP" ? marketUp : marketDown) + : null; + const currentMktPrice = posPrice != null ? posPrice / 100 : null; + return formatPositionLines({ position: pos, currentMarketPrice: currentMktPrice, tradingEnabled: trading.tradingEnabled }); + })(), "", sepLine(), centerText(`${ANSI.dim}${ANSI.gray}created by @krajekis${ANSI.reset}`, screenWidth()) diff --git a/src/trading/client.js b/src/trading/client.js new file mode 100644 index 00000000..3ab81286 --- /dev/null +++ b/src/trading/client.js @@ -0,0 +1,46 @@ +import { ClobClient, SignatureType } from "@polymarket/clob-client"; +import { Wallet } from "ethers"; + +let _cached = null; + +export async function initTradingClient(config) { + if (_cached) return _cached; + + const { privateKey, funder, signatureType, tradeAmount } = config.trading; + + if (!privateKey) { + _cached = { client: null, tradingEnabled: false, tradeAmount: 0 }; + return _cached; + } + + const signer = new Wallet(privateKey); + const sigType = signatureType === 1 + ? SignatureType.POLY_PROXY + : signatureType === 2 + ? SignatureType.POLY_GNOSIS_SAFE + : SignatureType.EOA; + const funderAddr = funder || signer.address; + + const clientL1 = new ClobClient( + config.clobBaseUrl, + 137, + signer, + undefined, + sigType, + funderAddr + ); + + const creds = await clientL1.createOrDeriveApiKey(); + + const client = new ClobClient( + config.clobBaseUrl, + 137, + signer, + creds, + sigType, + funderAddr + ); + + _cached = { client, tradingEnabled: true, tradeAmount }; + return _cached; +} diff --git a/src/trading/orders.js b/src/trading/orders.js new file mode 100644 index 00000000..e17b5e73 --- /dev/null +++ b/src/trading/orders.js @@ -0,0 +1,27 @@ +import { Side } from "@polymarket/clob-client"; + +export async function buyMarketOrder({ client, tokenId, amount }) { + try { + const order = await client.createAndPostMarketOrder({ + tokenID: tokenId, + amount, + side: Side.BUY, + }); + return { ok: true, order }; + } catch (err) { + return { ok: false, error: err?.message ?? String(err) }; + } +} + +export async function sellMarketOrder({ client, tokenId, amount }) { + try { + const order = await client.createAndPostMarketOrder({ + tokenID: tokenId, + amount, + side: Side.SELL, + }); + return { ok: true, order }; + } catch (err) { + return { ok: false, error: err?.message ?? String(err) }; + } +} diff --git a/src/trading/position.js b/src/trading/position.js new file mode 100644 index 00000000..63a52284 --- /dev/null +++ b/src/trading/position.js @@ -0,0 +1,75 @@ +import { AssetType } from "@polymarket/clob-client"; + +let position = { + active: false, + side: null, + tokenId: null, + shares: 0, + entryPrice: 0, + invested: 0, + marketSlug: null, + orderId: null, + timestamp: null, +}; + +export function getPosition() { + return { ...position }; +} + +export function recordBuy({ side, tokenId, shares, entryPrice, invested, marketSlug, orderId }) { + position = { + active: true, + side, + tokenId, + shares, + entryPrice, + invested, + marketSlug, + orderId: orderId ?? null, + timestamp: Date.now(), + }; +} + +export function recordSell() { + position = { + active: false, + side: null, + tokenId: null, + shares: 0, + entryPrice: 0, + invested: 0, + marketSlug: null, + orderId: null, + timestamp: null, + }; +} + +export function computeROI(currentPrice) { + if (!position.active || !position.shares || !position.invested) { + return { currentValue: 0, roi: 0, pnlUsdc: 0 }; + } + const currentValue = position.shares * currentPrice; + const pnlUsdc = currentValue - position.invested; + const roi = (pnlUsdc / position.invested) * 100; + return { currentValue, roi, pnlUsdc }; +} + +export function resetIfMarketChanged(currentSlug) { + if (position.active && position.marketSlug && position.marketSlug !== currentSlug) { + recordSell(); + return true; + } + return false; +} + +export async function fetchPositionBalance(client, tokenId) { + try { + const res = await client.getBalanceAllowance({ + asset_type: AssetType.CONDITIONAL, + token_id: tokenId, + }); + return Number(res?.balance ?? 0); + } catch { + return 0; + } +} From 5d14f050d530c1f34d0cdc130b60d59dcef86d18 Mon Sep 17 00:00:00 2001 From: Jean Carraro Date: Fri, 3 Apr 2026 19:04:42 -0300 Subject: [PATCH 04/49] Add exit signal recommendation for open positions MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Evaluates 4 exit conditions each tick when a position is open: - TAKE_PROFIT: ROI >= takeProfitPct (default +20%) - STOP_LOSS: ROI <= -stopLossPct (default -25%) - SIGNAL_FLIPPED: opposite side model prob >= signalFlipMinProb (default 0.58) - TIME_DECAY: < 1.5min left and ROI < -5% Shows "► VENDER — " in the position section (yellow=MEDIUM, red=HIGH). Thresholds configurable via TRADE_TAKE_PROFIT_PCT, TRADE_STOP_LOSS_PCT, TRADE_SIGNAL_FLIP_PROB env vars. Co-Authored-By: Claude Sonnet 4.6 --- src/config.js | 4 ++++ src/display.js | 15 ++++++++++++++- src/index.js | 14 ++++++++++++-- src/index5m.js | 14 ++++++++++++-- src/trading/position.js | 38 ++++++++++++++++++++++++++++++++++++++ 5 files changed, 80 insertions(+), 5 deletions(-) diff --git a/src/config.js b/src/config.js index 00dc9300..66c951f5 100644 --- a/src/config.js +++ b/src/config.js @@ -30,6 +30,10 @@ export const CONFIG = { funder: process.env.POLYMARKET_FUNDER || "", signatureType: Number(process.env.POLYMARKET_SIGNATURE_TYPE || "0"), tradeAmount: Number(process.env.POLYMARKET_TRADE_AMOUNT || "5"), + // Exit thresholds + takeProfitPct: Number(process.env.TRADE_TAKE_PROFIT_PCT || "20"), // vender ao atingir +20% ROI + stopLossPct: Number(process.env.TRADE_STOP_LOSS_PCT || "25"), // vender ao atingir -25% ROI + signalFlipMinProb: Number(process.env.TRADE_SIGNAL_FLIP_PROB || "0.58"), // prob oposta que indica inversão }, chainlink: { diff --git a/src/display.js b/src/display.js index 4a7fedcf..fb140328 100644 --- a/src/display.js +++ b/src/display.js @@ -280,7 +280,14 @@ export function getStatusLine() { return null; } -export function formatPositionLines({ position, currentMarketPrice, tradingEnabled }) { +const EXIT_REASON_LABEL = { + TAKE_PROFIT: "REALIZAR LUCRO", + STOP_LOSS: "STOP LOSS", + SIGNAL_FLIPPED: "SINAL INVERTIDO", + TIME_DECAY: "TEMPO CURTO", +}; + +export function formatPositionLines({ position, currentMarketPrice, tradingEnabled, exitEval }) { if (!tradingEnabled) return []; const lines = []; @@ -314,6 +321,12 @@ export function formatPositionLines({ position, currentMarketPrice, tradingEnabl lines.push(kv("POSITION:", `${sideColor}${sideLabel}${ANSI.reset} @ ${entryStr} | ${sharesStr} shares | $${position.invested.toFixed(2)}`)); lines.push(kv("ROI:", roi)); + + if (exitEval?.shouldSell) { + const urgencyColor = exitEval.urgency === "HIGH" ? ANSI.red : ANSI.yellow; + const label = EXIT_REASON_LABEL[exitEval.reason] ?? exitEval.reason; + lines.push(kv("SAÍDA:", `${urgencyColor}► VENDER — ${label}${ANSI.reset}`)); + } } lines.push(""); diff --git a/src/index.js b/src/index.js index efa658ad..8a11abfe 100644 --- a/src/index.js +++ b/src/index.js @@ -33,7 +33,7 @@ import { } from "./display.js"; import { initTradingClient } from "./trading/client.js"; import { buyMarketOrder, sellMarketOrder } from "./trading/orders.js"; -import { getPosition, recordBuy, recordSell, resetIfMarketChanged, fetchPositionBalance } from "./trading/position.js"; +import { getPosition, recordBuy, recordSell, resetIfMarketChanged, fetchPositionBalance, evaluateExit } from "./trading/position.js"; function countVwapCrosses(closes, vwapSeries, lookback) { if (closes.length < lookback || vwapSeries.length < lookback) return null; @@ -620,7 +620,17 @@ async function main() { ? (pos.side === "UP" ? marketUp : marketDown) : null; const currentMktPrice = posPrice != null ? posPrice / 100 : null; - return formatPositionLines({ position: pos, currentMarketPrice: currentMktPrice, tradingEnabled: trading.tradingEnabled }); + const exitEval = evaluateExit({ + position: pos, + modelUp: timeAware.adjustedUp, + modelDown: timeAware.adjustedDown, + currentMarketPrice: currentMktPrice, + timeLeftMin, + takeProfitPct: CONFIG.trading.takeProfitPct, + stopLossPct: CONFIG.trading.stopLossPct, + signalFlipMinProb: CONFIG.trading.signalFlipMinProb, + }); + return formatPositionLines({ position: pos, currentMarketPrice: currentMktPrice, tradingEnabled: trading.tradingEnabled, exitEval }); })(), "", sepLine(), diff --git a/src/index5m.js b/src/index5m.js index 29a06b12..69e2c5d5 100644 --- a/src/index5m.js +++ b/src/index5m.js @@ -35,7 +35,7 @@ import { } from "./display.js"; import { initTradingClient } from "./trading/client.js"; import { buyMarketOrder, sellMarketOrder } from "./trading/orders.js"; -import { getPosition, recordBuy, recordSell, computeROI, resetIfMarketChanged, fetchPositionBalance } from "./trading/position.js"; +import { getPosition, recordBuy, recordSell, computeROI, resetIfMarketChanged, fetchPositionBalance, evaluateExit } from "./trading/position.js"; applyGlobalProxyFromEnv(); @@ -575,7 +575,17 @@ async function main() { ? (pos.side === "UP" ? marketUp : marketDown) : null; const currentMktPrice = posPrice != null ? posPrice / 100 : null; - return formatPositionLines({ position: pos, currentMarketPrice: currentMktPrice, tradingEnabled: trading.tradingEnabled }); + const exitEval = evaluateExit({ + position: pos, + modelUp: timeAware.adjustedUp, + modelDown: timeAware.adjustedDown, + currentMarketPrice: currentMktPrice, + timeLeftMin, + takeProfitPct: CONFIG.trading.takeProfitPct, + stopLossPct: CONFIG.trading.stopLossPct, + signalFlipMinProb: CONFIG.trading.signalFlipMinProb, + }); + return formatPositionLines({ position: pos, currentMarketPrice: currentMktPrice, tradingEnabled: trading.tradingEnabled, exitEval }); })(), "", sepLine(), diff --git a/src/trading/position.js b/src/trading/position.js index 63a52284..ae68d139 100644 --- a/src/trading/position.js +++ b/src/trading/position.js @@ -62,6 +62,44 @@ export function resetIfMarketChanged(currentSlug) { return false; } +// Avalia se a posição aberta deve ser encerrada. +// Retorna { shouldSell, reason, urgency } onde urgency é "HIGH" | "MEDIUM" | null +export function evaluateExit({ position, modelUp, modelDown, currentMarketPrice, timeLeftMin, takeProfitPct, stopLossPct, signalFlipMinProb }) { + if (!position.active || currentMarketPrice == null) { + return { shouldSell: false, reason: null, urgency: null }; + } + + const currentValue = position.shares * currentMarketPrice; + const pnlUsdc = currentValue - position.invested; + const roiPct = (pnlUsdc / position.invested) * 100; + + // 1. Take profit + if (roiPct >= takeProfitPct) { + return { shouldSell: true, reason: "TAKE_PROFIT", urgency: "MEDIUM", roiPct }; + } + + // 2. Stop loss + if (roiPct <= -stopLossPct) { + return { shouldSell: true, reason: "STOP_LOSS", urgency: "HIGH", roiPct }; + } + + // 3. Sinal invertido — modelo agora favorece o lado oposto com confiança + if (modelUp != null && modelDown != null) { + const oppositeProb = position.side === "UP" ? modelDown : modelUp; + if (oppositeProb >= signalFlipMinProb) { + const urgency = oppositeProb >= 0.65 ? "HIGH" : "MEDIUM"; + return { shouldSell: true, reason: "SIGNAL_FLIPPED", urgency, roiPct }; + } + } + + // 4. Pouco tempo + perdendo — reduz exposição + if (timeLeftMin != null && timeLeftMin < 1.5 && roiPct < -5) { + return { shouldSell: true, reason: "TIME_DECAY", urgency: "MEDIUM", roiPct }; + } + + return { shouldSell: false, reason: null, urgency: null, roiPct }; +} + export async function fetchPositionBalance(client, tokenId) { try { const res = await client.getBalanceAllowance({ From f7ef82e7d407302a7b42ba741fa44f9368964d34 Mon Sep 17 00:00:00 2001 From: Jean Carraro Date: Mon, 6 Apr 2026 08:22:16 -0300 Subject: [PATCH 05/49] sync --- .claude/settings.json | 15 ++ src/data/chainlink.js | 71 ++++++++ src/display.js | 254 +++++++++++++++++--------- src/index.js | 415 +++++++++++++++++------------------------- src/index5m.js | 326 +++++++++++++++------------------ src/trading/client.js | 33 +++- src/trading/orders.js | 48 ++++- 7 files changed, 646 insertions(+), 516 deletions(-) create mode 100644 .claude/settings.json diff --git a/.claude/settings.json b/.claude/settings.json new file mode 100644 index 00000000..f48cfc8d --- /dev/null +++ b/.claude/settings.json @@ -0,0 +1,15 @@ +{ + "permissions": { + "allow": [ + "Bash(npm install:*)", + "Bash(fish -c \"npm install @polymarket/clob-client\")", + "Bash(fish -c \"nvm use latest && npm install @polymarket/clob-client\")", + "Bash(fish -c \"nvm list\")", + "Bash(fish -c \"nvm current\")", + "Bash(fish -c \"nvm use lts && npm install @polymarket/clob-client\")", + "Bash(fish -c \"nvm use lts && node -e \\\\\"import\\('@polymarket/clob-client'\\).then\\(m => console.log\\(Object.keys\\(m\\).join\\(', '\\)\\)\\)\\\\\"\")", + "Bash(fish -c 'nvm use lts && node -e \":*)", + "Bash(fish -c 'nvm use lts && node --input-type=module -e \":*)" + ] + } +} diff --git a/src/data/chainlink.js b/src/data/chainlink.js index 6ab8fe19..a29cb91c 100644 --- a/src/data/chainlink.js +++ b/src/data/chainlink.js @@ -3,6 +3,7 @@ import { CONFIG } from "../config.js"; const AGGREGATOR_ABI = [ "function latestRoundData() view returns (uint80 roundId,int256 answer,uint256 startedAt,uint256 updatedAt,uint80 answeredInRound)", + "function getRoundData(uint80 _roundId) view returns (uint80 roundId,int256 answer,uint256 startedAt,uint256 updatedAt,uint80 answeredInRound)", "function decimals() view returns (uint8)" ]; @@ -80,11 +81,81 @@ async function fetchLatestRoundData(rpcUrl, aggregator) { const result = await ethCall(rpcUrl, aggregator, data); const decoded = iface.decodeFunctionResult("latestRoundData", result); return { + roundId: decoded[0], answer: decoded[1], updatedAt: decoded[3] }; } +async function fetchRoundData(rpcUrl, aggregator, roundId) { + const data = iface.encodeFunctionData("getRoundData", [roundId]); + const result = await ethCall(rpcUrl, aggregator, data); + const decoded = iface.decodeFunctionResult("getRoundData", result); + return { + roundId: decoded[0], + answer: decoded[1], + updatedAt: decoded[3] + }; +} + +// Finds the Chainlink round whose updatedAt is the first one >= targetTimeSec. +// Uses binary search over the aggregator-level round index embedded in the roundId. +async function findRoundAtTimestamp(rpcUrl, aggregator, targetTimeSec) { + const latest = await fetchLatestRoundData(rpcUrl, aggregator); + const latestId = BigInt(latest.roundId); + const PHASE_MASK = BigInt("0xFFFFFFFFFFFFFFFF"); + const phaseId = latestId >> BigInt(64); + const latestAggRound = latestId & PHASE_MASK; + + // Binary search for the earliest round with updatedAt >= targetTimeSec + let lo = BigInt(1); + let hi = latestAggRound; + let result = latest; + + while (lo <= hi) { + const mid = (lo + hi) / BigInt(2); + const roundId = (phaseId << BigInt(64)) | mid; + try { + const rd = await fetchRoundData(rpcUrl, aggregator, roundId); + const updatedAt = Number(rd.updatedAt); + if (updatedAt === 0) { lo = mid + BigInt(1); continue; } + if (updatedAt >= targetTimeSec) { + result = rd; + hi = mid - BigInt(1); + } else { + lo = mid + BigInt(1); + } + } catch { + lo = mid + BigInt(1); + } + } + return result; +} + +// Returns the Chainlink BTC/USD price at (or just after) a given Unix timestamp in ms. +export async function fetchChainlinkPriceAtMs(targetMs) { + if (!CONFIG.chainlink.btcUsdAggregator) return null; + const targetSec = Math.floor(targetMs / 1000); + const rpcs = getOrderedRpcs(); + if (rpcs.length === 0) return null; + + const aggregator = CONFIG.chainlink.btcUsdAggregator; + for (const rpc of rpcs) { + try { + if (cachedDecimals === null) { + cachedDecimals = await fetchDecimals(rpc, aggregator); + } + const round = await findRoundAtTimestamp(rpc, aggregator, targetSec); + const scale = 10 ** Number(cachedDecimals); + return Number(round.answer) / scale; + } catch { + cachedDecimals = null; + continue; + } + } + return null; +} + export async function fetchChainlinkBtcUsd() { if ((!CONFIG.chainlink.polygonRpcUrl && (!CONFIG.chainlink.polygonRpcUrls || CONFIG.chainlink.polygonRpcUrls.length === 0)) || !CONFIG.chainlink.btcUsdAggregator) { return { price: null, updatedAt: null, source: "missing_config" }; diff --git a/src/display.js b/src/display.js index fb140328..c998f26c 100644 --- a/src/display.js +++ b/src/display.js @@ -8,7 +8,8 @@ export const ANSI = { lightRed: "\x1b[91m", gray: "\x1b[90m", white: "\x1b[97m", - dim: "\x1b[2m" + dim: "\x1b[2m", + bold: "\x1b[1m" }; export function screenWidth() { @@ -18,7 +19,7 @@ export function screenWidth() { export function sepLine(ch = "\u2500") { const w = screenWidth(); - return `${ANSI.white}${ch.repeat(w)}${ANSI.reset}`; + return `${ANSI.dim}${ch.repeat(w)}${ANSI.reset}`; } let _screenInitialized = false; @@ -52,53 +53,65 @@ export function stripAnsi(s) { return String(s).replace(/\x1b\[[0-9;]*m/g, ""); } +function visLen(s) { + return stripAnsi(String(s)).length; +} + +function padRight(s, width) { + const pad = width - visLen(s); + return pad > 0 ? s + " ".repeat(pad) : s; +} + export function padLabel(label, width) { - const visible = stripAnsi(label).length; + const visible = visLen(label); if (visible >= width) return label; return label + " ".repeat(width - visible); } export function centerText(text, width) { - const visible = stripAnsi(text).length; + const visible = visLen(text); if (visible >= width) return text; const left = Math.floor((width - visible) / 2); - const right = width - visible - left; - return " ".repeat(left) + text + " ".repeat(right); + return " ".repeat(left) + text; } -export const LABEL_W = 16; +export const LABEL_W = 14; export function kv(label, value) { - const l = padLabel(String(label), LABEL_W); - return `${l}${value}`; + return `${padLabel(String(label), LABEL_W)}${value}`; } export function section(title) { - return `${ANSI.white}${title}${ANSI.reset}`; + return `${ANSI.white}${ANSI.bold}${title}${ANSI.reset}`; +} + +// Merge left and right column arrays into full-width lines +function mergeColumns(left, right, totalWidth) { + const colW = Math.floor(totalWidth / 2) - 1; + const len = Math.max(left.length, right.length); + const out = []; + for (let i = 0; i < len; i++) { + const l = padRight(left[i] ?? "", colW); + const r = right[i] ?? ""; + out.push(`${l} ${ANSI.dim}\u2502${ANSI.reset} ${r}`); + } + return out; } export function colorPriceLine({ label, price, prevPrice, decimals = 0, prefix = "" }) { if (price === null || price === undefined) { return `${label}: ${ANSI.gray}-${ANSI.reset}`; } - const p = Number(price); const prev = prevPrice === null || prevPrice === undefined ? null : Number(prevPrice); - let color = ANSI.reset; let arrow = ""; if (prev !== null && Number.isFinite(prev) && Number.isFinite(p) && p !== prev) { - if (p > prev) { - color = ANSI.green; - arrow = " \u2191"; - } else { - color = ANSI.red; - arrow = " \u2193"; - } + if (p > prev) { color = ANSI.green; arrow = " \u2191"; } + else { color = ANSI.red; arrow = " \u2193"; } } - const formatted = `${prefix}${formatNumberDisplay(p, decimals)}`; - return `${label}: ${color}${formatted}${arrow}${ANSI.reset}`; + return `${color}${formatted}${arrow}${ANSI.reset}`; } export function formatNumberDisplay(x, digits = 0) { @@ -153,14 +166,9 @@ export function fmtEtTime(now = new Date()) { try { return new Intl.DateTimeFormat("en-US", { timeZone: "America/New_York", - hour: "2-digit", - minute: "2-digit", - second: "2-digit", - hour12: false + hour: "2-digit", minute: "2-digit", second: "2-digit", hour12: false }).format(now); - } catch { - return "-"; - } + } catch { return "-"; } } export function fmtEtHHMM(dateOrMs) { @@ -168,13 +176,9 @@ export function fmtEtHHMM(dateOrMs) { const d = typeof dateOrMs === "number" ? new Date(dateOrMs) : dateOrMs; return new Intl.DateTimeFormat("en-US", { timeZone: "America/New_York", - hour: "2-digit", - minute: "2-digit", - hour12: false + hour: "2-digit", minute: "2-digit", hour12: false }).format(d); - } catch { - return "-"; - } + } catch { return "-"; } } export function getBtcSession(now = new Date()) { @@ -182,9 +186,8 @@ export function getBtcSession(now = new Date()) { const inAsia = h >= 0 && h < 8; const inEurope = h >= 7 && h < 16; const inUs = h >= 13 && h < 22; - - if (inEurope && inUs) return "Europe/US overlap"; - if (inAsia && inEurope) return "Asia/Europe overlap"; + if (inEurope && inUs) return "EU/US"; + if (inAsia && inEurope) return "Asia/EU"; if (inAsia) return "Asia"; if (inEurope) return "Europe"; if (inUs) return "US"; @@ -209,12 +212,7 @@ export function parsePriceToBeat(market) { } export function safeFileSlug(x) { - return String(x ?? "") - .toLowerCase() - .replace(/[^a-z0-9_-]+/g, "-") - .replace(/-+/g, "-") - .replace(/(^-|-$)/g, "") - .slice(0, 120); + return String(x ?? "").toLowerCase().replace(/[^a-z0-9_-]+/g, "-").replace(/-+/g, "-").replace(/(^-|-$)/g, "").slice(0, 120); } export function extractNumericFromMarket(market) { @@ -223,39 +221,28 @@ export function extractNumericFromMarket(market) { "strike", "threshold", "thresholdPrice", "threshold_price", "targetPrice", "target_price", "referencePrice", "reference_price" ]; - for (const k of directKeys) { const v = market?.[k]; const n = typeof v === "string" ? Number(v) : typeof v === "number" ? v : NaN; if (Number.isFinite(n)) return n; } - const seen = new Set(); const stack = [{ obj: market, depth: 0 }]; - while (stack.length) { const { obj, depth } = stack.pop(); if (!obj || typeof obj !== "object") continue; if (seen.has(obj) || depth > 6) continue; seen.add(obj); - const entries = Array.isArray(obj) ? obj.entries() : Object.entries(obj); for (const [key, value] of entries) { const k = String(key).toLowerCase(); - if (value && typeof value === "object") { - stack.push({ obj: value, depth: depth + 1 }); - continue; - } - + if (value && typeof value === "object") { stack.push({ obj: value, depth: depth + 1 }); continue; } if (!/(price|strike|threshold|target|beat)/i.test(k)) continue; - const n = typeof value === "string" ? Number(value) : typeof value === "number" ? value : NaN; if (!Number.isFinite(n)) continue; - if (n > 1000 && n < 2_000_000) return n; } } - return null; } @@ -287,50 +274,139 @@ const EXIT_REASON_LABEL = { TIME_DECAY: "TEMPO CURTO", }; -export function formatPositionLines({ position, currentMarketPrice, tradingEnabled, exitEval }) { - if (!tradingEnabled) return []; - +// ───────────────────────────────────────────────────────── +// buildScreen(d) — unified 2-column layout for 15m and 5m +// ───────────────────────────────────────────────────────── +export function buildScreen(d) { + const W = screenWidth(); const lines = []; - lines.push(sepLine()); - lines.push(""); + // ── HEADER ── + const titleLine = d.modeTag ? `${d.modeTag} ${d.title}` : d.title; + lines.push(titleLine); + + // Trading status + shortcuts + clock on a single line + const etStr = `${ANSI.dim}${fmtEtTime()} ${getBtcSession()}${ANSI.reset}`; + const tradingBadge = d.tradingEnabled + ? `${ANSI.green}● ATIVO${ANSI.reset} $${d.tradeAmount}` + : d.initError + ? `${ANSI.red}● ERRO${ANSI.reset}` + : `${ANSI.gray}● LEITURA${ANSI.reset}`; + const confirmOrKeys = d.confirmHint ?? d.shortcutsHint ?? ""; + lines.push(`${tradingBadge} ${confirmOrKeys}${" ".repeat(Math.max(0, W - visLen(tradingBadge) - visLen(confirmOrKeys) - visLen(etStr) - 4))} ${etStr}`); + + // Status message (errors, confirmations) — always visible const statusLine = getStatusLine(); if (statusLine) { lines.push(statusLine); - lines.push(""); } - if (!position.active) { - lines.push(kv("POSITION:", `${ANSI.gray}Nenhuma posição aberta${ANSI.reset}`)); + lines.push(sepLine()); + + // ── TOP ROW: Prices left │ Polymarket right ── + const leftPrices = []; + leftPrices.push(section("PRECOS")); + leftPrices.push(kv("Binance:", d.binanceSpot)); + leftPrices.push(kv("Chainlink:", d.chainlinkLine)); + if (d.priceToBeat !== null) { + leftPrices.push(kv("Price Beat:", `$${formatNumberDisplay(d.priceToBeat, 0)}`)); + } + if (d.intervalLine) leftPrices.push(d.intervalLine); + + const rightPoly = []; + rightPoly.push(section("POLYMARKET")); + rightPoly.push(`${ANSI.green}\u2191 UP${ANSI.reset} ${d.marketUpStr} ${ANSI.dim}|${ANSI.reset} ${ANSI.red}\u2193 DOWN${ANSI.reset} ${d.marketDownStr}`); + rightPoly.push(kv("Time left:", `${d.timeColor}${fmtTimeLeft(d.timeLeftMin)}${ANSI.reset}`)); + if (d.liquidity !== null) rightPoly.push(kv("Liquidity:", formatNumberDisplay(d.liquidity, 0))); + rightPoly.push(kv("Market:", d.marketSlug)); + + lines.push(...mergeColumns(leftPrices, rightPoly, W)); + lines.push(sepLine()); + + // ── MIDDLE ROW: Indicators left │ Signal right ── + const leftInd = []; + leftInd.push(section("INDICADORES")); + for (const ind of d.indicators) { + leftInd.push(kv(ind.label + ":", ind.value)); + } + + const rightSignal = []; + rightSignal.push(section("SINAL")); + rightSignal.push(kv("Predict:", d.predictValue)); + rightSignal.push(kv("Rec:", d.recLine)); + // pad to match left height + while (rightSignal.length < leftInd.length) rightSignal.push(""); + + lines.push(...mergeColumns(leftInd, rightSignal, W)); + lines.push(sepLine()); + + // ── BOTTOM ROW: Position left │ History right ── + const leftPos = []; + leftPos.push(section("POSICAO")); + + if (!d.tradingEnabled) { + leftPos.push(`${ANSI.gray}Trading desativado${ANSI.reset}`); + } else if (!d.position.active) { + leftPos.push(`${ANSI.gray}Nenhuma posicao aberta${ANSI.reset}`); } else { - const sideColor = position.side === "UP" ? ANSI.green : ANSI.red; - const sideLabel = position.side === "UP" ? "↑ UP" : "↓ DOWN"; - const sharesStr = position.shares.toFixed(2); - const entryStr = (position.entryPrice * 100).toFixed(1) + "¢"; - - const roi = currentMarketPrice != null - ? (() => { - const currentValue = position.shares * currentMarketPrice; - const pnlUsdc = currentValue - position.invested; - const roiPct = (pnlUsdc / position.invested) * 100; - const roiColor = pnlUsdc >= 0 ? ANSI.green : ANSI.red; - const sign = pnlUsdc >= 0 ? "+" : ""; - return `${roiColor}${sign}${roiPct.toFixed(1)}%${ANSI.reset} | P&L: ${roiColor}${sign}$${pnlUsdc.toFixed(2)}${ANSI.reset} | Val: $${currentValue.toFixed(2)}`; - })() - : `${ANSI.gray}-${ANSI.reset}`; - - lines.push(kv("POSITION:", `${sideColor}${sideLabel}${ANSI.reset} @ ${entryStr} | ${sharesStr} shares | $${position.invested.toFixed(2)}`)); - lines.push(kv("ROI:", roi)); - - if (exitEval?.shouldSell) { - const urgencyColor = exitEval.urgency === "HIGH" ? ANSI.red : ANSI.yellow; - const label = EXIT_REASON_LABEL[exitEval.reason] ?? exitEval.reason; - lines.push(kv("SAÍDA:", `${urgencyColor}► VENDER — ${label}${ANSI.reset}`)); + const p = d.position; + const sideColor = p.side === "UP" ? ANSI.green : ANSI.red; + const sideLabel = p.side === "UP" ? "\u2191 UP" : "\u2193 DOWN"; + const entryStr = (p.entryPrice * 100).toFixed(1) + "\u00A2"; + leftPos.push(`${sideColor}${sideLabel}${ANSI.reset} @ ${entryStr} ${p.shares.toFixed(2)} shares $${p.invested.toFixed(2)}`); + if (d.currentMktPrice != null) { + const val = p.shares * d.currentMktPrice; + const pnl = val - p.invested; + const roiPct = (pnl / p.invested) * 100; + const c = pnl >= 0 ? ANSI.green : ANSI.red; + const s = pnl >= 0 ? "+" : ""; + leftPos.push(kv("ROI:", `${c}${s}${roiPct.toFixed(1)}%${ANSI.reset} P&L: ${c}${s}$${pnl.toFixed(2)}${ANSI.reset} Val: $${val.toFixed(2)}`)); + } + if (d.exitEval?.shouldSell) { + const uc = d.exitEval.urgency === "HIGH" ? ANSI.red : ANSI.yellow; + const label = EXIT_REASON_LABEL[d.exitEval.reason] ?? d.exitEval.reason; + leftPos.push(`${uc}\u25BA VENDER \u2014 ${label}${ANSI.reset}`); + } + } + + const rightHist = []; + rightHist.push(section("HISTORICO")); + // Signal stats + const rs = d.runningStats ?? { wins: 0, losses: 0, totalPnl: 0 }; + const total = rs.wins + rs.losses; + const wr = total > 0 ? `${((rs.wins / total) * 100).toFixed(0)}%` : "-"; + const pc = rs.totalPnl > 0 ? ANSI.green : rs.totalPnl < 0 ? ANSI.red : ANSI.gray; + const ps = rs.totalPnl > 0 ? "+" : ""; + rightHist.push(`W:${ANSI.green}${rs.wins}${ANSI.reset} L:${ANSI.red}${rs.losses}${ANSI.reset} WR:${wr} ${pc}${ps}${rs.totalPnl.toFixed(2)} USDC${ANSI.reset}`); + + // Closed trades + if (d.closedTrades?.length) { + for (const t of d.closedTrades.slice(0, 3)) { + const color = t.pnl >= 0 ? ANSI.green : ANSI.red; + const pSign = t.pnl >= 0 ? "+" : ""; + const rSign = t.roi >= 0 ? "+" : ""; + const sl = t.side === "UP" ? `${ANSI.green}\u2191UP${ANSI.reset}` : `${ANSI.red}\u2193DN${ANSI.reset}`; + const ts = new Date(t.ts).toLocaleTimeString("pt-BR", { hour: "2-digit", minute: "2-digit" }); + rightHist.push(`${ANSI.dim}${ts}${ANSI.reset} ${sl} ${color}${pSign}$${t.pnl.toFixed(2)} ${rSign}${t.roi.toFixed(0)}%${ANSI.reset}`); + } + } + + // Recent outcomes (signal-based) + if (d.recentOutcomes?.length) { + for (const o of d.recentOutcomes.slice(0, 2)) { + const color = o.won ? ANSI.green : ANSI.red; + const label = o.won ? "WIN" : "LOSS"; + rightHist.push(`${color}${label}${ANSI.reset} ${ANSI.dim}${o.side}${ANSI.reset} ${color}${o.pnl > 0 ? "+" : ""}${o.pnl.toFixed(2)}${ANSI.reset}`); } } - lines.push(""); - lines.push(` ${ANSI.white}[B]${ANSI.reset} Comprar ${ANSI.white}[S]${ANSI.reset} Vender ${ANSI.white}[Q]${ANSI.reset} Sair`); + // Pad columns + while (leftPos.length < rightHist.length) leftPos.push(""); + while (rightHist.length < leftPos.length) rightHist.push(""); + + lines.push(...mergeColumns(leftPos, rightHist, W)); + lines.push(sepLine()); + lines.push(centerText(`${ANSI.dim}${ANSI.gray}created by @krajekis${ANSI.reset}`, W)); - return lines; + return lines.join("\n") + "\n"; } diff --git a/src/index.js b/src/index.js index 8a11abfe..72767a3a 100644 --- a/src/index.js +++ b/src/index.js @@ -1,6 +1,6 @@ import { CONFIG } from "./config.js"; import { fetchKlines, fetchLastPrice } from "./data/binance.js"; -import { fetchChainlinkBtcUsd } from "./data/chainlink.js"; +import { fetchChainlinkBtcUsd, fetchChainlinkPriceAtMs } from "./data/chainlink.js"; import { startChainlinkPriceStream } from "./data/chainlinkWs.js"; import { startPolymarketChainlinkPriceStream } from "./data/polymarketLiveWs.js"; import { @@ -25,11 +25,11 @@ import fs from "node:fs"; import path from "node:path"; import { applyGlobalProxyFromEnv } from "./net/proxy.js"; import { - ANSI, screenWidth, sepLine, renderScreen, centerText, - kv, colorPriceLine, formatSignedDelta, + ANSI, renderScreen, buildScreen, + colorPriceLine, formatSignedDelta, formatNumberDisplay, colorByNarrative, formatNarrativeValue, narrativeFromSign, - narrativeFromSlope, formatProbPct, fmtEtTime, getBtcSession, fmtTimeLeft, - safeFileSlug, setStatusMessage, formatPositionLines + narrativeFromSlope, formatProbPct, fmtTimeLeft, + safeFileSlug, priceToBeatFromPolymarketMarket, setStatusMessage } from "./display.js"; import { initTradingClient } from "./trading/client.js"; import { buyMarketOrder, sellMarketOrder } from "./trading/orders.js"; @@ -176,34 +176,50 @@ async function main() { const chainlinkStream = startChainlinkPriceStream({}); // --- Trading setup --- - let trading = { client: null, tradingEnabled: false, tradeAmount: 0 }; + let trading = { client: null, tradingEnabled: false, tradeAmount: 0, initError: null }; try { trading = await initTradingClient(CONFIG); - } catch { /* trading stays disabled */ } + } catch (err) { + trading.initError = err?.message ?? String(err); + } const actionQueue = []; + let pendingAction = null; // { type: "buy" | "sell" } waiting Y/N confirmation let lastPoly = null; let lastRec = null; - if (trading.tradingEnabled) { + let stdinError = null; + try { + if (!process.stdin.isTTY) throw new Error("stdin não é TTY — rode com: node src/index.js"); process.stdin.setRawMode(true); process.stdin.resume(); process.stdin.on("data", (key) => { const ch = key.toString().toLowerCase(); - if (ch === "b") actionQueue.push({ type: "buy" }); - else if (ch === "s") actionQueue.push({ type: "sell" }); - else if (ch === "q" || key[0] === 0x03) process.exit(0); + if (trading.tradingEnabled) { + if (pendingAction !== null) { + if (ch === "y") { actionQueue.push({ ...pendingAction }); pendingAction = null; } + else if (ch === "n" || key[0] === 0x1b) { pendingAction = null; } + } else { + if (ch === "b") pendingAction = { type: "buy" }; + else if (ch === "s") pendingAction = { type: "sell" }; + } + } + if (ch === "q" || key[0] === 0x03) process.exit(0); }); + } catch (err) { + stdinError = err?.message ?? String(err); } let prevSpotPrice = null; let prevCurrentPrice = null; let priceToBeatState = { slug: null, value: null, setAtMs: null }; + let priceToBeatFetching = false; // guard against concurrent historical fetches // Trade outcome tracking (per-market) let tradeState = { slug: null, side: null, entryMarketPrice: null, priceToBeat: null, lastChainlinkPrice: null, hasSignal: false }; let runningStats = { wins: 0, losses: 0, totalPnl: 0 }; let recentOutcomes = []; // { slug, side, won, pnl, ts }[] + let closedTrades = []; // { side, entryPrice, exitPrice, pnl, roi, ts }[] const header = [ "timestamp", @@ -333,17 +349,21 @@ async function main() { const side = rec.action === "ENTER" ? rec.side : (timeAware.adjustedUp >= timeAware.adjustedDown ? "UP" : "DOWN"); const tokenId = side === "UP" ? poly.tokens.upTokenId : poly.tokens.downTokenId; const mktPrice = side === "UP" ? marketUp : marketDown; - const priceNum = mktPrice != null ? mktPrice / 100 : 0.5; + const priceNum = mktPrice != null ? mktPrice : 0.5; setStatusMessage(`Comprando ${side}...`); - const result = await buyMarketOrder({ client: trading.client, tokenId, amount: trading.tradeAmount }); + const result = await buyMarketOrder({ client: trading.client, tokenId, amount: trading.tradeAmount, price: priceNum }); if (result.ok) { - const shares = trading.tradeAmount / priceNum; - recordBuy({ side, tokenId, shares, entryPrice: priceNum, invested: trading.tradeAmount, marketSlug: marketSlugNow, orderId: result.order?.orderID }); const balance = await fetchPositionBalance(trading.client, tokenId); - if (balance > 0) recordBuy({ side, tokenId, shares: balance, entryPrice: priceNum, invested: trading.tradeAmount, marketSlug: marketSlugNow, orderId: result.order?.orderID }); - setStatusMessage(`COMPROU ${side} @ ${(priceNum * 100).toFixed(1)}¢ | $${trading.tradeAmount}`); + const shares = balance > 0 ? balance : trading.tradeAmount / priceNum; + recordBuy({ side, tokenId, shares, entryPrice: priceNum, invested: trading.tradeAmount, marketSlug: marketSlugNow, orderId: result.order?.orderID }); + const orderId = result.order?.orderID ?? result.order?.id ?? "-"; + const balanceStr = balance > 0 ? `shares: ${balance.toFixed(2)}` : "saldo 0 (ordem não preenchida?)"; + setStatusMessage(`COMPROU ${side} @ ${(priceNum * 100).toFixed(1)}¢ | $${trading.tradeAmount} | ${balanceStr} | ID: ${String(orderId).slice(0, 12)}`, 8000); } else { - setStatusMessage(`Erro: ${result.error}`); + const errMsg = `Erro na compra: ${result.error}`; + setStatusMessage(errMsg, 15000); + fs.mkdirSync("./logs", { recursive: true }); + fs.appendFileSync("./logs/trade_errors.log", `${new Date().toISOString()} BUY ${side} ${errMsg}\n`); } } } else if (action.type === "sell") { @@ -352,84 +372,30 @@ async function main() { setStatusMessage("Nenhuma posição para vender"); } else { setStatusMessage(`Vendendo ${pos.side}...`); - const result = await sellMarketOrder({ client: trading.client, tokenId: pos.tokenId, amount: pos.shares }); + const sellMktPrice = pos.side === "UP" ? marketUp : marketDown; + const sellPriceNum = sellMktPrice != null ? sellMktPrice : 0.5; + const result = await sellMarketOrder({ client: trading.client, tokenId: pos.tokenId, amount: pos.shares, price: sellPriceNum }); if (result.ok) { const mktPrice = pos.side === "UP" ? marketUp : marketDown; - const priceNum = mktPrice != null ? mktPrice / 100 : 0.5; + const priceNum = mktPrice != null ? mktPrice : 0.5; const pnl = (pos.shares * priceNum) - pos.invested; + const roi = (pnl / pos.invested) * 100; const sign = pnl >= 0 ? "+" : ""; - setStatusMessage(`VENDEU ${pos.side} | P&L: ${sign}$${pnl.toFixed(2)}`); + setStatusMessage(`VENDEU ${pos.side} | P&L: ${sign}$${pnl.toFixed(2)}`, 8000); + closedTrades.unshift({ side: pos.side, entryPrice: pos.entryPrice, exitPrice: priceNum, pnl, roi, ts: Date.now() }); + if (closedTrades.length > 10) closedTrades.pop(); recordSell(); } else { - setStatusMessage(`Erro: ${result.error}`); + const errMsg = `Erro na venda: ${result.error}`; + setStatusMessage(errMsg, 15000); + fs.mkdirSync("./logs", { recursive: true }); + fs.appendFileSync("./logs/trade_errors.log", `${new Date().toISOString()} SELL ${pos.side} ${errMsg}\n`); } } } } - const vwapSlopeLabel = vwapSlope === null ? "-" : vwapSlope > 0 ? "UP" : vwapSlope < 0 ? "DOWN" : "FLAT"; - - const macdLabel = macd === null - ? "-" - : macd.hist < 0 - ? (macd.histDelta !== null && macd.histDelta < 0 ? "bearish (expanding)" : "bearish") - : (macd.histDelta !== null && macd.histDelta > 0 ? "bullish (expanding)" : "bullish"); - - const lastCandle = klines1m.length ? klines1m[klines1m.length - 1] : null; - const lastClose = lastCandle?.close ?? null; - const close1mAgo = klines1m.length >= 2 ? klines1m[klines1m.length - 2]?.close ?? null : null; - const close3mAgo = klines1m.length >= 4 ? klines1m[klines1m.length - 4]?.close ?? null : null; - const delta1m = lastClose !== null && close1mAgo !== null ? lastClose - close1mAgo : null; - const delta3m = lastClose !== null && close3mAgo !== null ? lastClose - close3mAgo : null; - - const haNarrative = (consec.color ?? "").toLowerCase() === "green" ? "LONG" : (consec.color ?? "").toLowerCase() === "red" ? "SHORT" : "NEUTRAL"; - const rsiNarrative = narrativeFromSlope(rsiSlope); - const macdNarrative = narrativeFromSign(macd?.hist ?? null); - const vwapNarrative = narrativeFromSign(vwapDist); - - const pLong = timeAware?.adjustedUp ?? null; - const pShort = timeAware?.adjustedDown ?? null; - const predictNarrative = (pLong !== null && pShort !== null && Number.isFinite(pLong) && Number.isFinite(pShort)) - ? (pLong > pShort ? "LONG" : pShort > pLong ? "SHORT" : "NEUTRAL") - : "NEUTRAL"; - const predictValue = `${ANSI.green}LONG${ANSI.reset} ${ANSI.green}${formatProbPct(pLong, 0)}${ANSI.reset} / ${ANSI.red}SHORT${ANSI.reset} ${ANSI.red}${formatProbPct(pShort, 0)}${ANSI.reset}`; - const predictLine = `Predict: ${predictValue}`; - - const marketUpStr = `${marketUp ?? "-"}${marketUp === null || marketUp === undefined ? "" : "¢"}`; - const marketDownStr = `${marketDown ?? "-"}${marketDown === null || marketDown === undefined ? "" : "¢"}`; - const polyHeaderValue = `${ANSI.green}↑ UP${ANSI.reset} ${marketUpStr} | ${ANSI.red}↓ DOWN${ANSI.reset} ${marketDownStr}`; - - const heikenValue = `${consec.color ?? "-"} x${consec.count}`; - const heikenLine = formatNarrativeValue("Heiken Ashi", heikenValue, haNarrative); - - const rsiArrow = rsiSlope !== null && rsiSlope < 0 ? "↓" : rsiSlope !== null && rsiSlope > 0 ? "↑" : "-"; - const rsiValue = `${formatNumber(rsiNow, 1)} ${rsiArrow}`; - const rsiLine = formatNarrativeValue("RSI", rsiValue, rsiNarrative); - - const macdLine = formatNarrativeValue("MACD", macdLabel, macdNarrative); - - const delta1Narrative = narrativeFromSign(delta1m); - const delta3Narrative = narrativeFromSign(delta3m); - const deltaValue = `${colorByNarrative(formatSignedDelta(delta1m, lastClose), delta1Narrative)} | ${colorByNarrative(formatSignedDelta(delta3m, lastClose), delta3Narrative)}`; - const deltaLine = `Delta 1/3Min: ${deltaValue}`; - - const vwapValue = `${formatNumber(vwapNow, 0)} (${formatPct(vwapDist, 2)}) | slope: ${vwapSlopeLabel}`; - const vwapLine = formatNarrativeValue("VWAP", vwapValue, vwapNarrative); - - const signal = rec.action === "ENTER" ? (rec.side === "UP" ? "BUY UP" : "BUY DOWN") : "NO TRADE"; - - const actionLine = rec.action === "ENTER" - ? `${rec.action} NOW (${rec.phase} ENTRY)` - : `NO TRADE (${rec.phase})`; - - const spreadUp = poly.ok ? poly.orderbook.up.spread : null; - const spreadDown = poly.ok ? poly.orderbook.down.spread : null; - - const spread = spreadUp !== null && spreadDown !== null ? Math.max(spreadUp, spreadDown) : (spreadUp ?? spreadDown); - const liquidity = poly.ok - ? (Number(poly.market?.liquidityNum) || Number(poly.market?.liquidity) || null) - : null; - + // --- Display data --- const spotPrice = wsPrice ?? lastPrice; const currentPrice = chainlink?.price ?? null; const marketSlug = poly.ok ? String(poly.market?.slug ?? "") : ""; @@ -438,206 +404,169 @@ async function main() { if (marketSlug && priceToBeatState.slug !== marketSlug) { priceToBeatState = { slug: marketSlug, value: null, setAtMs: null }; } - - if (priceToBeatState.slug && priceToBeatState.value === null && currentPrice !== null) { + if (priceToBeatState.slug && priceToBeatState.value === null && poly.ok && poly.market) { + const fromMarket = priceToBeatFromPolymarketMarket(poly.market); + if (fromMarket !== null) { + priceToBeatState = { slug: priceToBeatState.slug, value: fromMarket, setAtMs: Date.now(), source: "market" }; + } + } + if (priceToBeatState.slug && priceToBeatState.value === null && !priceToBeatFetching) { const nowMs = Date.now(); const okToLatch = marketStartMs === null ? true : nowMs >= marketStartMs; if (okToLatch) { - priceToBeatState = { slug: priceToBeatState.slug, value: Number(currentPrice), setAtMs: nowMs }; + const lateMs = marketStartMs !== null ? nowMs - marketStartMs : 0; + if (lateMs > 30_000 && marketStartMs !== null) { + // App started late — fetch historical Chainlink price at market open + priceToBeatFetching = true; + fetchChainlinkPriceAtMs(marketStartMs).then((p) => { + if (p !== null && priceToBeatState.slug === marketSlug && priceToBeatState.value === null) { + priceToBeatState = { slug: marketSlug, value: p, setAtMs: marketStartMs, source: "chainlink_historical" }; + } else if (p === null && currentPrice !== null && priceToBeatState.slug === marketSlug && priceToBeatState.value === null) { + priceToBeatState = { slug: marketSlug, value: Number(currentPrice), setAtMs: nowMs, source: "chainlink_latch" }; + } + priceToBeatFetching = false; + }).catch(() => { priceToBeatFetching = false; }); + } else if (currentPrice !== null) { + priceToBeatState = { slug: priceToBeatState.slug, value: Number(currentPrice), setAtMs: nowMs, source: "chainlink_latch" }; + } } } - const priceToBeat = priceToBeatState.slug === marketSlug ? priceToBeatState.value : null; - // --- Trade outcome tracking --- - // Detect market settlement: slug changed from a known previous market + // Trade outcome tracking if (tradeState.slug !== null && tradeState.slug !== "" && marketSlug !== tradeState.slug) { if (tradeState.hasSignal && tradeState.priceToBeat !== null && tradeState.lastChainlinkPrice !== null) { const winner = tradeState.lastChainlinkPrice > tradeState.priceToBeat ? "UP" : "DOWN"; const won = tradeState.side === winner; const ep = tradeState.entryMarketPrice ?? 0.5; const pnl = won ? (1 / ep) - 1 : -1; - if (won) runningStats.wins += 1; - else runningStats.losses += 1; + if (won) runningStats.wins += 1; else runningStats.losses += 1; runningStats.totalPnl += pnl; recentOutcomes.unshift({ slug: tradeState.slug, side: tradeState.side, won, pnl, ts: new Date().toISOString() }); if (recentOutcomes.length > 10) recentOutcomes.pop(); - // Write settlement row to CSV appendCsvRow("./logs/signals.csv", header, [ new Date().toISOString(), "SETTLED", "0", tradeState.slug, `${tradeState.side}:${won ? "WIN" : "LOSS"}`, "", "", "", "", "", "", - `${won ? "WIN" : "LOSS"}:${tradeState.side}`, - won ? "WIN" : "LOSS", - pnl.toFixed(4) + `${won ? "WIN" : "LOSS"}:${tradeState.side}`, won ? "WIN" : "LOSS", pnl.toFixed(4) ]); } - // Reset for new market tradeState = { slug: marketSlug, side: null, entryMarketPrice: null, priceToBeat: null, lastChainlinkPrice: currentPrice, hasSignal: false }; } else if (tradeState.slug === null || tradeState.slug === "") { tradeState.slug = marketSlug; } - - // Record first ENTER signal for this market if (!tradeState.hasSignal && rec.action === "ENTER" && marketSlug) { tradeState.side = rec.side; tradeState.entryMarketPrice = rec.side === "UP" ? marketUp : marketDown; tradeState.hasSignal = true; } - - // Always keep last known price and price-to-beat updated if (currentPrice !== null) tradeState.lastChainlinkPrice = currentPrice; if (priceToBeat !== null) tradeState.priceToBeat = priceToBeat; - const currentPriceBaseLine = colorPriceLine({ - label: "CURRENT PRICE", - price: currentPrice, - prevPrice: prevCurrentPrice, - decimals: 2, - prefix: "$" - }); - - const ptbDelta = (currentPrice !== null && priceToBeat !== null && Number.isFinite(currentPrice) && Number.isFinite(priceToBeat)) - ? currentPrice - priceToBeat - : null; - const ptbDeltaColor = ptbDelta === null - ? ANSI.gray - : ptbDelta > 0 - ? ANSI.green - : ptbDelta < 0 - ? ANSI.red - : ANSI.gray; - const ptbDeltaText = ptbDelta === null - ? `${ANSI.gray}-${ANSI.reset}` - : `${ptbDeltaColor}${ptbDelta > 0 ? "+" : ptbDelta < 0 ? "-" : ""}$${Math.abs(ptbDelta).toFixed(2)}${ANSI.reset}`; - const currentPriceValue = currentPriceBaseLine.split(": ")[1] ?? currentPriceBaseLine; - const currentPriceLine = kv("CURRENT PRICE:", `${currentPriceValue} (${ptbDeltaText})`); - if (poly.ok && poly.market && priceToBeatState.value === null) { const slug = safeFileSlug(poly.market.slug || poly.market.id || "market"); if (slug && !dumpedMarkets.has(slug)) { dumpedMarkets.add(slug); - try { - fs.mkdirSync("./logs", { recursive: true }); - fs.writeFileSync(path.join("./logs", `polymarket_market_${slug}.json`), JSON.stringify(poly.market, null, 2), "utf8"); - } catch { - // ignore - } + try { fs.mkdirSync("./logs", { recursive: true }); fs.writeFileSync(path.join("./logs", `polymarket_market_${slug}.json`), JSON.stringify(poly.market, null, 2), "utf8"); } catch { /* ignore */ } } } - const binanceSpotBaseLine = colorPriceLine({ label: "BTC (Binance)", price: spotPrice, prevPrice: prevSpotPrice, decimals: 0, prefix: "$" }); - const diffLine = (spotPrice !== null && currentPrice !== null && Number.isFinite(spotPrice) && Number.isFinite(currentPrice) && currentPrice !== 0) - ? (() => { - const diffUsd = spotPrice - currentPrice; - const diffPct = (diffUsd / currentPrice) * 100; - const sign = diffUsd > 0 ? "+" : diffUsd < 0 ? "-" : ""; - return ` (${sign}$${Math.abs(diffUsd).toFixed(2)}, ${sign}${Math.abs(diffPct).toFixed(2)}%)`; - })() - : ""; - const binanceSpotLine = `${binanceSpotBaseLine}${diffLine}`; - const binanceSpotValue = binanceSpotLine.split(": ")[1] ?? binanceSpotLine; - const binanceSpotKvLine = kv("BTC (Binance):", binanceSpotValue); - - const titleLine = poly.ok ? `${poly.market?.question ?? "-"}` : "-"; - const marketLine = kv("Market:", poly.ok ? (poly.market?.slug ?? "-") : "-"); - - const timeColor = timeLeftMin >= 10 && timeLeftMin <= 15 - ? ANSI.green - : timeLeftMin >= 5 && timeLeftMin < 10 - ? ANSI.yellow - : timeLeftMin >= 0 && timeLeftMin < 5 - ? ANSI.red - : ANSI.reset; - const timeLeftLine = `⏱ Time left: ${timeColor}${fmtTimeLeft(timeLeftMin)}${ANSI.reset}`; - - const polyTimeLeftColor = settlementLeftMin !== null - ? (settlementLeftMin >= 10 && settlementLeftMin <= 15 - ? ANSI.green - : settlementLeftMin >= 5 && settlementLeftMin < 10 - ? ANSI.yellow - : settlementLeftMin >= 0 && settlementLeftMin < 5 - ? ANSI.red - : ANSI.reset) - : ANSI.reset; + // Indicator formatting + const vwapSlopeLabel = vwapSlope === null ? "-" : vwapSlope > 0 ? "UP" : vwapSlope < 0 ? "DOWN" : "FLAT"; + const macdLabel = macd === null ? "-" : macd.hist < 0 ? (macd.histDelta !== null && macd.histDelta < 0 ? "bearish (exp)" : "bearish") : (macd.histDelta !== null && macd.histDelta > 0 ? "bullish (exp)" : "bullish"); + const lastCandle = klines1m.length ? klines1m[klines1m.length - 1] : null; + const lastClose = lastCandle?.close ?? null; + const close1mAgo = klines1m.length >= 2 ? klines1m[klines1m.length - 2]?.close ?? null : null; + const close3mAgo = klines1m.length >= 4 ? klines1m[klines1m.length - 4]?.close ?? null : null; + const delta1m = lastClose !== null && close1mAgo !== null ? lastClose - close1mAgo : null; + const delta3m = lastClose !== null && close3mAgo !== null ? lastClose - close3mAgo : null; + const haNarrative = (consec.color ?? "").toLowerCase() === "green" ? "LONG" : (consec.color ?? "").toLowerCase() === "red" ? "SHORT" : "NEUTRAL"; + const rsiNarrative = narrativeFromSlope(rsiSlope); + const macdNarrative = narrativeFromSign(macd?.hist ?? null); + const vwapNarrative = narrativeFromSign(vwapDist); + const rsiArrow = rsiSlope !== null && rsiSlope < 0 ? "\u2193" : rsiSlope !== null && rsiSlope > 0 ? "\u2191" : ""; + const delta1Narr = narrativeFromSign(delta1m); + const delta3Narr = narrativeFromSign(delta3m); + + const pLong = timeAware?.adjustedUp ?? null; + const pShort = timeAware?.adjustedDown ?? null; - const recColor = rec.action === "ENTER" ? ANSI.green : ANSI.gray; - const recLabel = rec.action === "ENTER" - ? `► ${rec.side === "UP" ? "BUY UP" : "BUY DOWN"} [${rec.phase} · ${rec.strength}]` - : `NO TRADE [${rec.phase}]`; - const recLine = `${recColor}${recLabel}${ANSI.reset}`; - - const lines = [ - titleLine, - marketLine, - kv("Time left:", `${timeColor}${fmtTimeLeft(timeLeftMin)}${ANSI.reset}`), - "", - sepLine(), - "", - kv("TA Predict:", predictValue), - kv("Heiken Ashi:", heikenLine.split(": ")[1] ?? heikenLine), - kv("RSI:", rsiLine.split(": ")[1] ?? rsiLine), - kv("MACD:", macdLine.split(": ")[1] ?? macdLine), - kv("Delta 1/3:", deltaLine.split(": ")[1] ?? deltaLine), - kv("VWAP:", vwapLine.split(": ")[1] ?? vwapLine), - "", - kv("Recommendation:", recLine), - "", - sepLine(), - "", - kv("POLYMARKET:", polyHeaderValue), - liquidity !== null ? kv("Liquidity:", formatNumber(liquidity, 0)) : null, - settlementLeftMin !== null ? kv("Time left:", `${polyTimeLeftColor}${fmtTimeLeft(settlementLeftMin)}${ANSI.reset}`) : null, - priceToBeat !== null ? kv("PRICE TO BEAT: ", `$${formatNumber(priceToBeat, 0)}`) : kv("PRICE TO BEAT: ", `${ANSI.gray}-${ANSI.reset}`), - currentPriceLine, - "", - sepLine(), - "", - binanceSpotKvLine, - "", - sepLine(), - "", - kv("ET | Session:", `${ANSI.white}${fmtEtTime(new Date())}${ANSI.reset} | ${ANSI.white}${getBtcSession(new Date())}${ANSI.reset}`), - "", - sepLine(), - "", - (() => { - const total = runningStats.wins + runningStats.losses; - const winRateStr = total > 0 ? `${((runningStats.wins / total) * 100).toFixed(0)}%` : "-"; - const pnlColor = runningStats.totalPnl > 0 ? ANSI.green : runningStats.totalPnl < 0 ? ANSI.red : ANSI.gray; - const pnlSign = runningStats.totalPnl > 0 ? "+" : ""; - const statsLine = `${ANSI.white}TRADE HISTORY${ANSI.reset} W:${ANSI.green}${runningStats.wins}${ANSI.reset} L:${ANSI.red}${runningStats.losses}${ANSI.reset} Win Rate:${ANSI.white}${winRateStr}${ANSI.reset} P&L:${pnlColor}${pnlSign}${runningStats.totalPnl.toFixed(2)} USDC${ANSI.reset}`; - return statsLine; - })(), - ...recentOutcomes.slice(0, 5).map((o, i) => { - const color = o.won ? ANSI.green : ANSI.red; - const label = o.won ? "WIN" : "LOSS"; - const pnlSign = o.pnl > 0 ? "+" : ""; - return `${ANSI.gray} ${i + 1}.${ANSI.reset} ${color}${label}${ANSI.reset} ${ANSI.dim}${o.side}${ANSI.reset} ${color}${pnlSign}${o.pnl.toFixed(2)} USDC${ANSI.reset} ${ANSI.gray}${o.slug.slice(0, 40)}${ANSI.reset}`; - }), - ...(() => { + // Confirm hint + const shortcutsHint = trading.tradingEnabled && !stdinError ? `${ANSI.dim}[B]${ANSI.reset} Comprar ${ANSI.dim}[S]${ANSI.reset} Vender ${ANSI.dim}[Q]${ANSI.reset} Sair` : `${ANSI.dim}[Q]${ANSI.reset} Sair`; + const confirmHint = (() => { + if (!pendingAction) return null; + if (pendingAction.type === "buy") { + const side = rec.action === "ENTER" ? rec.side : (pLong >= pShort ? "UP" : "DOWN"); + const sc = side === "UP" ? ANSI.green : ANSI.red; + const mp = side === "UP" ? marketUp : marketDown; + const ps = mp != null ? `@ ${(mp * 100).toFixed(1)}\u00A2` : ""; + return `${ANSI.yellow}\u26A1 BUY ${sc}${side}${ANSI.reset} ${ANSI.yellow}${ps} $${trading.tradeAmount}${ANSI.reset} ${ANSI.white}[Y]${ANSI.reset} Sim ${ANSI.white}[N]${ANSI.reset} Cancelar`; + } + if (pendingAction.type === "sell") { const pos = getPosition(); - const posPrice = pos.active - ? (pos.side === "UP" ? marketUp : marketDown) - : null; - const currentMktPrice = posPrice != null ? posPrice / 100 : null; - const exitEval = evaluateExit({ - position: pos, - modelUp: timeAware.adjustedUp, - modelDown: timeAware.adjustedDown, - currentMarketPrice: currentMktPrice, - timeLeftMin, - takeProfitPct: CONFIG.trading.takeProfitPct, - stopLossPct: CONFIG.trading.stopLossPct, - signalFlipMinProb: CONFIG.trading.signalFlipMinProb, - }); - return formatPositionLines({ position: pos, currentMarketPrice: currentMktPrice, tradingEnabled: trading.tradingEnabled, exitEval }); - })(), - "", - sepLine(), - centerText(`${ANSI.dim}${ANSI.gray}created by @krajekis${ANSI.reset}`, screenWidth()) - ].filter((x) => x !== null); - - renderScreen(lines.join("\n") + "\n"); + if (!pos.active) return `${ANSI.gray}Sem posicao${ANSI.reset} ${ANSI.white}[N]${ANSI.reset}`; + const sc = pos.side === "UP" ? ANSI.green : ANSI.red; + return `${ANSI.yellow}\u26A1 VENDER ${sc}${pos.side}${ANSI.reset} ${ANSI.yellow}${pos.shares.toFixed(2)} sh${ANSI.reset} ${ANSI.white}[Y]${ANSI.reset} Sim ${ANSI.white}[N]${ANSI.reset} Cancelar`; + } + return null; + })(); + + const timeColor = timeLeftMin >= 10 ? ANSI.green : timeLeftMin >= 5 ? ANSI.yellow : ANSI.red; + const liquidity = poly.ok ? (Number(poly.market?.liquidityNum) || Number(poly.market?.liquidity) || null) : null; + const recColor = rec.action === "ENTER" ? ANSI.green : ANSI.gray; + const recLabel = rec.action === "ENTER" ? `\u25BA ${rec.side === "UP" ? "BUY UP" : "BUY DOWN"} [${rec.phase}\u00B7${rec.strength}]` : `NO TRADE [${rec.phase}]`; + + // Chainlink display + const clLine = colorPriceLine({ label: "", price: currentPrice, prevPrice: prevCurrentPrice, decimals: 2, prefix: "$" }); + const ptbDelta = (currentPrice !== null && priceToBeat !== null) ? currentPrice - priceToBeat : null; + const ptbStr = ptbDelta === null ? "" : ` (${ptbDelta > 0 ? ANSI.green + "+" : ptbDelta < 0 ? ANSI.red : ANSI.gray}$${Math.abs(ptbDelta).toFixed(2)}${ANSI.reset})`; + + const pos = getPosition(); + const posPrice = pos.active ? (pos.side === "UP" ? marketUp : marketDown) : null; + const currentMktPrice = posPrice != null ? posPrice : null; + const exitEval = evaluateExit({ + position: pos, modelUp: pLong, modelDown: pShort, + currentMarketPrice: currentMktPrice, timeLeftMin, + takeProfitPct: CONFIG.trading.takeProfitPct, + stopLossPct: CONFIG.trading.stopLossPct, + signalFlipMinProb: CONFIG.trading.signalFlipMinProb, + }); + + const signal = rec.action === "ENTER" ? (rec.side === "UP" ? "BUY UP" : "BUY DOWN") : "NO TRADE"; + + renderScreen(buildScreen({ + title: poly.ok ? (poly.market?.question ?? "-") : "-", + modeTag: null, + marketSlug, + tradingEnabled: trading.tradingEnabled, + initError: trading.initError, + tradeAmount: trading.tradeAmount, + confirmHint, + shortcutsHint, + binanceSpot: `${colorPriceLine({ label: "", price: spotPrice, prevPrice: prevSpotPrice, decimals: 0, prefix: "$" })}`, + chainlinkLine: `${clLine}${ptbStr}`, + priceToBeat, + intervalLine: null, + marketUpStr: marketUp != null ? `${(marketUp * 100).toFixed(1)}\u00A2` : "-", + marketDownStr: marketDown != null ? `${(marketDown * 100).toFixed(1)}\u00A2` : "-", + timeLeftMin, + timeColor, + liquidity, + indicators: [ + { label: "Heiken Ashi", value: colorByNarrative(`${consec.color ?? "-"} x${consec.count}`, haNarrative) }, + { label: "RSI", value: colorByNarrative(`${formatNumber(rsiNow, 1)} ${rsiArrow}`, rsiNarrative) }, + { label: "MACD", value: colorByNarrative(macdLabel, macdNarrative) }, + { label: "\u0394 1/3 min", value: `${colorByNarrative(formatSignedDelta(delta1m, lastClose), delta1Narr)} | ${colorByNarrative(formatSignedDelta(delta3m, lastClose), delta3Narr)}` }, + { label: "VWAP", value: colorByNarrative(`${formatNumber(vwapNow, 0)} (${formatPct(vwapDist, 2)}) ${vwapSlopeLabel}`, vwapNarrative) }, + ], + predictValue: `${ANSI.green}LONG ${formatProbPct(pLong, 0)}${ANSI.reset} / ${ANSI.red}SHORT ${formatProbPct(pShort, 0)}${ANSI.reset}`, + recLine: `${recColor}${recLabel}${ANSI.reset}`, + position: pos, + currentMktPrice, + exitEval, + closedTrades, + runningStats, + recentOutcomes, + })); prevSpotPrice = spotPrice ?? prevSpotPrice; prevCurrentPrice = currentPrice ?? prevCurrentPrice; diff --git a/src/index5m.js b/src/index5m.js index 69e2c5d5..d2fc4b16 100644 --- a/src/index5m.js +++ b/src/index5m.js @@ -1,6 +1,6 @@ import { CONFIG } from "./config5m.js"; import { fetchKlines, fetchLastPrice } from "./data/binance.js"; -import { fetchChainlinkBtcUsd } from "./data/chainlink.js"; +import { fetchChainlinkBtcUsd, fetchChainlinkPriceAtMs } from "./data/chainlink.js"; import { startChainlinkPriceStream } from "./data/chainlinkWs.js"; import { startPolymarketChainlinkPriceStream } from "./data/polymarketLiveWs.js"; import { @@ -26,12 +26,12 @@ import fs from "node:fs"; import path from "node:path"; import { applyGlobalProxyFromEnv } from "./net/proxy.js"; import { - ANSI, screenWidth, sepLine, renderScreen, centerText, - kv, colorPriceLine, formatSignedDelta, + ANSI, renderScreen, buildScreen, kv, + colorPriceLine, formatSignedDelta, formatNumberDisplay, colorByNarrative, formatNarrativeValue, narrativeFromSign, - narrativeFromSlope, formatProbPct, fmtEtTime, fmtEtHHMM, getBtcSession, fmtTimeLeft, + narrativeFromSlope, formatProbPct, fmtEtHHMM, fmtTimeLeft, safeFileSlug, priceToBeatFromPolymarketMarket, - setStatusMessage, formatPositionLines + setStatusMessage } from "./display.js"; import { initTradingClient } from "./trading/client.js"; import { buyMarketOrder, sellMarketOrder } from "./trading/orders.js"; @@ -173,29 +173,44 @@ async function main() { const chainlinkStream = startChainlinkPriceStream({}); // --- Trading setup --- - let trading = { client: null, tradingEnabled: false, tradeAmount: 0 }; + let trading = { client: null, tradingEnabled: false, tradeAmount: 0, initError: null }; try { trading = await initTradingClient(CONFIG); - } catch { /* trading stays disabled */ } + } catch (err) { + trading.initError = err?.message ?? String(err); + } const actionQueue = []; + let pendingAction = null; let lastPoly = null; let lastRec = null; - if (trading.tradingEnabled) { + let stdinError = null; + try { + if (!process.stdin.isTTY) throw new Error("stdin não é TTY — rode com: node src/index5m.js"); process.stdin.setRawMode(true); process.stdin.resume(); process.stdin.on("data", (key) => { const ch = key.toString().toLowerCase(); - if (ch === "b") actionQueue.push({ type: "buy" }); - else if (ch === "s") actionQueue.push({ type: "sell" }); - else if (ch === "q" || key[0] === 0x03) process.exit(0); + if (trading.tradingEnabled) { + if (pendingAction !== null) { + if (ch === "y") { actionQueue.push({ ...pendingAction }); pendingAction = null; } + else if (ch === "n" || key[0] === 0x1b) { pendingAction = null; } + } else { + if (ch === "b") pendingAction = { type: "buy" }; + else if (ch === "s") pendingAction = { type: "sell" }; + } + } + if (ch === "q" || key[0] === 0x03) process.exit(0); }); + } catch (err) { + stdinError = err?.message ?? String(err); } let prevSpotPrice = null; let prevCurrentPrice = null; let priceToBeatState = { slug: null, value: null, setAtMs: null }; + let priceToBeatFetching = false; let tradeState = { slug: null, side: null, entryMarketPrice: null, priceToBeat: null, lastChainlinkPrice: null, hasSignal: false }; let runningStats = { wins: 0, losses: 0, totalPnl: 0 }; @@ -313,17 +328,21 @@ async function main() { const side = rec.action === "ENTER" ? rec.side : (timeAware.adjustedUp >= timeAware.adjustedDown ? "UP" : "DOWN"); const tokenId = side === "UP" ? poly.tokens.upTokenId : poly.tokens.downTokenId; const mktPrice = side === "UP" ? marketUp : marketDown; - const priceNum = mktPrice != null ? mktPrice / 100 : 0.5; + const priceNum = mktPrice != null ? mktPrice : 0.5; setStatusMessage(`Comprando ${side}...`); - const result = await buyMarketOrder({ client: trading.client, tokenId, amount: trading.tradeAmount }); + const result = await buyMarketOrder({ client: trading.client, tokenId, amount: trading.tradeAmount, price: priceNum }); if (result.ok) { - const shares = trading.tradeAmount / priceNum; - recordBuy({ side, tokenId, shares, entryPrice: priceNum, invested: trading.tradeAmount, marketSlug: marketSlugNow, orderId: result.order?.orderID }); const balance = await fetchPositionBalance(trading.client, tokenId); - if (balance > 0) recordBuy({ side, tokenId, shares: balance, entryPrice: priceNum, invested: trading.tradeAmount, marketSlug: marketSlugNow, orderId: result.order?.orderID }); - setStatusMessage(`COMPROU ${side} @ ${(priceNum * 100).toFixed(1)}¢ | $${trading.tradeAmount}`); + const shares = balance > 0 ? balance : trading.tradeAmount / priceNum; + recordBuy({ side, tokenId, shares, entryPrice: priceNum, invested: trading.tradeAmount, marketSlug: marketSlugNow, orderId: result.order?.orderID }); + const orderId = result.order?.orderID ?? result.order?.id ?? "-"; + const balanceStr = balance > 0 ? `shares: ${balance.toFixed(2)}` : "saldo 0 (ordem não preenchida?)"; + setStatusMessage(`COMPROU ${side} @ ${(priceNum * 100).toFixed(1)}¢ | $${trading.tradeAmount} | ${balanceStr} | ID: ${String(orderId).slice(0, 12)}`, 8000); } else { - setStatusMessage(`Erro: ${result.error}`); + const errMsg = `Erro na compra: ${result.error}`; + setStatusMessage(errMsg, 15000); + fs.mkdirSync("./logs", { recursive: true }); + fs.appendFileSync("./logs/trade_errors.log", `${new Date().toISOString()} BUY ${side} ${errMsg}\n`); } } } else if (action.type === "sell") { @@ -332,16 +351,21 @@ async function main() { setStatusMessage("Nenhuma posição para vender"); } else { setStatusMessage(`Vendendo ${pos.side}...`); - const result = await sellMarketOrder({ client: trading.client, tokenId: pos.tokenId, amount: pos.shares }); + const sellMktPrice = pos.side === "UP" ? marketUp : marketDown; + const sellPriceNum = sellMktPrice != null ? sellMktPrice : 0.5; + const result = await sellMarketOrder({ client: trading.client, tokenId: pos.tokenId, amount: pos.shares, price: sellPriceNum }); if (result.ok) { const mktPrice = pos.side === "UP" ? marketUp : marketDown; - const priceNum = mktPrice != null ? mktPrice / 100 : 0.5; + const priceNum = mktPrice != null ? mktPrice : 0.5; const pnl = (pos.shares * priceNum) - pos.invested; const sign = pnl >= 0 ? "+" : ""; - setStatusMessage(`VENDEU ${pos.side} | P&L: ${sign}$${pnl.toFixed(2)}`); + setStatusMessage(`VENDEU ${pos.side} | P&L: ${sign}$${pnl.toFixed(2)}`, 8000); recordSell(); } else { - setStatusMessage(`Erro: ${result.error}`); + const errMsg = `Erro na venda: ${result.error}`; + setStatusMessage(errMsg, 15000); + fs.mkdirSync("./logs", { recursive: true }); + fs.appendFileSync("./logs/trade_errors.log", `${new Date().toISOString()} SELL ${pos.side} ${errMsg}\n`); } } } @@ -385,33 +409,17 @@ async function main() { return `1m:${r1} | 3m:${r3}${acc}`; })(); const momNarrative = momentum?.roc1 != null ? narrativeFromSign(momentum.roc1) : "NEUTRAL"; - - // RSI - const rsiArrow = rsiSlope !== null && rsiSlope < 0 ? "\u2193" : rsiSlope !== null && rsiSlope > 0 ? "\u2191" : "-"; - const rsiValue = `${formatNumber(rsiNow, 1)} ${rsiArrow}`; + const rsiArrow = rsiSlope !== null && rsiSlope < 0 ? "\u2193" : rsiSlope !== null && rsiSlope > 0 ? "\u2191" : ""; const rsiNarrative = narrativeFromSlope(rsiSlope); - - // HA - const heikenValue = `${consec.color ?? "-"} x${consec.count}`; const haNarrative = (consec.color ?? "").toLowerCase() === "green" ? "LONG" : (consec.color ?? "").toLowerCase() === "red" ? "SHORT" : "NEUTRAL"; - - // VWAP const vwapSlopeLabel = vwapSlope === null ? "-" : vwapSlope > 0 ? "UP" : vwapSlope < 0 ? "DOWN" : "FLAT"; - const vwapValue = `${formatNumber(vwapNow, 0)} (${formatPct(vwapDist, 2)}) | slope: ${vwapSlopeLabel}`; const vwapNarrative = narrativeFromSign(vwapDist); - - // Delta - const delta1Narrative = narrativeFromSign(delta1m); - const delta3Narrative = narrativeFromSign(delta3m); - const deltaValue = `${colorByNarrative(formatSignedDelta(delta1m, lastClose), delta1Narrative)} | ${colorByNarrative(formatSignedDelta(delta3m, lastClose), delta3Narrative)}`; + const delta1Narr = narrativeFromSign(delta1m); + const delta3Narr = narrativeFromSign(delta3m); const signal = rec.action === "ENTER" ? (rec.side === "UP" ? "BUY UP" : "BUY DOWN") : "NO TRADE"; - const recColor = rec.action === "ENTER" ? ANSI.green : ANSI.gray; - const recLabel = rec.action === "ENTER" - ? `\u25BA ${rec.side === "UP" ? "BUY UP" : "BUY DOWN"} [${rec.phase} \u00B7 ${rec.strength}]` - : `NO TRADE [${rec.phase}]`; - const recLine = `${recColor}${recLabel}${ANSI.reset}`; + const recLabel = rec.action === "ENTER" ? `\u25BA ${rec.side === "UP" ? "BUY UP" : "BUY DOWN"} [${rec.phase}\u00B7${rec.strength}]` : `NO TRADE [${rec.phase}]`; const spotPrice = wsPrice ?? lastPrice; const currentPrice = chainlink?.price ?? null; @@ -421,11 +429,28 @@ async function main() { if (marketSlug && priceToBeatState.slug !== marketSlug) { priceToBeatState = { slug: marketSlug, value: null, setAtMs: null }; } - if (priceToBeatState.slug && priceToBeatState.value === null && currentPrice !== null) { + if (priceToBeatState.slug && priceToBeatState.value === null && poly.ok && poly.market) { + const fromMarket = priceToBeatFromPolymarketMarket(poly.market); + if (fromMarket !== null) priceToBeatState = { slug: priceToBeatState.slug, value: fromMarket, setAtMs: Date.now(), source: "market" }; + } + if (priceToBeatState.slug && priceToBeatState.value === null && !priceToBeatFetching) { const nowMs = Date.now(); const okToLatch = marketStartMs === null ? true : nowMs >= marketStartMs; if (okToLatch) { - priceToBeatState = { slug: priceToBeatState.slug, value: Number(currentPrice), setAtMs: nowMs }; + const lateMs = marketStartMs !== null ? nowMs - marketStartMs : 0; + if (lateMs > 30_000 && marketStartMs !== null) { + priceToBeatFetching = true; + fetchChainlinkPriceAtMs(marketStartMs).then((p) => { + if (p !== null && priceToBeatState.slug === marketSlug && priceToBeatState.value === null) { + priceToBeatState = { slug: marketSlug, value: p, setAtMs: marketStartMs, source: "chainlink_historical" }; + } else if (p === null && currentPrice !== null && priceToBeatState.slug === marketSlug && priceToBeatState.value === null) { + priceToBeatState = { slug: marketSlug, value: Number(currentPrice), setAtMs: nowMs, source: "chainlink_latch" }; + } + priceToBeatFetching = false; + }).catch(() => { priceToBeatFetching = false; }); + } else if (currentPrice !== null) { + priceToBeatState = { slug: priceToBeatState.slug, value: Number(currentPrice), setAtMs: nowMs, source: "chainlink_latch" }; + } } } const priceToBeat = priceToBeatState.slug === marketSlug ? priceToBeatState.value : null; @@ -437,162 +462,111 @@ async function main() { const won = tradeState.side === winner; const ep = tradeState.entryMarketPrice ?? 0.5; const pnl = won ? (1 / ep) - 1 : -1; - if (won) runningStats.wins += 1; - else runningStats.losses += 1; + if (won) runningStats.wins += 1; else runningStats.losses += 1; runningStats.totalPnl += pnl; recentOutcomes.unshift({ slug: tradeState.slug, side: tradeState.side, won, pnl, ts: new Date().toISOString() }); if (recentOutcomes.length > 10) recentOutcomes.pop(); appendCsvRow("./logs/signals_5m.csv", header, [ new Date().toISOString(), "SETTLED", "0", "", "", "", "", "", "", "", "", `${tradeState.side}:${won ? "WIN" : "LOSS"}`, "", "", "", "", "", - `${won ? "WIN" : "LOSS"}:${tradeState.side}`, - won ? "WIN" : "LOSS", - pnl.toFixed(4) + `${won ? "WIN" : "LOSS"}:${tradeState.side}`, won ? "WIN" : "LOSS", pnl.toFixed(4) ]); } tradeState = { slug: marketSlug, side: null, entryMarketPrice: null, priceToBeat: null, lastChainlinkPrice: currentPrice, hasSignal: false }; - } else if (tradeState.slug === null || tradeState.slug === "") { - tradeState.slug = marketSlug; - } - - if (!tradeState.hasSignal && rec.action === "ENTER" && marketSlug) { - tradeState.side = rec.side; - tradeState.entryMarketPrice = rec.side === "UP" ? marketUp : marketDown; - tradeState.hasSignal = true; - } + } else if (tradeState.slug === null || tradeState.slug === "") { tradeState.slug = marketSlug; } + if (!tradeState.hasSignal && rec.action === "ENTER" && marketSlug) { tradeState.side = rec.side; tradeState.entryMarketPrice = rec.side === "UP" ? marketUp : marketDown; tradeState.hasSignal = true; } if (currentPrice !== null) tradeState.lastChainlinkPrice = currentPrice; if (priceToBeat !== null) tradeState.priceToBeat = priceToBeat; - // Price to beat display - const currentPriceBaseLine = colorPriceLine({ label: "CURRENT PRICE", price: currentPrice, prevPrice: prevCurrentPrice, decimals: 2, prefix: "$" }); - const ptbDelta = (currentPrice !== null && priceToBeat !== null && Number.isFinite(currentPrice) && Number.isFinite(priceToBeat)) - ? currentPrice - priceToBeat : null; - const ptbDeltaColor = ptbDelta === null ? ANSI.gray : ptbDelta > 0 ? ANSI.green : ptbDelta < 0 ? ANSI.red : ANSI.gray; - const ptbDeltaText = ptbDelta === null - ? `${ANSI.gray}-${ANSI.reset}` - : `${ptbDeltaColor}${ptbDelta > 0 ? "+" : ptbDelta < 0 ? "-" : ""}$${Math.abs(ptbDelta).toFixed(2)}${ANSI.reset}`; - const currentPriceValue = currentPriceBaseLine.split(": ")[1] ?? currentPriceBaseLine; - const currentPriceLine = kv("CURRENT PRICE:", `${currentPriceValue} (${ptbDeltaText})`); - if (poly.ok && poly.market && priceToBeatState.value === null) { const slug = safeFileSlug(poly.market.slug || poly.market.id || "market"); if (slug && !dumpedMarkets.has(slug)) { dumpedMarkets.add(slug); - try { - fs.mkdirSync("./logs", { recursive: true }); - fs.writeFileSync(path.join("./logs", `polymarket_market_${slug}.json`), JSON.stringify(poly.market, null, 2), "utf8"); - } catch { /* ignore */ } + try { fs.mkdirSync("./logs", { recursive: true }); fs.writeFileSync(path.join("./logs", `polymarket_market_${slug}.json`), JSON.stringify(poly.market, null, 2), "utf8"); } catch { /* ignore */ } } } - const binanceSpotBaseLine = colorPriceLine({ label: "BTC (Binance)", price: spotPrice, prevPrice: prevSpotPrice, decimals: 0, prefix: "$" }); - const diffLine = (spotPrice !== null && currentPrice !== null && Number.isFinite(spotPrice) && Number.isFinite(currentPrice) && currentPrice !== 0) - ? (() => { - const diffUsd = spotPrice - currentPrice; - const diffPct = (diffUsd / currentPrice) * 100; - const sign = diffUsd > 0 ? "+" : diffUsd < 0 ? "-" : ""; - return ` (${sign}$${Math.abs(diffUsd).toFixed(2)}, ${sign}${Math.abs(diffPct).toFixed(2)}%)`; - })() - : ""; - const binanceSpotValue = (binanceSpotBaseLine + diffLine).split(": ")[1] ?? (binanceSpotBaseLine + diffLine); - const binanceSpotKvLine = kv("BTC (Binance):", binanceSpotValue); - const isNextMarket = marketStartMs !== null && marketStartMs > Date.now(); - const modeTag = isNextMarket ? `${ANSI.yellow}[5m MODE]${ANSI.reset} ${ANSI.yellow}[PRÓXIMO MERCADO]${ANSI.reset}` : `${ANSI.yellow}[5m MODE]${ANSI.reset}`; - const titleLine = poly.ok ? `${poly.market?.question ?? "-"}` : "-"; - const marketLine = kv("Market:", poly.ok ? (poly.market?.slug ?? "-") : "-"); - const intervalLabel = isNextMarket ? "Próx. intervalo:" : "Interval:"; - const intervalLine = (marketStartMs !== null && settlementMs !== null) - ? kv(intervalLabel, `${isNextMarket ? ANSI.yellow : ""}${fmtEtHHMM(marketStartMs)} → ${fmtEtHHMM(settlementMs)} ET${isNextMarket ? ANSI.reset : ""}`) - : null; - - const timeColor = isNextMarket - ? ANSI.yellow - : timeLeftMin >= 3 ? ANSI.green : timeLeftMin >= 1.5 ? ANSI.yellow : ANSI.red; - const startsInMin = isNextMarket && marketStartMs !== null ? (marketStartMs - Date.now()) / 60_000 : null; + const timeColor = isNextMarket ? ANSI.yellow : timeLeftMin >= 3 ? ANSI.green : timeLeftMin >= 1.5 ? ANSI.yellow : ANSI.red; + const liquidity = poly.ok ? (Number(poly.market?.liquidityNum) || Number(poly.market?.liquidity) || null) : null; + const settlementMs5m = poly.ok && poly.market?.endDate ? new Date(poly.market.endDate).getTime() : null; + + const clLine = colorPriceLine({ label: "", price: currentPrice, prevPrice: prevCurrentPrice, decimals: 2, prefix: "$" }); + const ptbDelta = (currentPrice !== null && priceToBeat !== null) ? currentPrice - priceToBeat : null; + const ptbStr = ptbDelta === null ? "" : ` (${ptbDelta > 0 ? ANSI.green + "+" : ptbDelta < 0 ? ANSI.red : ANSI.gray}$${Math.abs(ptbDelta).toFixed(2)}${ANSI.reset})`; + + const shortcutsHint = trading.tradingEnabled && !stdinError ? `${ANSI.dim}[B]${ANSI.reset} Comprar ${ANSI.dim}[S]${ANSI.reset} Vender ${ANSI.dim}[Q]${ANSI.reset} Sair` : `${ANSI.dim}[Q]${ANSI.reset} Sair`; + const confirmHint = (() => { + if (!pendingAction) return null; + if (pendingAction.type === "buy") { + const side = rec.action === "ENTER" ? rec.side : (timeAware.adjustedUp >= timeAware.adjustedDown ? "UP" : "DOWN"); + const sc = side === "UP" ? ANSI.green : ANSI.red; + const mp = side === "UP" ? marketUp : marketDown; + const ps = mp != null ? `@ ${(mp * 100).toFixed(1)}\u00A2` : ""; + return `${ANSI.yellow}\u26A1 BUY ${sc}${side}${ANSI.reset} ${ANSI.yellow}${ps} $${trading.tradeAmount}${ANSI.reset} ${ANSI.white}[Y]${ANSI.reset} Sim ${ANSI.white}[N]${ANSI.reset} Cancelar`; + } + if (pendingAction.type === "sell") { + const pos = getPosition(); + if (!pos.active) return `${ANSI.gray}Sem posicao${ANSI.reset} ${ANSI.white}[N]${ANSI.reset}`; + const sc = pos.side === "UP" ? ANSI.green : ANSI.red; + return `${ANSI.yellow}\u26A1 VENDER ${sc}${pos.side}${ANSI.reset} ${ANSI.yellow}${pos.shares.toFixed(2)} sh${ANSI.reset} ${ANSI.white}[Y]${ANSI.reset} Sim ${ANSI.white}[N]${ANSI.reset} Cancelar`; + } + return null; + })(); - const polyTimeLeftColor = settlementLeftMin !== null - ? (settlementLeftMin >= 3 ? ANSI.green : settlementLeftMin >= 1.5 ? ANSI.yellow : ANSI.red) - : ANSI.reset; + const intervalLine = (marketStartMs !== null && settlementMs5m !== null) + ? kv("Intervalo:", `${isNextMarket ? ANSI.yellow : ""}${fmtEtHHMM(marketStartMs)} \u2192 ${fmtEtHHMM(settlementMs5m)} ET${isNextMarket ? ANSI.reset : ""}`) + : null; - const liquidity = poly.ok ? (Number(poly.market?.liquidityNum) || Number(poly.market?.liquidity) || null) : null; + const pos = getPosition(); + const posPrice = pos.active ? (pos.side === "UP" ? marketUp : marketDown) : null; + const currentMktPrice = posPrice != null ? posPrice : null; + const exitEval = evaluateExit({ + position: pos, modelUp: timeAware.adjustedUp, modelDown: timeAware.adjustedDown, + currentMarketPrice: currentMktPrice, timeLeftMin, + takeProfitPct: CONFIG.trading.takeProfitPct, + stopLossPct: CONFIG.trading.stopLossPct, + signalFlipMinProb: CONFIG.trading.signalFlipMinProb, + }); - const lines = [ - `${modeTag} ${titleLine}`, - marketLine, + const modeTag = isNextMarket ? `${ANSI.yellow}[5m] [PROXIMO]${ANSI.reset}` : `${ANSI.yellow}[5m]${ANSI.reset}`; + + renderScreen(buildScreen({ + title: poly.ok ? (poly.market?.question ?? "-") : "-", + modeTag, + marketSlug, + tradingEnabled: trading.tradingEnabled, + initError: trading.initError, + tradeAmount: trading.tradeAmount, + confirmHint, + shortcutsHint, + binanceSpot: `${colorPriceLine({ label: "", price: spotPrice, prevPrice: prevSpotPrice, decimals: 0, prefix: "$" })}`, + chainlinkLine: `${clLine}${ptbStr}`, + priceToBeat, intervalLine, - isNextMarket && startsInMin !== null - ? kv("Inicia em:", `${ANSI.yellow}${fmtTimeLeft(startsInMin)}${ANSI.reset}`) - : kv("Time left:", `${timeColor}${fmtTimeLeft(timeLeftMin)}${ANSI.reset}`), - "", - sepLine(), - "", - kv("ORDER FLOW:", ofiValue), - kv("Momentum:", formatNarrativeValue("", momLabel, momNarrative).slice(2)), - kv("EMA Cross:", formatNarrativeValue("", emaLabel, emaNarrative).slice(2)), - kv("RSI:", formatNarrativeValue("", rsiValue, rsiNarrative).slice(2)), - kv("Heiken Ashi:", formatNarrativeValue("", heikenValue, haNarrative).slice(2)), - kv("VWAP:", formatNarrativeValue("", vwapValue, vwapNarrative).slice(2)), - kv("Delta 1/3:", deltaValue), - "", - kv("TA Predict:", predictValue), - kv("Recommendation:", recLine), - "", - sepLine(), - "", - kv("POLYMARKET:", polyHeaderValue), - liquidity !== null ? kv("Liquidity:", formatNumber(liquidity, 0)) : null, - settlementLeftMin !== null ? kv("Time left:", `${polyTimeLeftColor}${fmtTimeLeft(settlementLeftMin)}${ANSI.reset}`) : null, - priceToBeat !== null ? kv("PRICE TO BEAT: ", `$${formatNumber(priceToBeat, 0)}`) : kv("PRICE TO BEAT: ", `${ANSI.gray}-${ANSI.reset}`), - currentPriceLine, - "", - sepLine(), - "", - binanceSpotKvLine, - "", - sepLine(), - "", - kv("ET | Session:", `${ANSI.white}${fmtEtTime(new Date())}${ANSI.reset} | ${ANSI.white}${getBtcSession(new Date())}${ANSI.reset}`), - "", - sepLine(), - "", - (() => { - const total = runningStats.wins + runningStats.losses; - const winRateStr = total > 0 ? `${((runningStats.wins / total) * 100).toFixed(0)}%` : "-"; - const pnlColor = runningStats.totalPnl > 0 ? ANSI.green : runningStats.totalPnl < 0 ? ANSI.red : ANSI.gray; - const pnlSign = runningStats.totalPnl > 0 ? "+" : ""; - return `${ANSI.white}TRADE HISTORY${ANSI.reset} W:${ANSI.green}${runningStats.wins}${ANSI.reset} L:${ANSI.red}${runningStats.losses}${ANSI.reset} Win Rate:${ANSI.white}${winRateStr}${ANSI.reset} P&L:${pnlColor}${pnlSign}${runningStats.totalPnl.toFixed(2)} USDC${ANSI.reset}`; - })(), - ...recentOutcomes.slice(0, 5).map((o, i) => { - const color = o.won ? ANSI.green : ANSI.red; - const label = o.won ? "WIN" : "LOSS"; - const pnlSign = o.pnl > 0 ? "+" : ""; - return `${ANSI.gray} ${i + 1}.${ANSI.reset} ${color}${label}${ANSI.reset} ${ANSI.dim}${o.side}${ANSI.reset} ${color}${pnlSign}${o.pnl.toFixed(2)} USDC${ANSI.reset} ${ANSI.gray}${o.slug.slice(0, 40)}${ANSI.reset}`; - }), - ...(() => { - const pos = getPosition(); - const posPrice = pos.active - ? (pos.side === "UP" ? marketUp : marketDown) - : null; - const currentMktPrice = posPrice != null ? posPrice / 100 : null; - const exitEval = evaluateExit({ - position: pos, - modelUp: timeAware.adjustedUp, - modelDown: timeAware.adjustedDown, - currentMarketPrice: currentMktPrice, - timeLeftMin, - takeProfitPct: CONFIG.trading.takeProfitPct, - stopLossPct: CONFIG.trading.stopLossPct, - signalFlipMinProb: CONFIG.trading.signalFlipMinProb, - }); - return formatPositionLines({ position: pos, currentMarketPrice: currentMktPrice, tradingEnabled: trading.tradingEnabled, exitEval }); - })(), - "", - sepLine(), - centerText(`${ANSI.dim}${ANSI.gray}created by @krajekis${ANSI.reset}`, screenWidth()) - ].filter(x => x !== null); - - renderScreen(lines.join("\n") + "\n"); + marketUpStr: marketUp != null ? `${(marketUp * 100).toFixed(1)}\u00A2` : "-", + marketDownStr: marketDown != null ? `${(marketDown * 100).toFixed(1)}\u00A2` : "-", + timeLeftMin, + timeColor, + liquidity, + indicators: [ + { label: "Order Flow", value: ofiValue }, + { label: "Momentum", value: colorByNarrative(momLabel, momNarrative) }, + { label: "EMA Cross", value: colorByNarrative(emaLabel, emaNarrative) }, + { label: "RSI", value: colorByNarrative(`${formatNumber(rsiNow, 1)} ${rsiArrow}`, rsiNarrative) }, + { label: "Heiken Ashi", value: colorByNarrative(`${consec.color ?? "-"} x${consec.count}`, haNarrative) }, + { label: "VWAP", value: colorByNarrative(`${formatNumber(vwapNow, 0)} (${formatPct(vwapDist, 2)}) ${vwapSlopeLabel}`, vwapNarrative) }, + { label: "\u0394 1/3 min", value: `${colorByNarrative(formatSignedDelta(delta1m, lastClose), delta1Narr)} | ${colorByNarrative(formatSignedDelta(delta3m, lastClose), delta3Narr)}` }, + ], + predictValue, + recLine: `${recColor}${recLabel}${ANSI.reset}`, + position: pos, + currentMktPrice, + exitEval, + closedTrades: [], + runningStats, + recentOutcomes, + })); prevSpotPrice = spotPrice ?? prevSpotPrice; prevCurrentPrice = currentPrice ?? prevCurrentPrice; diff --git a/src/trading/client.js b/src/trading/client.js index 3ab81286..ecb01cd9 100644 --- a/src/trading/client.js +++ b/src/trading/client.js @@ -1,8 +1,17 @@ import { ClobClient, SignatureType } from "@polymarket/clob-client"; import { Wallet } from "ethers"; +import fs from "node:fs"; let _cached = null; +function logTrading(msg) { + try { + fs.mkdirSync("./logs", { recursive: true }); + fs.appendFileSync("./logs/trade_orders.log", + `${new Date().toISOString()} [CLIENT] ${msg}\n`); + } catch { /* ignore */ } +} + export async function initTradingClient(config) { if (_cached) return _cached; @@ -13,13 +22,27 @@ export async function initTradingClient(config) { return _cached; } - const signer = new Wallet(privateKey); + const _wallet = new Wallet(privateKey); + // clob-client v5 detects ethers v5 signers via _signTypedData (renamed to + // signTypedData in ethers v6). Expose both so the library uses the right path. + const signer = Object.assign(_wallet, { + _signTypedData: (domain, types, value) => _wallet.signTypedData(domain, types, value), + getAddress: () => Promise.resolve(_wallet.address), + }); const sigType = signatureType === 1 ? SignatureType.POLY_PROXY : signatureType === 2 ? SignatureType.POLY_GNOSIS_SAFE : SignatureType.EOA; - const funderAddr = funder || signer.address; + // For EOA, funder should be undefined (not the signer address) so the library + // uses signer address as maker directly. + const funderAddr = sigType === SignatureType.EOA + ? undefined + : (funder || undefined); + + const sigTypeName = sigType === SignatureType.POLY_PROXY ? "POLY_PROXY" + : sigType === SignatureType.POLY_GNOSIS_SAFE ? "GNOSIS_SAFE" : "EOA"; + logTrading(`EOA=${_wallet.address} funder=${funderAddr ?? "(none)"} sigType=${sigTypeName}(${sigType})`); const clientL1 = new ClobClient( config.clobBaseUrl, @@ -31,6 +54,7 @@ export async function initTradingClient(config) { ); const creds = await clientL1.createOrDeriveApiKey(); + logTrading(`API key derived: ${creds.key ? "OK" : "MISSING"}`); const client = new ClobClient( config.clobBaseUrl, @@ -44,3 +68,8 @@ export async function initTradingClient(config) { _cached = { client, tradingEnabled: true, tradeAmount }; return _cached; } + +/** Force re-derive client on next init (useful after config change). */ +export function resetTradingClient() { + _cached = null; +} diff --git a/src/trading/orders.js b/src/trading/orders.js index e17b5e73..01fc0926 100644 --- a/src/trading/orders.js +++ b/src/trading/orders.js @@ -1,27 +1,63 @@ import { Side } from "@polymarket/clob-client"; +import fs from "node:fs"; -export async function buyMarketOrder({ client, tokenId, amount }) { +function logOrder(action, data) { try { - const order = await client.createAndPostMarketOrder({ + fs.mkdirSync("./logs", { recursive: true }); + fs.appendFileSync("./logs/trade_orders.log", + `${new Date().toISOString()} [${action}] ${JSON.stringify(data)}\n`); + } catch { /* ignore */ } +} + +export async function buyMarketOrder({ client, tokenId, amount, price }) { + try { + const userOrder = { tokenID: tokenId, amount, side: Side.BUY, - }); + }; + if (price != null && price > 0 && price < 1) { + userOrder.price = price; + } + logOrder("BUY_REQ", userOrder); + const order = await client.createAndPostMarketOrder(userOrder); + logOrder("BUY_RES", order); + + // Check if the order was actually accepted/matched + const success = order && !order.error && !order.errorMsg && order.success !== false && order.status !== "DEAD"; + if (!success) { + const reason = order?.error || order?.errorMsg || order?.status || "ordem não preenchida (FOK rejeitada)"; + return { ok: false, error: reason, order }; + } return { ok: true, order }; } catch (err) { + logOrder("BUY_ERR", { error: err?.message ?? String(err) }); return { ok: false, error: err?.message ?? String(err) }; } } -export async function sellMarketOrder({ client, tokenId, amount }) { +export async function sellMarketOrder({ client, tokenId, amount, price }) { try { - const order = await client.createAndPostMarketOrder({ + const userOrder = { tokenID: tokenId, amount, side: Side.SELL, - }); + }; + if (price != null && price > 0 && price < 1) { + userOrder.price = price; + } + logOrder("SELL_REQ", userOrder); + const order = await client.createAndPostMarketOrder(userOrder); + logOrder("SELL_RES", order); + + const success = order && !order.error && !order.errorMsg && order.success !== false && order.status !== "DEAD"; + if (!success) { + const reason = order?.error || order?.errorMsg || order?.status || "ordem não preenchida (FOK rejeitada)"; + return { ok: false, error: reason, order }; + } return { ok: true, order }; } catch (err) { + logOrder("SELL_ERR", { error: err?.message ?? String(err) }); return { ok: false, error: err?.message ?? String(err) }; } } From 9cd376698183fe19dd80c84c5ee0e0eefda2d71a Mon Sep 17 00:00:00 2001 From: Jean Carraro Date: Mon, 6 Apr 2026 12:35:04 -0300 Subject: [PATCH 06/49] Add live trading improvements: USDC balance, GnosisSafe auth, FAK orders, model-gated exits - Display USDC.e balance on-chain (Polygon) in header, refreshed every 30s - Auto-detect POLY_GNOSIS_SAFE when funder is a GnosisSafe contract (fixes invalid signature) - Switch orders from FOK to FAK to allow partial fills - Buy price uses bestAsk+0.02, sell uses bestBid-0.02 for reliable matching - Fetch actual on-chain share balance before selling (fixes balance mismatch error) - TP and SL exit signals now require model to also confirm reversal - TIME_DECAY exit skipped when entry price < 0.50 (cheap positions held to resolution) - Document new env vars: TRADE_TAKE_PROFIT_PCT, TRADE_STOP_LOSS_PCT, TRADE_SIGNAL_FLIP_PROB Co-Authored-By: Claude Sonnet 4.6 --- .env.example | 23 ++++++++++-- CLAUDE.md | 13 ++++--- src/display.js | 9 ++++- src/index.js | 46 +++++++++++++++++------- src/index5m.js | 44 ++++++++++++++++------- src/trading/client.js | 39 ++++++++++++++++++-- src/trading/orders.js | 18 +++++----- src/trading/position.js | 79 +++++++++++++++++++++++++++++++++-------- 8 files changed, 211 insertions(+), 60 deletions(-) diff --git a/.env.example b/.env.example index 995fb60b..15667e64 100644 --- a/.env.example +++ b/.env.example @@ -25,12 +25,31 @@ # Endereço do perfil Polymarket (necessário se a conta foi criada pelo site/email) # POLYMARKET_FUNDER=0x... -# Tipo de assinatura: 0=EOA (carteira direta), 1=POLY_PROXY (login por email), 2=GNOSIS_SAFE -# POLYMARKET_SIGNATURE_TYPE=0 +# Tipo de assinatura: +# 0 = EOA (carteira direta, sem proxy) +# 1 = POLY_PROXY (proxy Polymarket — auto-detectado se POLYMARKET_FUNDER for GnosisSafe) +# 2 = GNOSIS_SAFE (conta criada pelo site Polymarket via Metamask — mais comum) +# POLYMARKET_SIGNATURE_TYPE=2 # Valor em USDC por trade (padrão: 5) # POLYMARKET_TRADE_AMOUNT=5 +# ───────────────────────────────────────────── +# Thresholds de saída de posição (exit strategy) +# ───────────────────────────────────────────── + +# Take profit: % de ROI para recomendar venda com lucro (padrão: 20) +# Só dispara se o modelo também confirmar reversão. +# TRADE_TAKE_PROFIT_PCT=20 + +# Stop loss: % de ROI negativo para recomendar saída (padrão: 25) +# Só dispara se o modelo também confirmar reversão. +# TRADE_STOP_LOSS_PCT=25 + +# Probabilidade mínima do lado oposto para considerar que o modelo inverteu (padrão: 0.58) +# Usado como gatilho para TP, SL e SIGNAL_FLIPPED. +# TRADE_SIGNAL_FLIP_PROB=0.58 + # ───────────────────────────────────────────── # Chainlink / Polygon RPC # ───────────────────────────────────────────── diff --git a/CLAUDE.md b/CLAUDE.md index bd6ff144..14495507 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -61,9 +61,9 @@ All tunable parameters (poll interval, TA periods, Polymarket series IDs, RPC UR Optional live-trading integration using `@polymarket/clob-client` SDK. Enabled when `POLYMARKET_PRIVATE_KEY` is set; otherwise the app runs in read-only mode. -- **client.js** — Initializes `ClobClient` with L1 (EIP-712) + L2 (HMAC) auth. Derives API credentials on first run via `createOrDeriveApiKey()`. Caches the client singleton. -- **orders.js** — `buyMarketOrder()` and `sellMarketOrder()` wrappers around `client.createAndPostMarketOrder()`. Returns `{ ok, order }` or `{ ok: false, error }`. -- **position.js** — In-memory position state: `recordBuy()`, `recordSell()`, `getPosition()`, `computeROI()`, `resetIfMarketChanged()`. Also `fetchPositionBalance()` to sync shares from chain via `getBalanceAllowance()`. +- **client.js** — Initializes `ClobClient` with L1 (EIP-712) + L2 (HMAC) auth. Derives API credentials on first run via `createOrDeriveApiKey()`. Caches the client singleton. Auto-detects `POLY_GNOSIS_SAFE` when `POLYMARKET_SIGNATURE_TYPE=1` but the funder address is a GnosisSafe contract. Exposes `balanceAddress` (funder or EOA) for USDC balance queries. +- **orders.js** — `buyMarketOrder()` and `sellMarketOrder()` wrappers around `client.createAndPostMarketOrder()` using `OrderType.FAK` (Fill and Kill — partial fills accepted). Buy price = `bestAsk + 0.02`; sell price = `bestBid - 0.02`, both clamped to valid range. Returns `{ ok, order }` or `{ ok: false, error }`. +- **position.js** — In-memory position state: `recordBuy()`, `recordSell()`, `getPosition()`, `computeROI()`, `resetIfMarketChanged()`. `fetchPositionBalance()` syncs shares from chain via `getBalanceAllowance()` (used before selling to get actual on-chain balance). `fetchUsdcBalance(address)` reads USDC.e balance directly from Polygon blockchain (not the CLOB API, which only tracks deposited collateral). `evaluateExit()` recommends exits: TP and SL only trigger when the model also confirms reversal (`oppositeProb >= signalFlipMinProb`); TIME_DECAY only applies when entry price ≥ 0.50 (cheap entries are held to resolution). Both main loops listen for keypresses when trading is enabled: **[B]** buy the recommended side, **[S]** sell 100% of position, **[Q]** quit. Actions are queued and processed inside the main loop where market data is available. @@ -84,9 +84,12 @@ Called once at startup via `applyGlobalProxyFromEnv()`. Reads `HTTPS_PROXY`/`HTT | `POLYMARKET_5M_SERIES_SLUG` | `btc-up-or-down-5m` | Series slug for 5m markets | | `HTTPS_PROXY` / `ALL_PROXY` | — | Proxy for all outbound connections | | `POLYMARKET_PRIVATE_KEY` | — | Polygon wallet private key (enables trading) | -| `POLYMARKET_FUNDER` | (derived from key) | Polymarket profile address (for proxy wallets) | -| `POLYMARKET_SIGNATURE_TYPE` | `0` | `0`=EOA, `1`=POLY_PROXY, `2`=GNOSIS_SAFE | +| `POLYMARKET_FUNDER` | (derived from key) | Polymarket profile address (proxy/GnosisSafe wallet) | +| `POLYMARKET_SIGNATURE_TYPE` | `0` | `0`=EOA, `1`=POLY_PROXY (auto-detects GnosisSafe), `2`=GNOSIS_SAFE | | `POLYMARKET_TRADE_AMOUNT` | `5` | USDC amount per trade | +| `TRADE_TAKE_PROFIT_PCT` | `20` | ROI % to recommend take-profit (requires model reversal) | +| `TRADE_STOP_LOSS_PCT` | `25` | ROI % loss to recommend stop-loss (requires model reversal) | +| `TRADE_SIGNAL_FLIP_PROB` | `0.58` | Min opposite-side probability to consider model reversed | ## Output diff --git a/src/display.js b/src/display.js index c998f26c..06cc0146 100644 --- a/src/display.js +++ b/src/display.js @@ -287,11 +287,18 @@ export function buildScreen(d) { // Trading status + shortcuts + clock on a single line const etStr = `${ANSI.dim}${fmtEtTime()} ${getBtcSession()}${ANSI.reset}`; - const tradingBadge = d.tradingEnabled + let tradingBadge = d.tradingEnabled ? `${ANSI.green}● ATIVO${ANSI.reset} $${d.tradeAmount}` : d.initError ? `${ANSI.red}● ERRO${ANSI.reset}` : `${ANSI.gray}● LEITURA${ANSI.reset}`; + if (d.tradingEnabled) { + if (d.usdcBalanceError) { + tradingBadge += ` ${ANSI.red}Saldo: ${d.usdcBalanceError}${ANSI.reset}`; + } else if (d.usdcBalance !== null && d.usdcBalance !== undefined) { + tradingBadge += ` ${ANSI.dim}Saldo: ${ANSI.reset}${ANSI.white}$${Number(d.usdcBalance).toFixed(2)} USDC${ANSI.reset}`; + } + } const confirmOrKeys = d.confirmHint ?? d.shortcutsHint ?? ""; lines.push(`${tradingBadge} ${confirmOrKeys}${" ".repeat(Math.max(0, W - visLen(tradingBadge) - visLen(confirmOrKeys) - visLen(etStr) - 4))} ${etStr}`); diff --git a/src/index.js b/src/index.js index 72767a3a..24a6776a 100644 --- a/src/index.js +++ b/src/index.js @@ -33,7 +33,7 @@ import { } from "./display.js"; import { initTradingClient } from "./trading/client.js"; import { buyMarketOrder, sellMarketOrder } from "./trading/orders.js"; -import { getPosition, recordBuy, recordSell, resetIfMarketChanged, fetchPositionBalance, evaluateExit } from "./trading/position.js"; +import { getPosition, recordBuy, recordSell, resetIfMarketChanged, fetchPositionBalance, fetchUsdcBalance, evaluateExit } from "./trading/position.js"; function countVwapCrosses(closes, vwapSeries, lookback) { if (closes.length < lookback || vwapSeries.length < lookback) return null; @@ -212,6 +212,9 @@ async function main() { let prevSpotPrice = null; let prevCurrentPrice = null; + let usdcBalance = null; + let usdcBalanceError = null; + let usdcLastFetchMs = 0; let priceToBeatState = { slug: null, value: null, setAtMs: null }; let priceToBeatFetching = false; // guard against concurrent historical fetches @@ -348,17 +351,20 @@ async function main() { } else { const side = rec.action === "ENTER" ? rec.side : (timeAware.adjustedUp >= timeAware.adjustedDown ? "UP" : "DOWN"); const tokenId = side === "UP" ? poly.tokens.upTokenId : poly.tokens.downTokenId; - const mktPrice = side === "UP" ? marketUp : marketDown; - const priceNum = mktPrice != null ? mktPrice : 0.5; + const book = side === "UP" ? poly.orderbook.up : poly.orderbook.down; + // Usa bestAsk + margem para garantir o match; cai no mid-price se não houver book + const rawAsk = book?.bestAsk ?? (side === "UP" ? marketUp : marketDown); + const priceNum = rawAsk != null ? Math.min(rawAsk + 0.02, 0.97) : 0.5; + const entryRef = rawAsk ?? priceNum; setStatusMessage(`Comprando ${side}...`); const result = await buyMarketOrder({ client: trading.client, tokenId, amount: trading.tradeAmount, price: priceNum }); if (result.ok) { const balance = await fetchPositionBalance(trading.client, tokenId); - const shares = balance > 0 ? balance : trading.tradeAmount / priceNum; - recordBuy({ side, tokenId, shares, entryPrice: priceNum, invested: trading.tradeAmount, marketSlug: marketSlugNow, orderId: result.order?.orderID }); + const shares = balance > 0 ? balance : trading.tradeAmount / entryRef; + recordBuy({ side, tokenId, shares, entryPrice: entryRef, invested: trading.tradeAmount, marketSlug: marketSlugNow, orderId: result.order?.orderID }); const orderId = result.order?.orderID ?? result.order?.id ?? "-"; const balanceStr = balance > 0 ? `shares: ${balance.toFixed(2)}` : "saldo 0 (ordem não preenchida?)"; - setStatusMessage(`COMPROU ${side} @ ${(priceNum * 100).toFixed(1)}¢ | $${trading.tradeAmount} | ${balanceStr} | ID: ${String(orderId).slice(0, 12)}`, 8000); + setStatusMessage(`COMPROU ${side} @ ${(entryRef * 100).toFixed(1)}¢ | $${trading.tradeAmount} | ${balanceStr} | ID: ${String(orderId).slice(0, 12)}`, 8000); } else { const errMsg = `Erro na compra: ${result.error}`; setStatusMessage(errMsg, 15000); @@ -372,13 +378,16 @@ async function main() { setStatusMessage("Nenhuma posição para vender"); } else { setStatusMessage(`Vendendo ${pos.side}...`); - const sellMktPrice = pos.side === "UP" ? marketUp : marketDown; - const sellPriceNum = sellMktPrice != null ? sellMktPrice : 0.5; - const result = await sellMarketOrder({ client: trading.client, tokenId: pos.tokenId, amount: pos.shares, price: sellPriceNum }); + const sellBook = pos.side === "UP" ? poly.orderbook.up : poly.orderbook.down; + const rawBid = sellBook?.bestBid ?? (pos.side === "UP" ? marketUp : marketDown); + const sellPriceNum = rawBid != null ? Math.max(rawBid - 0.02, 0.03) : 0.5; + // Usa saldo real on-chain para evitar erro de saldo insuficiente + const actualShares = await fetchPositionBalance(trading.client, pos.tokenId); + const sharesToSell = actualShares > 0 ? actualShares : pos.shares; + const result = await sellMarketOrder({ client: trading.client, tokenId: pos.tokenId, amount: sharesToSell, price: sellPriceNum }); if (result.ok) { - const mktPrice = pos.side === "UP" ? marketUp : marketDown; - const priceNum = mktPrice != null ? mktPrice : 0.5; - const pnl = (pos.shares * priceNum) - pos.invested; + const priceNum = rawBid ?? sellPriceNum; + const pnl = (sharesToSell * priceNum) - pos.invested; const roi = (pnl / pos.invested) * 100; const sign = pnl >= 0 ? "+" : ""; setStatusMessage(`VENDEU ${pos.side} | P&L: ${sign}$${pnl.toFixed(2)}`, 8000); @@ -395,6 +404,17 @@ async function main() { } } + // --- USDC balance fetch (every 30s) --- + if (trading.tradingEnabled && Date.now() - usdcLastFetchMs > 30_000) { + usdcLastFetchMs = Date.now(); + fetchUsdcBalance(trading.balanceAddress).then((bal) => { + usdcBalance = bal; + usdcBalanceError = null; + }).catch((err) => { + usdcBalanceError = err?.message ? err.message.slice(0, 40) : "erro"; + }); + } + // --- Display data --- const spotPrice = wsPrice ?? lastPrice; const currentPrice = chainlink?.price ?? null; @@ -540,6 +560,8 @@ async function main() { tradingEnabled: trading.tradingEnabled, initError: trading.initError, tradeAmount: trading.tradeAmount, + usdcBalance, + usdcBalanceError, confirmHint, shortcutsHint, binanceSpot: `${colorPriceLine({ label: "", price: spotPrice, prevPrice: prevSpotPrice, decimals: 0, prefix: "$" })}`, diff --git a/src/index5m.js b/src/index5m.js index d2fc4b16..5b6acba0 100644 --- a/src/index5m.js +++ b/src/index5m.js @@ -35,7 +35,7 @@ import { } from "./display.js"; import { initTradingClient } from "./trading/client.js"; import { buyMarketOrder, sellMarketOrder } from "./trading/orders.js"; -import { getPosition, recordBuy, recordSell, computeROI, resetIfMarketChanged, fetchPositionBalance, evaluateExit } from "./trading/position.js"; +import { getPosition, recordBuy, recordSell, computeROI, resetIfMarketChanged, fetchPositionBalance, fetchUsdcBalance, evaluateExit } from "./trading/position.js"; applyGlobalProxyFromEnv(); @@ -209,6 +209,9 @@ async function main() { let prevSpotPrice = null; let prevCurrentPrice = null; + let usdcBalance = null; + let usdcBalanceError = null; + let usdcLastFetchMs = 0; let priceToBeatState = { slug: null, value: null, setAtMs: null }; let priceToBeatFetching = false; @@ -327,17 +330,19 @@ async function main() { } else { const side = rec.action === "ENTER" ? rec.side : (timeAware.adjustedUp >= timeAware.adjustedDown ? "UP" : "DOWN"); const tokenId = side === "UP" ? poly.tokens.upTokenId : poly.tokens.downTokenId; - const mktPrice = side === "UP" ? marketUp : marketDown; - const priceNum = mktPrice != null ? mktPrice : 0.5; + const book = side === "UP" ? poly.orderbook.up : poly.orderbook.down; + const rawAsk = book?.bestAsk ?? (side === "UP" ? marketUp : marketDown); + const priceNum = rawAsk != null ? Math.min(rawAsk + 0.02, 0.97) : 0.5; + const entryRef = rawAsk ?? priceNum; setStatusMessage(`Comprando ${side}...`); const result = await buyMarketOrder({ client: trading.client, tokenId, amount: trading.tradeAmount, price: priceNum }); if (result.ok) { const balance = await fetchPositionBalance(trading.client, tokenId); - const shares = balance > 0 ? balance : trading.tradeAmount / priceNum; - recordBuy({ side, tokenId, shares, entryPrice: priceNum, invested: trading.tradeAmount, marketSlug: marketSlugNow, orderId: result.order?.orderID }); + const shares = balance > 0 ? balance : trading.tradeAmount / entryRef; + recordBuy({ side, tokenId, shares, entryPrice: entryRef, invested: trading.tradeAmount, marketSlug: marketSlugNow, orderId: result.order?.orderID }); const orderId = result.order?.orderID ?? result.order?.id ?? "-"; const balanceStr = balance > 0 ? `shares: ${balance.toFixed(2)}` : "saldo 0 (ordem não preenchida?)"; - setStatusMessage(`COMPROU ${side} @ ${(priceNum * 100).toFixed(1)}¢ | $${trading.tradeAmount} | ${balanceStr} | ID: ${String(orderId).slice(0, 12)}`, 8000); + setStatusMessage(`COMPROU ${side} @ ${(entryRef * 100).toFixed(1)}¢ | $${trading.tradeAmount} | ${balanceStr} | ID: ${String(orderId).slice(0, 12)}`, 8000); } else { const errMsg = `Erro na compra: ${result.error}`; setStatusMessage(errMsg, 15000); @@ -351,13 +356,15 @@ async function main() { setStatusMessage("Nenhuma posição para vender"); } else { setStatusMessage(`Vendendo ${pos.side}...`); - const sellMktPrice = pos.side === "UP" ? marketUp : marketDown; - const sellPriceNum = sellMktPrice != null ? sellMktPrice : 0.5; - const result = await sellMarketOrder({ client: trading.client, tokenId: pos.tokenId, amount: pos.shares, price: sellPriceNum }); + const sellBook = pos.side === "UP" ? poly.orderbook.up : poly.orderbook.down; + const rawBid = sellBook?.bestBid ?? (pos.side === "UP" ? marketUp : marketDown); + const sellPriceNum = rawBid != null ? Math.max(rawBid - 0.02, 0.03) : 0.5; + const actualShares = await fetchPositionBalance(trading.client, pos.tokenId); + const sharesToSell = actualShares > 0 ? actualShares : pos.shares; + const result = await sellMarketOrder({ client: trading.client, tokenId: pos.tokenId, amount: sharesToSell, price: sellPriceNum }); if (result.ok) { - const mktPrice = pos.side === "UP" ? marketUp : marketDown; - const priceNum = mktPrice != null ? mktPrice : 0.5; - const pnl = (pos.shares * priceNum) - pos.invested; + const priceNum = rawBid ?? sellPriceNum; + const pnl = (sharesToSell * priceNum) - pos.invested; const sign = pnl >= 0 ? "+" : ""; setStatusMessage(`VENDEU ${pos.side} | P&L: ${sign}$${pnl.toFixed(2)}`, 8000); recordSell(); @@ -486,6 +493,17 @@ async function main() { } } + // --- USDC balance fetch (every 30s) --- + if (trading.tradingEnabled && Date.now() - usdcLastFetchMs > 30_000) { + usdcLastFetchMs = Date.now(); + fetchUsdcBalance(trading.balanceAddress).then((bal) => { + usdcBalance = bal; + usdcBalanceError = null; + }).catch((err) => { + usdcBalanceError = err?.message ? err.message.slice(0, 40) : "erro"; + }); + } + const isNextMarket = marketStartMs !== null && marketStartMs > Date.now(); const timeColor = isNextMarket ? ANSI.yellow : timeLeftMin >= 3 ? ANSI.green : timeLeftMin >= 1.5 ? ANSI.yellow : ANSI.red; const liquidity = poly.ok ? (Number(poly.market?.liquidityNum) || Number(poly.market?.liquidity) || null) : null; @@ -538,6 +556,8 @@ async function main() { tradingEnabled: trading.tradingEnabled, initError: trading.initError, tradeAmount: trading.tradeAmount, + usdcBalance, + usdcBalanceError, confirmHint, shortcutsHint, binanceSpot: `${colorPriceLine({ label: "", price: spotPrice, prevPrice: prevSpotPrice, decimals: 0, prefix: "$" })}`, diff --git a/src/trading/client.js b/src/trading/client.js index ecb01cd9..7e66f6e8 100644 --- a/src/trading/client.js +++ b/src/trading/client.js @@ -1,5 +1,5 @@ import { ClobClient, SignatureType } from "@polymarket/clob-client"; -import { Wallet } from "ethers"; +import { Wallet, ethers } from "ethers"; import fs from "node:fs"; let _cached = null; @@ -29,17 +29,48 @@ export async function initTradingClient(config) { _signTypedData: (domain, types, value) => _wallet.signTypedData(domain, types, value), getAddress: () => Promise.resolve(_wallet.address), }); - const sigType = signatureType === 1 + let sigType = signatureType === 1 ? SignatureType.POLY_PROXY : signatureType === 2 ? SignatureType.POLY_GNOSIS_SAFE : SignatureType.EOA; + // For EOA, funder should be undefined (not the signer address) so the library // uses signer address as maker directly. const funderAddr = sigType === SignatureType.EOA ? undefined : (funder || undefined); + // Auto-detect: se funder é um contrato GnosisSafe mas o tipo está como POLY_PROXY, + // corrige para POLY_GNOSIS_SAFE automaticamente. + if (sigType === SignatureType.POLY_PROXY && funderAddr) { + try { + const provider = new ethers.JsonRpcProvider( + "https://polygon-bor-rpc.publicnode.com", + ethers.Network.from(137), + { staticNetwork: ethers.Network.from(137) } + ); + const code = await provider.getCode(funderAddr); + provider.destroy(); + if (code && code !== "0x" && code.length > 10) { + // É um contrato — verifica se é GnosisSafe + const gsSafe = new ethers.Contract(funderAddr, + ["function isOwner(address) view returns (bool)"], + new ethers.JsonRpcProvider("https://polygon-bor-rpc.publicnode.com", ethers.Network.from(137), { staticNetwork: ethers.Network.from(137) }) + ); + try { + const isOwner = await gsSafe.isOwner(_wallet.address); + if (isOwner) { + sigType = SignatureType.POLY_GNOSIS_SAFE; + logTrading(`Auto-detectado: funder é GnosisSafe, usando POLY_GNOSIS_SAFE. Defina POLYMARKET_SIGNATURE_TYPE=2 para evitar esta detecção.`); + } + } catch { /* não é GnosisSafe */ } + const p2 = gsSafe.runner?.provider; + try { p2?.destroy?.(); } catch { /* ignore */ } + } + } catch { /* ignora erro de detecção */ } + } + const sigTypeName = sigType === SignatureType.POLY_PROXY ? "POLY_PROXY" : sigType === SignatureType.POLY_GNOSIS_SAFE ? "GNOSIS_SAFE" : "EOA"; logTrading(`EOA=${_wallet.address} funder=${funderAddr ?? "(none)"} sigType=${sigTypeName}(${sigType})`); @@ -65,7 +96,9 @@ export async function initTradingClient(config) { funderAddr ); - _cached = { client, tradingEnabled: true, tradeAmount }; + // balanceAddress: onde está o USDC — o funder (proxy) ou o EOA + const balanceAddress = funderAddr ?? _wallet.address; + _cached = { client, tradingEnabled: true, tradeAmount, balanceAddress }; return _cached; } diff --git a/src/trading/orders.js b/src/trading/orders.js index 01fc0926..3aa706b8 100644 --- a/src/trading/orders.js +++ b/src/trading/orders.js @@ -1,4 +1,4 @@ -import { Side } from "@polymarket/clob-client"; +import { Side, OrderType } from "@polymarket/clob-client"; import fs from "node:fs"; function logOrder(action, data) { @@ -20,13 +20,12 @@ export async function buyMarketOrder({ client, tokenId, amount, price }) { userOrder.price = price; } logOrder("BUY_REQ", userOrder); - const order = await client.createAndPostMarketOrder(userOrder); + const order = await client.createAndPostMarketOrder(userOrder, undefined, OrderType.FAK); logOrder("BUY_RES", order); - // Check if the order was actually accepted/matched - const success = order && !order.error && !order.errorMsg && order.success !== false && order.status !== "DEAD"; - if (!success) { - const reason = order?.error || order?.errorMsg || order?.status || "ordem não preenchida (FOK rejeitada)"; + // FAK: aceita preenchimento total ou parcial; rejeita só se houver erro explícito + if (order?.error || order?.errorMsg) { + const reason = order.error || order.errorMsg; return { ok: false, error: reason, order }; } return { ok: true, order }; @@ -47,12 +46,11 @@ export async function sellMarketOrder({ client, tokenId, amount, price }) { userOrder.price = price; } logOrder("SELL_REQ", userOrder); - const order = await client.createAndPostMarketOrder(userOrder); + const order = await client.createAndPostMarketOrder(userOrder, undefined, OrderType.FAK); logOrder("SELL_RES", order); - const success = order && !order.error && !order.errorMsg && order.success !== false && order.status !== "DEAD"; - if (!success) { - const reason = order?.error || order?.errorMsg || order?.status || "ordem não preenchida (FOK rejeitada)"; + if (order?.error || order?.errorMsg) { + const reason = order.error || order.errorMsg; return { ok: false, error: reason, order }; } return { ok: true, order }; diff --git a/src/trading/position.js b/src/trading/position.js index ae68d139..154df70e 100644 --- a/src/trading/position.js +++ b/src/trading/position.js @@ -1,4 +1,40 @@ import { AssetType } from "@polymarket/clob-client"; +import { ethers } from "ethers"; +import { CONFIG } from "../config.js"; + +const USDC_E = "0x2791Bca1f2de4661ED88A30C99A7a9449Aa84174"; // USDC.e Polygon +const ERC20_ABI = ["function balanceOf(address) view returns (uint256)"]; +const POLYGON_NETWORK = ethers.Network.from(137); + +let _cachedProvider = null; + +async function getPolygonProvider() { + if (_cachedProvider) return _cachedProvider; + + const rpcs = [ + ...(CONFIG.chainlink.polygonRpcUrls ?? []), + CONFIG.chainlink.polygonRpcUrl, + "https://polygon-bor-rpc.publicnode.com", + "https://rpc.ankr.com/polygon", + "https://polygon.llamarpc.com", + ].map(s => String(s || "").trim()).filter(Boolean); + + for (const rpc of rpcs) { + // staticNetwork=true evita auto-detecção (e os logs de retry) no ethers v6 + const p = new ethers.JsonRpcProvider(rpc, POLYGON_NETWORK, { staticNetwork: POLYGON_NETWORK }); + try { + await Promise.race([ + p.getBlockNumber(), + new Promise((_, r) => setTimeout(() => r(new Error("timeout")), 3000)), + ]); + _cachedProvider = p; + return p; + } catch { + p.destroy(); + } + } + throw new Error("Nenhum RPC Polygon disponível"); +} let position = { active: false, @@ -73,27 +109,33 @@ export function evaluateExit({ position, modelUp, modelDown, currentMarketPrice, const pnlUsdc = currentValue - position.invested; const roiPct = (pnlUsdc / position.invested) * 100; - // 1. Take profit - if (roiPct >= takeProfitPct) { - return { shouldSell: true, reason: "TAKE_PROFIT", urgency: "MEDIUM", roiPct }; + const oppositeProb = (modelUp != null && modelDown != null) + ? (position.side === "UP" ? modelDown : modelUp) + : null; + const modelConfirmsReversal = oppositeProb != null && oppositeProb >= signalFlipMinProb; + + // 1. Take profit — só recomenda se o modelo também aponta reversão + if (roiPct >= takeProfitPct && modelConfirmsReversal) { + const urgency = oppositeProb >= 0.65 ? "HIGH" : "MEDIUM"; + return { shouldSell: true, reason: "TAKE_PROFIT", urgency, roiPct }; } - // 2. Stop loss - if (roiPct <= -stopLossPct) { - return { shouldSell: true, reason: "STOP_LOSS", urgency: "HIGH", roiPct }; + // 2. Stop loss — só recomenda se o modelo também aponta reversão + if (roiPct <= -stopLossPct && modelConfirmsReversal) { + const urgency = oppositeProb >= 0.65 ? "HIGH" : "MEDIUM"; + return { shouldSell: true, reason: "STOP_LOSS", urgency, roiPct }; } - // 3. Sinal invertido — modelo agora favorece o lado oposto com confiança - if (modelUp != null && modelDown != null) { - const oppositeProb = position.side === "UP" ? modelDown : modelUp; - if (oppositeProb >= signalFlipMinProb) { - const urgency = oppositeProb >= 0.65 ? "HIGH" : "MEDIUM"; - return { shouldSell: true, reason: "SIGNAL_FLIPPED", urgency, roiPct }; - } + // 3. Sinal invertido com força suficiente, independente do ROI + if (modelConfirmsReversal) { + const urgency = oppositeProb >= 0.65 ? "HIGH" : "MEDIUM"; + return { shouldSell: true, reason: "SIGNAL_FLIPPED", urgency, roiPct }; } - // 4. Pouco tempo + perdendo — reduz exposição - if (timeLeftMin != null && timeLeftMin < 1.5 && roiPct < -5) { + // 4. Pouco tempo + perdendo — só aplica se a entrada foi cara (>= 50¢) + // Posições baratas já têm o risco precificado; vale segurar até a resolução + const entryWasCheap = position.entryPrice < 0.50; + if (timeLeftMin != null && timeLeftMin < 1.5 && roiPct < -5 && !entryWasCheap) { return { shouldSell: true, reason: "TIME_DECAY", urgency: "MEDIUM", roiPct }; } @@ -111,3 +153,10 @@ export async function fetchPositionBalance(client, tokenId) { return 0; } } + +export async function fetchUsdcBalance(funderAddress) { + const provider = await getPolygonProvider(); + const usdc = new ethers.Contract(USDC_E, ERC20_ABI, provider); + const raw = await usdc.balanceOf(funderAddress); + return Number(raw) / 1e6; // USDC.e tem 6 decimais +} From 4a1cb5246530751ca6e55c5fe5085cda3ff10b7e Mon Sep 17 00:00:00 2001 From: Jean Carraro Date: Mon, 6 Apr 2026 16:14:13 -0300 Subject: [PATCH 07/49] Simplify: code cleanup and quality improvements - client.js: reuse single provider for GnosisSafe detection (was creating two) - position.js: add promise lock to getPolygonProvider to prevent concurrent init race - index.js/index5m.js: replace Math.min/max with existing clamp() utility - display.js: simplify null/undefined check to != null - orders.js: remove narrating comment Co-Authored-By: Claude Sonnet 4.6 --- src/display.js | 2 +- src/trading/client.js | 7 ++----- src/trading/orders.js | 1 - src/trading/position.js | 8 ++++++++ 4 files changed, 11 insertions(+), 7 deletions(-) diff --git a/src/display.js b/src/display.js index 06cc0146..cd6b69bd 100644 --- a/src/display.js +++ b/src/display.js @@ -295,7 +295,7 @@ export function buildScreen(d) { if (d.tradingEnabled) { if (d.usdcBalanceError) { tradingBadge += ` ${ANSI.red}Saldo: ${d.usdcBalanceError}${ANSI.reset}`; - } else if (d.usdcBalance !== null && d.usdcBalance !== undefined) { + } else if (d.usdcBalance != null) { tradingBadge += ` ${ANSI.dim}Saldo: ${ANSI.reset}${ANSI.white}$${Number(d.usdcBalance).toFixed(2)} USDC${ANSI.reset}`; } } diff --git a/src/trading/client.js b/src/trading/client.js index 7e66f6e8..e529e3c0 100644 --- a/src/trading/client.js +++ b/src/trading/client.js @@ -51,12 +51,10 @@ export async function initTradingClient(config) { { staticNetwork: ethers.Network.from(137) } ); const code = await provider.getCode(funderAddr); - provider.destroy(); if (code && code !== "0x" && code.length > 10) { - // É um contrato — verifica se é GnosisSafe const gsSafe = new ethers.Contract(funderAddr, ["function isOwner(address) view returns (bool)"], - new ethers.JsonRpcProvider("https://polygon-bor-rpc.publicnode.com", ethers.Network.from(137), { staticNetwork: ethers.Network.from(137) }) + provider ); try { const isOwner = await gsSafe.isOwner(_wallet.address); @@ -65,9 +63,8 @@ export async function initTradingClient(config) { logTrading(`Auto-detectado: funder é GnosisSafe, usando POLY_GNOSIS_SAFE. Defina POLYMARKET_SIGNATURE_TYPE=2 para evitar esta detecção.`); } } catch { /* não é GnosisSafe */ } - const p2 = gsSafe.runner?.provider; - try { p2?.destroy?.(); } catch { /* ignore */ } } + provider.destroy(); } catch { /* ignora erro de detecção */ } } diff --git a/src/trading/orders.js b/src/trading/orders.js index 3aa706b8..70983dbb 100644 --- a/src/trading/orders.js +++ b/src/trading/orders.js @@ -23,7 +23,6 @@ export async function buyMarketOrder({ client, tokenId, amount, price }) { const order = await client.createAndPostMarketOrder(userOrder, undefined, OrderType.FAK); logOrder("BUY_RES", order); - // FAK: aceita preenchimento total ou parcial; rejeita só se houver erro explícito if (order?.error || order?.errorMsg) { const reason = order.error || order.errorMsg; return { ok: false, error: reason, order }; diff --git a/src/trading/position.js b/src/trading/position.js index 154df70e..dfdeb446 100644 --- a/src/trading/position.js +++ b/src/trading/position.js @@ -7,9 +7,17 @@ const ERC20_ABI = ["function balanceOf(address) view returns (uint256)"]; const POLYGON_NETWORK = ethers.Network.from(137); let _cachedProvider = null; +let _providerInitPromise = null; async function getPolygonProvider() { if (_cachedProvider) return _cachedProvider; + if (_providerInitPromise) return _providerInitPromise; + _providerInitPromise = _initProvider().finally(() => { _providerInitPromise = null; }); + return _providerInitPromise; +} + +async function _initProvider() { + if (_cachedProvider) return _cachedProvider; const rpcs = [ ...(CONFIG.chainlink.polygonRpcUrls ?? []), From 14bd9e541a92eb555e18ae0861b01a3b2e0758f0 Mon Sep 17 00:00:00 2001 From: Jean Carraro Date: Mon, 6 Apr 2026 16:14:27 -0300 Subject: [PATCH 08/49] Modularize trading layer for multi-bot reuse Extract shared concerns from index.js/index5m.js into reusable modules: - data/polymarket.js: add createMarketResolver() + fetchPolymarketSnapshot() so any future bot can resolve and snapshot a Polymarket market in one call - trading/keyboard.js: stdin setup, action queue, confirm-hint builder - trading/executor.js: buy/sell execution (price slippage, balance fetch, logging) - trading/priceLatch.js: Chainlink reference-price latch at market open - trading/tracker.js: per-market win/loss tracking; returns settled outcome for the caller to write in its own CSV format index.js and index5m.js are now thin orchestrators: they own only the mode-specific indicator pipeline, signal scoring, and display rendering. All trading state and Polymarket data access are delegated to the modules above. Co-Authored-By: Claude Sonnet 4.6 --- CLAUDE.md | 14 +- src/data/polymarket.js | 109 +++++++ src/index.js | 584 +++++++++--------------------------- src/index5m.js | 605 +++++++++++--------------------------- src/trading/executor.js | 91 ++++++ src/trading/keyboard.js | 70 +++++ src/trading/priceLatch.js | 74 +++++ src/trading/tracker.js | 86 ++++++ 8 files changed, 748 insertions(+), 885 deletions(-) create mode 100644 src/trading/executor.js create mode 100644 src/trading/keyboard.js create mode 100644 src/trading/priceLatch.js create mode 100644 src/trading/tracker.js diff --git a/CLAUDE.md b/CLAUDE.md index 14495507..76015f67 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -19,7 +19,7 @@ This is a single-process real-time console assistant for Polymarket BTC 15-minut ### Data layer (`src/data/`) - **binance.js / binanceWs.js** — Binance REST (klines, last price) and WebSocket trade stream for live spot price. -- **polymarket.js** — Gamma API + CLOB API: fetches the active 15m market, outcome token IDs, CLOB prices, and order books. +- **polymarket.js** — Gamma API + CLOB API. Reusable functions: `createMarketResolver(polyConfig, pollIntervalMs)` returns a cached async resolver; `fetchPolymarketSnapshot(resolveMarket, polyConfig)` returns `{ ok, market, tokens, prices, orderbook }` — the canonical way for any app to get live market state. - **polymarketLiveWs.js** — Polymarket live WebSocket (`wss://ws-live-data.polymarket.com`); primary source for the Chainlink BTC/USD price shown on Polymarket UI. - **chainlink.js / chainlinkWs.js** — Fallback: reads Chainlink BTC/USD aggregator on Polygon via HTTP RPC or WSS RPC using ethers v6. @@ -50,8 +50,10 @@ Pure functions operating on arrays of OHLCV candles (Binance 1m klines): ### Main loops -- **index.js** (15m) — Starts three WebSocket streams (Binance trades, Polymarket live, Chainlink), loops fetching klines + Polymarket snapshot, computes TA indicators, renders terminal screen, logs to `./logs/signals.csv`. -- **index5m.js** (5m) — Same structure but uses `binanceWsOfi.js` (order flow stream), 5m-specific indicators/engines, shorter VWAP window, logs to `./logs/signals_5m.csv`. +Thin orchestrators — each one initializes mode-specific streams, computes mode-specific indicators/signals, then delegates all shared concerns to the trading modules below. + +- **index.js** (15m) — Starts Binance trade stream + Polymarket live WS + Chainlink WS; indicator pipeline: VWAP, RSI, MACD, Heiken Ashi, regime; logs to `./logs/signals.csv`. +- **index5m.js** (5m) — Starts Binance OFI stream + Polymarket live WS + Chainlink WS; indicator pipeline: short VWAP, RSI(5), EMA cross, momentum, order flow; logs to `./logs/signals_5m.csv`. ### Configuration (`src/config.js`, `src/config5m.js`) @@ -61,9 +63,15 @@ All tunable parameters (poll interval, TA periods, Polymarket series IDs, RPC UR Optional live-trading integration using `@polymarket/clob-client` SDK. Enabled when `POLYMARKET_PRIVATE_KEY` is set; otherwise the app runs in read-only mode. +All modules are reusable by future bots targeting other markets or strategies. + - **client.js** — Initializes `ClobClient` with L1 (EIP-712) + L2 (HMAC) auth. Derives API credentials on first run via `createOrDeriveApiKey()`. Caches the client singleton. Auto-detects `POLY_GNOSIS_SAFE` when `POLYMARKET_SIGNATURE_TYPE=1` but the funder address is a GnosisSafe contract. Exposes `balanceAddress` (funder or EOA) for USDC balance queries. - **orders.js** — `buyMarketOrder()` and `sellMarketOrder()` wrappers around `client.createAndPostMarketOrder()` using `OrderType.FAK` (Fill and Kill — partial fills accepted). Buy price = `bestAsk + 0.02`; sell price = `bestBid - 0.02`, both clamped to valid range. Returns `{ ok, order }` or `{ ok: false, error }`. - **position.js** — In-memory position state: `recordBuy()`, `recordSell()`, `getPosition()`, `computeROI()`, `resetIfMarketChanged()`. `fetchPositionBalance()` syncs shares from chain via `getBalanceAllowance()` (used before selling to get actual on-chain balance). `fetchUsdcBalance(address)` reads USDC.e balance directly from Polygon blockchain (not the CLOB API, which only tracks deposited collateral). `evaluateExit()` recommends exits: TP and SL only trigger when the model also confirms reversal (`oppositeProb >= signalFlipMinProb`); TIME_DECAY only applies when entry price ≥ 0.50 (cheap entries are held to resolution). +- **keyboard.js** — `setupKeyboard({ tradingEnabled })` sets up stdin raw mode and returns `{ actionQueue, getConfirmHint(ctx), stdinError }`. The action queue is drained each tick by the executor. `getConfirmHint` builds the [Y]/[N] confirmation line shown on the display. +- **executor.js** — `processActionQueue(queue, ctx)` drains the keyboard action queue and executes buy/sell orders: selects side, picks bestAsk/bestBid with slippage, calls `buyMarketOrder`/`sellMarketOrder`, fetches on-chain balance, calls `recordBuy`/`recordSell`, logs errors to `./logs/trade_errors.log`. Calls optional `onSold` callback after a successful sell. +- **priceLatch.js** — `createPriceLatch()` returns `{ update(ctx) }`. Manages the state machine that latches the Chainlink BTC/USD reference price at market open (used as the "price to beat" on the display). Reads from the market object first, then fetches historical Chainlink if the app started late (>30s after open), otherwise latches the live price. +- **tracker.js** — `createTradeTracker()` returns `{ update(ctx), getStats(), getRecentOutcomes() }`. Tracks the first signal seen per market; when the market slug changes (settlement), computes win/loss and P&L based on the final Chainlink price vs the latched reference. Returns a `settled` object from `update()` so the caller writes it to the CSV in its own format. Both main loops listen for keypresses when trading is enabled: **[B]** buy the recommended side, **[S]** sell 100% of position, **[Q]** quit. Actions are queued and processed inside the main loop where market data is available. diff --git a/src/data/polymarket.js b/src/data/polymarket.js index d104afe9..1f9df2e1 100644 --- a/src/data/polymarket.js +++ b/src/data/polymarket.js @@ -1,5 +1,114 @@ import { CONFIG } from "../config.js"; +// --------------------------------------------------------------------------- +// Market resolver + snapshot (reusable across apps) +// --------------------------------------------------------------------------- + +/** + * Returns a stateful async function that resolves the current active market, + * caching the result for one poll interval to avoid hammering the API. + * + * @param {object} polyConfig - CONFIG.polymarket (or equivalent) + * @param {number} pollIntervalMs + */ +export function createMarketResolver(polyConfig, pollIntervalMs) { + let cache = { market: null, fetchedAtMs: 0 }; + return async function resolveMarket() { + if (polyConfig.marketSlug) return fetchMarketBySlug(polyConfig.marketSlug); + if (!polyConfig.autoSelectLatest) return null; + const now = Date.now(); + if (cache.market && now - cache.fetchedAtMs < pollIntervalMs) return cache.market; + const events = await fetchLiveEventsBySeriesId({ seriesId: polyConfig.seriesId, limit: 25 }); + const markets = flattenEventMarkets(events); + cache.market = pickLatestLiveMarket(markets); + cache.fetchedAtMs = now; + return cache.market; + }; +} + +/** + * Fetches a full market snapshot: resolved market, token IDs, CLOB prices, + * and orderbook summaries for both UP and DOWN outcomes. + * + * @param {Function} resolveMarket - from createMarketResolver() + * @param {object} polyConfig - needs upOutcomeLabel / downOutcomeLabel + */ +export async function fetchPolymarketSnapshot(resolveMarket, polyConfig) { + const market = await resolveMarket(); + if (!market) return { ok: false, reason: "market_not_found" }; + + const outcomes = Array.isArray(market.outcomes) + ? market.outcomes + : (typeof market.outcomes === "string" ? JSON.parse(market.outcomes) : []); + const outcomePrices = Array.isArray(market.outcomePrices) + ? market.outcomePrices + : (typeof market.outcomePrices === "string" ? JSON.parse(market.outcomePrices) : []); + const clobTokenIds = Array.isArray(market.clobTokenIds) + ? market.clobTokenIds + : (typeof market.clobTokenIds === "string" ? JSON.parse(market.clobTokenIds) : []); + + const upLabel = (polyConfig.upOutcomeLabel ?? "Up").toLowerCase(); + const downLabel = (polyConfig.downOutcomeLabel ?? "Down").toLowerCase(); + + let upTokenId = null; + let downTokenId = null; + for (let i = 0; i < outcomes.length; i += 1) { + const label = String(outcomes[i]).toLowerCase(); + const tokenId = clobTokenIds[i] ? String(clobTokenIds[i]) : null; + if (!tokenId) continue; + if (label === upLabel) upTokenId = tokenId; + if (label === downLabel) downTokenId = tokenId; + } + + const upIndex = outcomes.findIndex((x) => String(x).toLowerCase() === upLabel); + const downIndex = outcomes.findIndex((x) => String(x).toLowerCase() === downLabel); + const gammaYes = upIndex >= 0 ? Number(outcomePrices[upIndex]) : null; + const gammaNo = downIndex >= 0 ? Number(outcomePrices[downIndex]) : null; + + if (!upTokenId || !downTokenId) { + return { ok: false, reason: "missing_token_ids", market, outcomes, clobTokenIds, outcomePrices }; + } + + const emptyBook = { bestBid: null, bestAsk: null, spread: null, bidLiquidity: null, askLiquidity: null }; + let upBuy = null, downBuy = null; + let upBookSummary = { ...emptyBook }, downBookSummary = { ...emptyBook }; + + try { + const [yesBuy, noBuy, upBook, downBook] = await Promise.all([ + fetchClobPrice({ tokenId: upTokenId, side: "buy" }), + fetchClobPrice({ tokenId: downTokenId, side: "buy" }), + fetchOrderBook({ tokenId: upTokenId }), + fetchOrderBook({ tokenId: downTokenId }), + ]); + upBuy = yesBuy; + downBuy = noBuy; + upBookSummary = summarizeOrderBook(upBook); + downBookSummary = summarizeOrderBook(downBook); + } catch { + upBookSummary = { + bestBid: Number(market.bestBid) || null, + bestAsk: Number(market.bestAsk) || null, + spread: Number(market.spread) || null, + bidLiquidity: null, askLiquidity: null, + }; + downBookSummary = { + bestBid: null, bestAsk: null, + spread: Number(market.spread) || null, + bidLiquidity: null, askLiquidity: null, + }; + } + + return { + ok: true, + market, + tokens: { upTokenId, downTokenId }, + prices: { up: upBuy ?? gammaYes, down: downBuy ?? gammaNo }, + orderbook: { up: upBookSummary, down: downBookSummary }, + }; +} + +// --------------------------------------------------------------------------- + function toNumber(x) { const n = Number(x); return Number.isFinite(n) ? n : null; diff --git a/src/index.js b/src/index.js index 24a6776a..507d7f88 100644 --- a/src/index.js +++ b/src/index.js @@ -1,19 +1,11 @@ import { CONFIG } from "./config.js"; import { fetchKlines, fetchLastPrice } from "./data/binance.js"; -import { fetchChainlinkBtcUsd, fetchChainlinkPriceAtMs } from "./data/chainlink.js"; +import { fetchChainlinkBtcUsd } from "./data/chainlink.js"; import { startChainlinkPriceStream } from "./data/chainlinkWs.js"; import { startPolymarketChainlinkPriceStream } from "./data/polymarketLiveWs.js"; -import { - fetchMarketBySlug, - fetchLiveEventsBySeriesId, - flattenEventMarkets, - pickLatestLiveMarket, - fetchClobPrice, - fetchOrderBook, - summarizeOrderBook -} from "./data/polymarket.js"; -import { computeSessionVwap, computeVwapSeries } from "./indicators/vwap.js"; -import { computeRsi, sma, slopeLast } from "./indicators/rsi.js"; +import { createMarketResolver, fetchPolymarketSnapshot } from "./data/polymarket.js"; +import { computeVwapSeries } from "./indicators/vwap.js"; +import { computeRsi, slopeLast } from "./indicators/rsi.js"; import { computeMacd } from "./indicators/macd.js"; import { computeHeikenAshi, countConsecutive } from "./indicators/heikenAshi.js"; import { detectRegime } from "./engines/regime.js"; @@ -26,156 +18,45 @@ import path from "node:path"; import { applyGlobalProxyFromEnv } from "./net/proxy.js"; import { ANSI, renderScreen, buildScreen, - colorPriceLine, formatSignedDelta, formatNumberDisplay, - colorByNarrative, formatNarrativeValue, narrativeFromSign, - narrativeFromSlope, formatProbPct, fmtTimeLeft, - safeFileSlug, priceToBeatFromPolymarketMarket, setStatusMessage + colorPriceLine, formatSignedDelta, + colorByNarrative, narrativeFromSign, + narrativeFromSlope, formatProbPct, + safeFileSlug, setStatusMessage } from "./display.js"; import { initTradingClient } from "./trading/client.js"; -import { buyMarketOrder, sellMarketOrder } from "./trading/orders.js"; -import { getPosition, recordBuy, recordSell, resetIfMarketChanged, fetchPositionBalance, fetchUsdcBalance, evaluateExit } from "./trading/position.js"; +import { fetchUsdcBalance, evaluateExit, resetIfMarketChanged, getPosition } from "./trading/position.js"; +import { setupKeyboard } from "./trading/keyboard.js"; +import { processActionQueue } from "./trading/executor.js"; +import { createPriceLatch } from "./trading/priceLatch.js"; +import { createTradeTracker } from "./trading/tracker.js"; + +applyGlobalProxyFromEnv(); function countVwapCrosses(closes, vwapSeries, lookback) { if (closes.length < lookback || vwapSeries.length < lookback) return null; let crosses = 0; for (let i = closes.length - lookback + 1; i < closes.length; i += 1) { const prev = closes[i - 1] - vwapSeries[i - 1]; - const cur = closes[i] - vwapSeries[i]; + const cur = closes[i] - vwapSeries[i]; if (prev === 0) continue; if ((prev > 0 && cur < 0) || (prev < 0 && cur > 0)) crosses += 1; } return crosses; } -applyGlobalProxyFromEnv(); - -const dumpedMarkets = new Set(); - -const marketCache = { - market: null, - fetchedAtMs: 0 -}; - -async function resolveCurrentBtc15mMarket() { - if (CONFIG.polymarket.marketSlug) { - return await fetchMarketBySlug(CONFIG.polymarket.marketSlug); - } - - if (!CONFIG.polymarket.autoSelectLatest) return null; - - const now = Date.now(); - if (marketCache.market && now - marketCache.fetchedAtMs < CONFIG.pollIntervalMs) { - return marketCache.market; - } - - const events = await fetchLiveEventsBySeriesId({ seriesId: CONFIG.polymarket.seriesId, limit: 25 }); - const markets = flattenEventMarkets(events); - const picked = pickLatestLiveMarket(markets); - - marketCache.market = picked; - marketCache.fetchedAtMs = now; - return picked; -} - -async function fetchPolymarketSnapshot() { - const market = await resolveCurrentBtc15mMarket(); - - if (!market) return { ok: false, reason: "market_not_found" }; - - const outcomes = Array.isArray(market.outcomes) ? market.outcomes : (typeof market.outcomes === "string" ? JSON.parse(market.outcomes) : []); - const outcomePrices = Array.isArray(market.outcomePrices) - ? market.outcomePrices - : (typeof market.outcomePrices === "string" ? JSON.parse(market.outcomePrices) : []); - - const clobTokenIds = Array.isArray(market.clobTokenIds) - ? market.clobTokenIds - : (typeof market.clobTokenIds === "string" ? JSON.parse(market.clobTokenIds) : []); - - let upTokenId = null; - let downTokenId = null; - for (let i = 0; i < outcomes.length; i += 1) { - const label = String(outcomes[i]); - const tokenId = clobTokenIds[i] ? String(clobTokenIds[i]) : null; - if (!tokenId) continue; - - if (label.toLowerCase() === CONFIG.polymarket.upOutcomeLabel.toLowerCase()) upTokenId = tokenId; - if (label.toLowerCase() === CONFIG.polymarket.downOutcomeLabel.toLowerCase()) downTokenId = tokenId; - } - - const upIndex = outcomes.findIndex((x) => String(x).toLowerCase() === CONFIG.polymarket.upOutcomeLabel.toLowerCase()); - const downIndex = outcomes.findIndex((x) => String(x).toLowerCase() === CONFIG.polymarket.downOutcomeLabel.toLowerCase()); - - const gammaYes = upIndex >= 0 ? Number(outcomePrices[upIndex]) : null; - const gammaNo = downIndex >= 0 ? Number(outcomePrices[downIndex]) : null; - - if (!upTokenId || !downTokenId) { - return { - ok: false, - reason: "missing_token_ids", - market, - outcomes, - clobTokenIds, - outcomePrices - }; - } - - let upBuy = null; - let downBuy = null; - let upBookSummary = { bestBid: null, bestAsk: null, spread: null, bidLiquidity: null, askLiquidity: null }; - let downBookSummary = { bestBid: null, bestAsk: null, spread: null, bidLiquidity: null, askLiquidity: null }; - - try { - const [yesBuy, noBuy, upBook, downBook] = await Promise.all([ - fetchClobPrice({ tokenId: upTokenId, side: "buy" }), - fetchClobPrice({ tokenId: downTokenId, side: "buy" }), - fetchOrderBook({ tokenId: upTokenId }), - fetchOrderBook({ tokenId: downTokenId }) - ]); - - upBuy = yesBuy; - downBuy = noBuy; - upBookSummary = summarizeOrderBook(upBook); - downBookSummary = summarizeOrderBook(downBook); - } catch { - upBuy = null; - downBuy = null; - upBookSummary = { - bestBid: Number(market.bestBid) || null, - bestAsk: Number(market.bestAsk) || null, - spread: Number(market.spread) || null, - bidLiquidity: null, - askLiquidity: null - }; - downBookSummary = { - bestBid: null, - bestAsk: null, - spread: Number(market.spread) || null, - bidLiquidity: null, - askLiquidity: null - }; - } - - return { - ok: true, - market, - tokens: { upTokenId, downTokenId }, - prices: { - up: upBuy ?? gammaYes, - down: downBuy ?? gammaNo - }, - orderbook: { - up: upBookSummary, - down: downBookSummary - } - }; -} +const CSV_PATH = "./logs/signals.csv"; +const CSV_HEADER = [ + "timestamp", "entry_minute", "time_left_min", + "regime", "signal", + "model_up", "model_down", "mkt_up", "mkt_down", + "edge_up", "edge_down", "recommendation", "outcome", "pnl", +]; async function main() { - const binanceStream = startBinanceTradeStream({ symbol: CONFIG.symbol }); + const binanceStream = startBinanceTradeStream({ symbol: CONFIG.symbol }); const polymarketLiveStream = startPolymarketChainlinkPriceStream({}); - const chainlinkStream = startChainlinkPriceStream({}); + const chainlinkStream = startChainlinkPriceStream({}); - // --- Trading setup --- let trading = { client: null, tradingEnabled: false, tradeAmount: 0, initError: null }; try { trading = await initTradingClient(CONFIG); @@ -183,74 +64,33 @@ async function main() { trading.initError = err?.message ?? String(err); } - const actionQueue = []; - let pendingAction = null; // { type: "buy" | "sell" } waiting Y/N confirmation - let lastPoly = null; - let lastRec = null; + const resolveMarket = createMarketResolver(CONFIG.polymarket, CONFIG.pollIntervalMs); + const keyboard = setupKeyboard({ tradingEnabled: trading.tradingEnabled }); + const priceLatch = createPriceLatch(); + const tracker = createTradeTracker(); - let stdinError = null; - try { - if (!process.stdin.isTTY) throw new Error("stdin não é TTY — rode com: node src/index.js"); - process.stdin.setRawMode(true); - process.stdin.resume(); - process.stdin.on("data", (key) => { - const ch = key.toString().toLowerCase(); - if (trading.tradingEnabled) { - if (pendingAction !== null) { - if (ch === "y") { actionQueue.push({ ...pendingAction }); pendingAction = null; } - else if (ch === "n" || key[0] === 0x1b) { pendingAction = null; } - } else { - if (ch === "b") pendingAction = { type: "buy" }; - else if (ch === "s") pendingAction = { type: "sell" }; - } - } - if (ch === "q" || key[0] === 0x03) process.exit(0); - }); - } catch (err) { - stdinError = err?.message ?? String(err); - } + const dumpedMarkets = new Set(); + let closedTrades = []; // { side, entryPrice, exitPrice, pnl, roi, ts }[] + + const onSold = ({ side, entryPrice, exitPrice, pnl, roi }) => { + closedTrades.unshift({ side, entryPrice, exitPrice, pnl, roi, ts: Date.now() }); + if (closedTrades.length > 10) closedTrades.pop(); + }; - let prevSpotPrice = null; + let prevSpotPrice = null; let prevCurrentPrice = null; - let usdcBalance = null; + let usdcBalance = null; let usdcBalanceError = null; - let usdcLastFetchMs = 0; - let priceToBeatState = { slug: null, value: null, setAtMs: null }; - let priceToBeatFetching = false; // guard against concurrent historical fetches - - // Trade outcome tracking (per-market) - let tradeState = { slug: null, side: null, entryMarketPrice: null, priceToBeat: null, lastChainlinkPrice: null, hasSignal: false }; - let runningStats = { wins: 0, losses: 0, totalPnl: 0 }; - let recentOutcomes = []; // { slug, side, won, pnl, ts }[] - let closedTrades = []; // { side, entryPrice, exitPrice, pnl, roi, ts }[] - - const header = [ - "timestamp", - "entry_minute", - "time_left_min", - "regime", - "signal", - "model_up", - "model_down", - "mkt_up", - "mkt_down", - "edge_up", - "edge_down", - "recommendation", - "outcome", - "pnl" - ]; + let usdcLastFetchMs = 0; while (true) { const timing = getCandleWindowTiming(CONFIG.candleWindowMinutes); - const wsTick = binanceStream.getLast(); - const wsPrice = wsTick?.price ?? null; - + const wsTick = binanceStream.getLast(); + const wsPrice = wsTick?.price ?? null; const polymarketWsTick = polymarketLiveStream.getLast(); const polymarketWsPrice = polymarketWsTick?.price ?? null; - - const chainlinkWsTick = chainlinkStream.getLast(); + const chainlinkWsTick = chainlinkStream.getLast(); const chainlinkWsPrice = chainlinkWsTick?.price ?? null; try { @@ -265,285 +105,147 @@ async function main() { fetchKlines({ interval: "5m", limit: 200 }), fetchLastPrice(), chainlinkPromise, - fetchPolymarketSnapshot() + fetchPolymarketSnapshot(resolveMarket, CONFIG.polymarket), ]); - const settlementMs = poly.ok && poly.market?.endDate ? new Date(poly.market.endDate).getTime() : null; + const settlementMs = poly.ok && poly.market?.endDate ? new Date(poly.market.endDate).getTime() : null; const settlementLeftMin = settlementMs ? (settlementMs - Date.now()) / 60_000 : null; + const timeLeftMin = settlementLeftMin ?? timing.remainingMinutes; - const timeLeftMin = settlementLeftMin ?? timing.remainingMinutes; - + // ── Indicators ──────────────────────────────────────────────────────── const candles = klines1m; - const closes = candles.map((c) => c.close); + const closes = candles.map((c) => c.close); - const vwap = computeSessionVwap(candles); const vwapSeries = computeVwapSeries(candles); - const vwapNow = vwapSeries[vwapSeries.length - 1]; - - const lookback = CONFIG.vwapSlopeLookbackMinutes; - const vwapSlope = vwapSeries.length >= lookback ? (vwapNow - vwapSeries[vwapSeries.length - lookback]) / lookback : null; - const vwapDist = vwapNow ? (lastPrice - vwapNow) / vwapNow : null; - + const vwapNow = vwapSeries[vwapSeries.length - 1]; + const lookback = CONFIG.vwapSlopeLookbackMinutes; + const vwapSlope = vwapSeries.length >= lookback + ? (vwapNow - vwapSeries[vwapSeries.length - lookback]) / lookback + : null; + const vwapDist = vwapNow ? (lastPrice - vwapNow) / vwapNow : null; const rsiNow = computeRsi(closes, CONFIG.rsiPeriod); const rsiSeries = []; for (let i = 0; i < closes.length; i += 1) { - const sub = closes.slice(0, i + 1); - const r = computeRsi(sub, CONFIG.rsiPeriod); + const r = computeRsi(closes.slice(0, i + 1), CONFIG.rsiPeriod); if (r !== null) rsiSeries.push(r); } - const rsiMa = sma(rsiSeries, CONFIG.rsiMaPeriod); const rsiSlope = slopeLast(rsiSeries, 3); - const macd = computeMacd(closes, CONFIG.macdFast, CONFIG.macdSlow, CONFIG.macdSignal); - - const ha = computeHeikenAshi(candles); + const macd = computeMacd(closes, CONFIG.macdFast, CONFIG.macdSlow, CONFIG.macdSignal); + const ha = computeHeikenAshi(candles); const consec = countConsecutive(ha); - const vwapCrossCount = countVwapCrosses(closes, vwapSeries, 20); - const volumeRecent = candles.slice(-20).reduce((a, c) => a + c.volume, 0); - const volumeAvg = candles.slice(-120).reduce((a, c) => a + c.volume, 0) / 6; - + const vwapCrossCount = countVwapCrosses(closes, vwapSeries, 20); + const volumeRecent = candles.slice(-20).reduce((a, c) => a + c.volume, 0); + const volumeAvg = candles.slice(-120).reduce((a, c) => a + c.volume, 0) / 6; const failedVwapReclaim = vwapNow !== null && vwapSeries.length >= 3 ? closes[closes.length - 1] < vwapNow && closes[closes.length - 2] > vwapSeries[vwapSeries.length - 2] : false; - const regimeInfo = detectRegime({ - price: lastPrice, - vwap: vwapNow, - vwapSlope, - vwapCrossCount, - volumeRecent, - volumeAvg - }); + // ── Signal ──────────────────────────────────────────────────────────── + const regimeInfo = detectRegime({ price: lastPrice, vwap: vwapNow, vwapSlope, vwapCrossCount, volumeRecent, volumeAvg }); const scored = scoreDirection({ - price: lastPrice, - vwap: vwapNow, - vwapSlope, - rsi: rsiNow, - rsiSlope, - macd, - heikenColor: consec.color, - heikenCount: consec.count, - failedVwapReclaim + price: lastPrice, vwap: vwapNow, vwapSlope, + rsi: rsiNow, rsiSlope, macd, + heikenColor: consec.color, heikenCount: consec.count, + failedVwapReclaim, }); - const timeAware = applyTimeAwareness(scored.rawUp, timeLeftMin, CONFIG.candleWindowMinutes); - - const marketUp = poly.ok ? poly.prices.up : null; - const marketDown = poly.ok ? poly.prices.down : null; + const timeAware = applyTimeAwareness(scored.rawUp, timeLeftMin, CONFIG.candleWindowMinutes); + const marketUp = poly.ok ? poly.prices.up : null; + const marketDown = poly.ok ? poly.prices.down : null; const edge = computeEdge({ modelUp: timeAware.adjustedUp, modelDown: timeAware.adjustedDown, marketYes: marketUp, marketNo: marketDown }); + const rec = decide({ remainingMinutes: timeLeftMin, edgeUp: edge.edgeUp, edgeDown: edge.edgeDown, modelUp: timeAware.adjustedUp, modelDown: timeAware.adjustedDown }); - const rec = decide({ remainingMinutes: timeLeftMin, edgeUp: edge.edgeUp, edgeDown: edge.edgeDown, modelUp: timeAware.adjustedUp, modelDown: timeAware.adjustedDown }); - - // --- Trading actions --- - lastPoly = poly; - lastRec = rec; + // ── Trading ─────────────────────────────────────────────────────────── const marketSlugNow = poly.ok ? String(poly.market?.slug ?? "") : ""; resetIfMarketChanged(marketSlugNow); - while (actionQueue.length && trading.tradingEnabled && poly.ok) { - const action = actionQueue.shift(); - if (action.type === "buy") { - const pos = getPosition(); - if (pos.active) { - setStatusMessage("Já existe posição aberta"); - } else { - const side = rec.action === "ENTER" ? rec.side : (timeAware.adjustedUp >= timeAware.adjustedDown ? "UP" : "DOWN"); - const tokenId = side === "UP" ? poly.tokens.upTokenId : poly.tokens.downTokenId; - const book = side === "UP" ? poly.orderbook.up : poly.orderbook.down; - // Usa bestAsk + margem para garantir o match; cai no mid-price se não houver book - const rawAsk = book?.bestAsk ?? (side === "UP" ? marketUp : marketDown); - const priceNum = rawAsk != null ? Math.min(rawAsk + 0.02, 0.97) : 0.5; - const entryRef = rawAsk ?? priceNum; - setStatusMessage(`Comprando ${side}...`); - const result = await buyMarketOrder({ client: trading.client, tokenId, amount: trading.tradeAmount, price: priceNum }); - if (result.ok) { - const balance = await fetchPositionBalance(trading.client, tokenId); - const shares = balance > 0 ? balance : trading.tradeAmount / entryRef; - recordBuy({ side, tokenId, shares, entryPrice: entryRef, invested: trading.tradeAmount, marketSlug: marketSlugNow, orderId: result.order?.orderID }); - const orderId = result.order?.orderID ?? result.order?.id ?? "-"; - const balanceStr = balance > 0 ? `shares: ${balance.toFixed(2)}` : "saldo 0 (ordem não preenchida?)"; - setStatusMessage(`COMPROU ${side} @ ${(entryRef * 100).toFixed(1)}¢ | $${trading.tradeAmount} | ${balanceStr} | ID: ${String(orderId).slice(0, 12)}`, 8000); - } else { - const errMsg = `Erro na compra: ${result.error}`; - setStatusMessage(errMsg, 15000); - fs.mkdirSync("./logs", { recursive: true }); - fs.appendFileSync("./logs/trade_errors.log", `${new Date().toISOString()} BUY ${side} ${errMsg}\n`); - } - } - } else if (action.type === "sell") { - const pos = getPosition(); - if (!pos.active) { - setStatusMessage("Nenhuma posição para vender"); - } else { - setStatusMessage(`Vendendo ${pos.side}...`); - const sellBook = pos.side === "UP" ? poly.orderbook.up : poly.orderbook.down; - const rawBid = sellBook?.bestBid ?? (pos.side === "UP" ? marketUp : marketDown); - const sellPriceNum = rawBid != null ? Math.max(rawBid - 0.02, 0.03) : 0.5; - // Usa saldo real on-chain para evitar erro de saldo insuficiente - const actualShares = await fetchPositionBalance(trading.client, pos.tokenId); - const sharesToSell = actualShares > 0 ? actualShares : pos.shares; - const result = await sellMarketOrder({ client: trading.client, tokenId: pos.tokenId, amount: sharesToSell, price: sellPriceNum }); - if (result.ok) { - const priceNum = rawBid ?? sellPriceNum; - const pnl = (sharesToSell * priceNum) - pos.invested; - const roi = (pnl / pos.invested) * 100; - const sign = pnl >= 0 ? "+" : ""; - setStatusMessage(`VENDEU ${pos.side} | P&L: ${sign}$${pnl.toFixed(2)}`, 8000); - closedTrades.unshift({ side: pos.side, entryPrice: pos.entryPrice, exitPrice: priceNum, pnl, roi, ts: Date.now() }); - if (closedTrades.length > 10) closedTrades.pop(); - recordSell(); - } else { - const errMsg = `Erro na venda: ${result.error}`; - setStatusMessage(errMsg, 15000); - fs.mkdirSync("./logs", { recursive: true }); - fs.appendFileSync("./logs/trade_errors.log", `${new Date().toISOString()} SELL ${pos.side} ${errMsg}\n`); - } - } - } - } + await processActionQueue(keyboard.actionQueue, { trading, poly, rec, timeAware, marketSlugNow, onSold }); - // --- USDC balance fetch (every 30s) --- if (trading.tradingEnabled && Date.now() - usdcLastFetchMs > 30_000) { usdcLastFetchMs = Date.now(); - fetchUsdcBalance(trading.balanceAddress).then((bal) => { - usdcBalance = bal; - usdcBalanceError = null; - }).catch((err) => { - usdcBalanceError = err?.message ? err.message.slice(0, 40) : "erro"; - }); + fetchUsdcBalance(trading.balanceAddress) + .then((bal) => { usdcBalance = bal; usdcBalanceError = null; }) + .catch((err) => { usdcBalanceError = err?.message ? err.message.slice(0, 40) : "erro"; }); } - // --- Display data --- - const spotPrice = wsPrice ?? lastPrice; + // ── Derived display values ───────────────────────────────────────────── + const spotPrice = wsPrice ?? lastPrice; const currentPrice = chainlink?.price ?? null; - const marketSlug = poly.ok ? String(poly.market?.slug ?? "") : ""; - const marketStartMs = poly.ok && poly.market?.eventStartTime ? new Date(poly.market.eventStartTime).getTime() : null; - - if (marketSlug && priceToBeatState.slug !== marketSlug) { - priceToBeatState = { slug: marketSlug, value: null, setAtMs: null }; - } - if (priceToBeatState.slug && priceToBeatState.value === null && poly.ok && poly.market) { - const fromMarket = priceToBeatFromPolymarketMarket(poly.market); - if (fromMarket !== null) { - priceToBeatState = { slug: priceToBeatState.slug, value: fromMarket, setAtMs: Date.now(), source: "market" }; - } + const marketSlug = poly.ok ? String(poly.market?.slug ?? "") : ""; + const marketStartMs = poly.ok && poly.market?.eventStartTime + ? new Date(poly.market.eventStartTime).getTime() + : null; + + const priceToBeat = priceLatch.update({ marketSlug, currentPrice, marketStartMs, market: poly.market ?? null }); + + const settled = tracker.update({ marketSlug, rec, marketUp, marketDown, currentPrice, priceToBeat }); + if (settled) { + const { slug, side, won, pnl } = settled; + appendCsvRow(CSV_PATH, CSV_HEADER, [ + new Date().toISOString(), "SETTLED", "0", slug, + `${side}:${won ? "WIN" : "LOSS"}`, "", "", "", "", "", "", + `${won ? "WIN" : "LOSS"}:${side}`, won ? "WIN" : "LOSS", pnl.toFixed(4), + ]); } - if (priceToBeatState.slug && priceToBeatState.value === null && !priceToBeatFetching) { - const nowMs = Date.now(); - const okToLatch = marketStartMs === null ? true : nowMs >= marketStartMs; - if (okToLatch) { - const lateMs = marketStartMs !== null ? nowMs - marketStartMs : 0; - if (lateMs > 30_000 && marketStartMs !== null) { - // App started late — fetch historical Chainlink price at market open - priceToBeatFetching = true; - fetchChainlinkPriceAtMs(marketStartMs).then((p) => { - if (p !== null && priceToBeatState.slug === marketSlug && priceToBeatState.value === null) { - priceToBeatState = { slug: marketSlug, value: p, setAtMs: marketStartMs, source: "chainlink_historical" }; - } else if (p === null && currentPrice !== null && priceToBeatState.slug === marketSlug && priceToBeatState.value === null) { - priceToBeatState = { slug: marketSlug, value: Number(currentPrice), setAtMs: nowMs, source: "chainlink_latch" }; - } - priceToBeatFetching = false; - }).catch(() => { priceToBeatFetching = false; }); - } else if (currentPrice !== null) { - priceToBeatState = { slug: priceToBeatState.slug, value: Number(currentPrice), setAtMs: nowMs, source: "chainlink_latch" }; - } - } - } - const priceToBeat = priceToBeatState.slug === marketSlug ? priceToBeatState.value : null; - - // Trade outcome tracking - if (tradeState.slug !== null && tradeState.slug !== "" && marketSlug !== tradeState.slug) { - if (tradeState.hasSignal && tradeState.priceToBeat !== null && tradeState.lastChainlinkPrice !== null) { - const winner = tradeState.lastChainlinkPrice > tradeState.priceToBeat ? "UP" : "DOWN"; - const won = tradeState.side === winner; - const ep = tradeState.entryMarketPrice ?? 0.5; - const pnl = won ? (1 / ep) - 1 : -1; - if (won) runningStats.wins += 1; else runningStats.losses += 1; - runningStats.totalPnl += pnl; - recentOutcomes.unshift({ slug: tradeState.slug, side: tradeState.side, won, pnl, ts: new Date().toISOString() }); - if (recentOutcomes.length > 10) recentOutcomes.pop(); - appendCsvRow("./logs/signals.csv", header, [ - new Date().toISOString(), "SETTLED", "0", tradeState.slug, - `${tradeState.side}:${won ? "WIN" : "LOSS"}`, "", "", "", "", "", "", - `${won ? "WIN" : "LOSS"}:${tradeState.side}`, won ? "WIN" : "LOSS", pnl.toFixed(4) - ]); - } - tradeState = { slug: marketSlug, side: null, entryMarketPrice: null, priceToBeat: null, lastChainlinkPrice: currentPrice, hasSignal: false }; - } else if (tradeState.slug === null || tradeState.slug === "") { - tradeState.slug = marketSlug; - } - if (!tradeState.hasSignal && rec.action === "ENTER" && marketSlug) { - tradeState.side = rec.side; - tradeState.entryMarketPrice = rec.side === "UP" ? marketUp : marketDown; - tradeState.hasSignal = true; - } - if (currentPrice !== null) tradeState.lastChainlinkPrice = currentPrice; - if (priceToBeat !== null) tradeState.priceToBeat = priceToBeat; - if (poly.ok && poly.market && priceToBeatState.value === null) { + // Dump raw market JSON once per new slug (for debugging) + if (poly.ok && poly.market && priceToBeat === null) { const slug = safeFileSlug(poly.market.slug || poly.market.id || "market"); if (slug && !dumpedMarkets.has(slug)) { dumpedMarkets.add(slug); - try { fs.mkdirSync("./logs", { recursive: true }); fs.writeFileSync(path.join("./logs", `polymarket_market_${slug}.json`), JSON.stringify(poly.market, null, 2), "utf8"); } catch { /* ignore */ } + try { + fs.mkdirSync("./logs", { recursive: true }); + fs.writeFileSync(path.join("./logs", `polymarket_market_${slug}.json`), JSON.stringify(poly.market, null, 2), "utf8"); + } catch { /* ignore */ } } } - // Indicator formatting + // ── Display ─────────────────────────────────────────────────────────── const vwapSlopeLabel = vwapSlope === null ? "-" : vwapSlope > 0 ? "UP" : vwapSlope < 0 ? "DOWN" : "FLAT"; - const macdLabel = macd === null ? "-" : macd.hist < 0 ? (macd.histDelta !== null && macd.histDelta < 0 ? "bearish (exp)" : "bearish") : (macd.histDelta !== null && macd.histDelta > 0 ? "bullish (exp)" : "bullish"); - const lastCandle = klines1m.length ? klines1m[klines1m.length - 1] : null; - const lastClose = lastCandle?.close ?? null; - const close1mAgo = klines1m.length >= 2 ? klines1m[klines1m.length - 2]?.close ?? null : null; - const close3mAgo = klines1m.length >= 4 ? klines1m[klines1m.length - 4]?.close ?? null : null; - const delta1m = lastClose !== null && close1mAgo !== null ? lastClose - close1mAgo : null; - const delta3m = lastClose !== null && close3mAgo !== null ? lastClose - close3mAgo : null; - const haNarrative = (consec.color ?? "").toLowerCase() === "green" ? "LONG" : (consec.color ?? "").toLowerCase() === "red" ? "SHORT" : "NEUTRAL"; + const macdLabel = macd === null ? "-" + : macd.hist < 0 + ? (macd.histDelta !== null && macd.histDelta < 0 ? "bearish (exp)" : "bearish") + : (macd.histDelta !== null && macd.histDelta > 0 ? "bullish (exp)" : "bullish"); + const lastCandle = klines1m.length ? klines1m[klines1m.length - 1] : null; + const lastClose = lastCandle?.close ?? null; + const close1mAgo = klines1m.length >= 2 ? klines1m[klines1m.length - 2]?.close ?? null : null; + const close3mAgo = klines1m.length >= 4 ? klines1m[klines1m.length - 4]?.close ?? null : null; + const delta1m = lastClose !== null && close1mAgo !== null ? lastClose - close1mAgo : null; + const delta3m = lastClose !== null && close3mAgo !== null ? lastClose - close3mAgo : null; + const haNarrative = (consec.color ?? "").toLowerCase() === "green" ? "LONG" : (consec.color ?? "").toLowerCase() === "red" ? "SHORT" : "NEUTRAL"; const rsiNarrative = narrativeFromSlope(rsiSlope); const macdNarrative = narrativeFromSign(macd?.hist ?? null); const vwapNarrative = narrativeFromSign(vwapDist); - const rsiArrow = rsiSlope !== null && rsiSlope < 0 ? "\u2193" : rsiSlope !== null && rsiSlope > 0 ? "\u2191" : ""; - const delta1Narr = narrativeFromSign(delta1m); - const delta3Narr = narrativeFromSign(delta3m); + const rsiArrow = rsiSlope !== null && rsiSlope < 0 ? "\u2193" : rsiSlope !== null && rsiSlope > 0 ? "\u2191" : ""; + const delta1Narr = narrativeFromSign(delta1m); + const delta3Narr = narrativeFromSign(delta3m); - const pLong = timeAware?.adjustedUp ?? null; + const pLong = timeAware?.adjustedUp ?? null; const pShort = timeAware?.adjustedDown ?? null; - // Confirm hint - const shortcutsHint = trading.tradingEnabled && !stdinError ? `${ANSI.dim}[B]${ANSI.reset} Comprar ${ANSI.dim}[S]${ANSI.reset} Vender ${ANSI.dim}[Q]${ANSI.reset} Sair` : `${ANSI.dim}[Q]${ANSI.reset} Sair`; - const confirmHint = (() => { - if (!pendingAction) return null; - if (pendingAction.type === "buy") { - const side = rec.action === "ENTER" ? rec.side : (pLong >= pShort ? "UP" : "DOWN"); - const sc = side === "UP" ? ANSI.green : ANSI.red; - const mp = side === "UP" ? marketUp : marketDown; - const ps = mp != null ? `@ ${(mp * 100).toFixed(1)}\u00A2` : ""; - return `${ANSI.yellow}\u26A1 BUY ${sc}${side}${ANSI.reset} ${ANSI.yellow}${ps} $${trading.tradeAmount}${ANSI.reset} ${ANSI.white}[Y]${ANSI.reset} Sim ${ANSI.white}[N]${ANSI.reset} Cancelar`; - } - if (pendingAction.type === "sell") { - const pos = getPosition(); - if (!pos.active) return `${ANSI.gray}Sem posicao${ANSI.reset} ${ANSI.white}[N]${ANSI.reset}`; - const sc = pos.side === "UP" ? ANSI.green : ANSI.red; - return `${ANSI.yellow}\u26A1 VENDER ${sc}${pos.side}${ANSI.reset} ${ANSI.yellow}${pos.shares.toFixed(2)} sh${ANSI.reset} ${ANSI.white}[Y]${ANSI.reset} Sim ${ANSI.white}[N]${ANSI.reset} Cancelar`; - } - return null; - })(); - - const timeColor = timeLeftMin >= 10 ? ANSI.green : timeLeftMin >= 5 ? ANSI.yellow : ANSI.red; - const liquidity = poly.ok ? (Number(poly.market?.liquidityNum) || Number(poly.market?.liquidity) || null) : null; - const recColor = rec.action === "ENTER" ? ANSI.green : ANSI.gray; - const recLabel = rec.action === "ENTER" ? `\u25BA ${rec.side === "UP" ? "BUY UP" : "BUY DOWN"} [${rec.phase}\u00B7${rec.strength}]` : `NO TRADE [${rec.phase}]`; - - // Chainlink display - const clLine = colorPriceLine({ label: "", price: currentPrice, prevPrice: prevCurrentPrice, decimals: 2, prefix: "$" }); - const ptbDelta = (currentPrice !== null && priceToBeat !== null) ? currentPrice - priceToBeat : null; - const ptbStr = ptbDelta === null ? "" : ` (${ptbDelta > 0 ? ANSI.green + "+" : ptbDelta < 0 ? ANSI.red : ANSI.gray}$${Math.abs(ptbDelta).toFixed(2)}${ANSI.reset})`; - - const pos = getPosition(); - const posPrice = pos.active ? (pos.side === "UP" ? marketUp : marketDown) : null; - const currentMktPrice = posPrice != null ? posPrice : null; - const exitEval = evaluateExit({ + const shortcutsHint = trading.tradingEnabled && !keyboard.stdinError + ? `${ANSI.dim}[B]${ANSI.reset} Comprar ${ANSI.dim}[S]${ANSI.reset} Vender ${ANSI.dim}[Q]${ANSI.reset} Sair` + : `${ANSI.dim}[Q]${ANSI.reset} Sair`; + const confirmHint = keyboard.getConfirmHint({ rec, timeAware, marketUp, marketDown, tradeAmount: trading.tradeAmount }); + + const timeColor = timeLeftMin >= 10 ? ANSI.green : timeLeftMin >= 5 ? ANSI.yellow : ANSI.red; + const liquidity = poly.ok ? (Number(poly.market?.liquidityNum) || Number(poly.market?.liquidity) || null) : null; + const recColor = rec.action === "ENTER" ? ANSI.green : ANSI.gray; + const recLabel = rec.action === "ENTER" + ? `\u25BA ${rec.side === "UP" ? "BUY UP" : "BUY DOWN"} [${rec.phase}\u00B7${rec.strength}]` + : `NO TRADE [${rec.phase}]`; + + const clLine = colorPriceLine({ label: "", price: currentPrice, prevPrice: prevCurrentPrice, decimals: 2, prefix: "$" }); + const ptbDelta = currentPrice !== null && priceToBeat !== null ? currentPrice - priceToBeat : null; + const ptbStr = ptbDelta === null ? "" + : ` (${ptbDelta > 0 ? ANSI.green + "+" : ptbDelta < 0 ? ANSI.red : ANSI.gray}$${Math.abs(ptbDelta).toFixed(2)}${ANSI.reset})`; + + const pos = getPosition(); + const currentMktPrice = pos.active ? (pos.side === "UP" ? marketUp : marketDown) : null; + const exitEval = evaluateExit({ position: pos, modelUp: pLong, modelDown: pShort, currentMarketPrice: currentMktPrice, timeLeftMin, takeProfitPct: CONFIG.trading.takeProfitPct, @@ -568,17 +270,17 @@ async function main() { chainlinkLine: `${clLine}${ptbStr}`, priceToBeat, intervalLine: null, - marketUpStr: marketUp != null ? `${(marketUp * 100).toFixed(1)}\u00A2` : "-", + marketUpStr: marketUp != null ? `${(marketUp * 100).toFixed(1)}\u00A2` : "-", marketDownStr: marketDown != null ? `${(marketDown * 100).toFixed(1)}\u00A2` : "-", timeLeftMin, timeColor, liquidity, indicators: [ { label: "Heiken Ashi", value: colorByNarrative(`${consec.color ?? "-"} x${consec.count}`, haNarrative) }, - { label: "RSI", value: colorByNarrative(`${formatNumber(rsiNow, 1)} ${rsiArrow}`, rsiNarrative) }, - { label: "MACD", value: colorByNarrative(macdLabel, macdNarrative) }, + { label: "RSI", value: colorByNarrative(`${formatNumber(rsiNow, 1)} ${rsiArrow}`, rsiNarrative) }, + { label: "MACD", value: colorByNarrative(macdLabel, macdNarrative) }, { label: "\u0394 1/3 min", value: `${colorByNarrative(formatSignedDelta(delta1m, lastClose), delta1Narr)} | ${colorByNarrative(formatSignedDelta(delta3m, lastClose), delta3Narr)}` }, - { label: "VWAP", value: colorByNarrative(`${formatNumber(vwapNow, 0)} (${formatPct(vwapDist, 2)}) ${vwapSlopeLabel}`, vwapNarrative) }, + { label: "VWAP", value: colorByNarrative(`${formatNumber(vwapNow, 0)} (${formatPct(vwapDist, 2)}) ${vwapSlopeLabel}`, vwapNarrative) }, ], predictValue: `${ANSI.green}LONG ${formatProbPct(pLong, 0)}${ANSI.reset} / ${ANSI.red}SHORT ${formatProbPct(pShort, 0)}${ANSI.reset}`, recLine: `${recColor}${recLabel}${ANSI.reset}`, @@ -586,14 +288,14 @@ async function main() { currentMktPrice, exitEval, closedTrades, - runningStats, - recentOutcomes, + runningStats: tracker.getStats(), + recentOutcomes: tracker.getRecentOutcomes(), })); - prevSpotPrice = spotPrice ?? prevSpotPrice; + prevSpotPrice = spotPrice ?? prevSpotPrice; prevCurrentPrice = currentPrice ?? prevCurrentPrice; - appendCsvRow("./logs/signals.csv", header, [ + appendCsvRow(CSV_PATH, CSV_HEADER, [ new Date().toISOString(), timing.elapsedMinutes.toFixed(3), timeLeftMin.toFixed(3), @@ -606,8 +308,8 @@ async function main() { edge.edgeUp, edge.edgeDown, rec.action === "ENTER" ? `${rec.side}:${rec.phase}:${rec.strength}` : "NO_TRADE", - "", // outcome — preenchido na row SETTLED - "" // pnl — preenchido na row SETTLED + "", // outcome — filled in the SETTLED row + "", // pnl — filled in the SETTLED row ]); } catch (err) { console.log("────────────────────────────"); diff --git a/src/index5m.js b/src/index5m.js index 5b6acba0..8cb4aaed 100644 --- a/src/index5m.js +++ b/src/index5m.js @@ -1,19 +1,11 @@ import { CONFIG } from "./config5m.js"; import { fetchKlines, fetchLastPrice } from "./data/binance.js"; -import { fetchChainlinkBtcUsd, fetchChainlinkPriceAtMs } from "./data/chainlink.js"; +import { fetchChainlinkBtcUsd } from "./data/chainlink.js"; import { startChainlinkPriceStream } from "./data/chainlinkWs.js"; import { startPolymarketChainlinkPriceStream } from "./data/polymarketLiveWs.js"; -import { - fetchMarketBySlug, - fetchLiveEventsBySeriesId, - flattenEventMarkets, - pickLatestLiveMarket, - fetchClobPrice, - fetchOrderBook, - summarizeOrderBook -} from "./data/polymarket.js"; +import { createMarketResolver, fetchPolymarketSnapshot } from "./data/polymarket.js"; import { startBinanceOfiStream } from "./data/binanceWsOfi.js"; -import { computeSessionVwap, computeVwapSeries } from "./indicators/vwap.js"; +import { computeVwapSeries } from "./indicators/vwap.js"; import { computeRsi, slopeLast } from "./indicators/rsi.js"; import { computeHeikenAshi, countConsecutive } from "./indicators/heikenAshi.js"; import { computeEmaCross } from "./indicators/emaCross.js"; @@ -27,152 +19,48 @@ import path from "node:path"; import { applyGlobalProxyFromEnv } from "./net/proxy.js"; import { ANSI, renderScreen, buildScreen, kv, - colorPriceLine, formatSignedDelta, formatNumberDisplay, - colorByNarrative, formatNarrativeValue, narrativeFromSign, - narrativeFromSlope, formatProbPct, fmtEtHHMM, fmtTimeLeft, - safeFileSlug, priceToBeatFromPolymarketMarket, - setStatusMessage + colorPriceLine, formatSignedDelta, + colorByNarrative, narrativeFromSign, + narrativeFromSlope, formatProbPct, fmtEtHHMM, + safeFileSlug, setStatusMessage } from "./display.js"; import { initTradingClient } from "./trading/client.js"; -import { buyMarketOrder, sellMarketOrder } from "./trading/orders.js"; -import { getPosition, recordBuy, recordSell, computeROI, resetIfMarketChanged, fetchPositionBalance, fetchUsdcBalance, evaluateExit } from "./trading/position.js"; +import { fetchUsdcBalance, evaluateExit, resetIfMarketChanged, getPosition } from "./trading/position.js"; +import { setupKeyboard } from "./trading/keyboard.js"; +import { processActionQueue } from "./trading/executor.js"; +import { createPriceLatch } from "./trading/priceLatch.js"; +import { createTradeTracker } from "./trading/tracker.js"; applyGlobalProxyFromEnv(); -const dumpedMarkets = new Set(); - -const marketCache = { - market: null, - fetchedAtMs: 0 -}; - -async function resolveCurrentMarket() { - if (CONFIG.polymarket.marketSlug) { - return await fetchMarketBySlug(CONFIG.polymarket.marketSlug); - } - - if (!CONFIG.polymarket.autoSelectLatest) return null; - - const now = Date.now(); - if (marketCache.market && now - marketCache.fetchedAtMs < CONFIG.pollIntervalMs) { - return marketCache.market; - } - - const events = await fetchLiveEventsBySeriesId({ seriesId: CONFIG.polymarket.seriesId, limit: 25 }); - const markets = flattenEventMarkets(events); - const picked = pickLatestLiveMarket(markets); - - marketCache.market = picked; - marketCache.fetchedAtMs = now; - return picked; -} - -async function fetchPolymarketSnapshot() { - const market = await resolveCurrentMarket(); - - if (!market) return { ok: false, reason: "market_not_found" }; - - const outcomes = Array.isArray(market.outcomes) ? market.outcomes : (typeof market.outcomes === "string" ? JSON.parse(market.outcomes) : []); - const outcomePrices = Array.isArray(market.outcomePrices) - ? market.outcomePrices - : (typeof market.outcomePrices === "string" ? JSON.parse(market.outcomePrices) : []); - - const clobTokenIds = Array.isArray(market.clobTokenIds) - ? market.clobTokenIds - : (typeof market.clobTokenIds === "string" ? JSON.parse(market.clobTokenIds) : []); - - let upTokenId = null; - let downTokenId = null; - for (let i = 0; i < outcomes.length; i += 1) { - const label = String(outcomes[i]); - const tokenId = clobTokenIds[i] ? String(clobTokenIds[i]) : null; - if (!tokenId) continue; - - if (label.toLowerCase() === CONFIG.polymarket.upOutcomeLabel.toLowerCase()) upTokenId = tokenId; - if (label.toLowerCase() === CONFIG.polymarket.downOutcomeLabel.toLowerCase()) downTokenId = tokenId; - } - - const upIndex = outcomes.findIndex((x) => String(x).toLowerCase() === CONFIG.polymarket.upOutcomeLabel.toLowerCase()); - const downIndex = outcomes.findIndex((x) => String(x).toLowerCase() === CONFIG.polymarket.downOutcomeLabel.toLowerCase()); - - const gammaYes = upIndex >= 0 ? Number(outcomePrices[upIndex]) : null; - const gammaNo = downIndex >= 0 ? Number(outcomePrices[downIndex]) : null; - - if (!upTokenId || !downTokenId) { - return { ok: false, reason: "missing_token_ids", market, outcomes, clobTokenIds, outcomePrices }; - } - - let upBuy = null; - let downBuy = null; - let upBookSummary = { bestBid: null, bestAsk: null, spread: null, bidLiquidity: null, askLiquidity: null }; - let downBookSummary = { bestBid: null, bestAsk: null, spread: null, bidLiquidity: null, askLiquidity: null }; - - try { - const [yesBuy, noBuy, upBook, downBook] = await Promise.all([ - fetchClobPrice({ tokenId: upTokenId, side: "buy" }), - fetchClobPrice({ tokenId: downTokenId, side: "buy" }), - fetchOrderBook({ tokenId: upTokenId }), - fetchOrderBook({ tokenId: downTokenId }) - ]); - - upBuy = yesBuy; - downBuy = noBuy; - upBookSummary = summarizeOrderBook(upBook); - downBookSummary = summarizeOrderBook(downBook); - } catch { - upBuy = null; - downBuy = null; - upBookSummary = { - bestBid: Number(market.bestBid) || null, - bestAsk: Number(market.bestAsk) || null, - spread: Number(market.spread) || null, - bidLiquidity: null, - askLiquidity: null - }; - downBookSummary = { - bestBid: null, - bestAsk: null, - spread: Number(market.spread) || null, - bidLiquidity: null, - askLiquidity: null - }; - } - - return { - ok: true, - market, - tokens: { upTokenId, downTokenId }, - prices: { - up: upBuy ?? gammaYes, - down: downBuy ?? gammaNo - }, - orderbook: { - up: upBookSummary, - down: downBookSummary - } - }; -} - function ofiLabel(ofi) { if (!ofi || ofi.total === 0) return "-"; - const pct = (ofi.ofi * 100).toFixed(0); + const pct = (ofi.ofi * 100).toFixed(0); const sign = ofi.ofi > 0 ? "+" : ""; return `${sign}${pct}%`; } function ofiNarrative(ofi) { if (!ofi || ofi.total === 0) return "NEUTRAL"; - if (ofi.ofi > 0.05) return "LONG"; + if (ofi.ofi > 0.05) return "LONG"; if (ofi.ofi < -0.05) return "SHORT"; return "NEUTRAL"; } +const CSV_PATH = "./logs/signals_5m.csv"; +const CSV_HEADER = [ + "timestamp", "entry_minute", "time_left_min", + "ofi_30s", "ofi_1m", "ofi_2m", + "roc1", "roc3", "ema_cross", "rsi", "signal", + "model_up", "model_down", "mkt_up", "mkt_down", + "edge_up", "edge_down", "recommendation", "outcome", "pnl", +]; + async function main() { - const ofiStream = startBinanceOfiStream({ symbol: CONFIG.symbol }); + const ofiStream = startBinanceOfiStream({ symbol: CONFIG.symbol }); const polymarketLiveStream = startPolymarketChainlinkPriceStream({}); - const chainlinkStream = startChainlinkPriceStream({}); + const chainlinkStream = startChainlinkPriceStream({}); - // --- Trading setup --- let trading = { client: null, tradingEnabled: false, tradeAmount: 0, initError: null }; try { trading = await initTradingClient(CONFIG); @@ -180,63 +68,29 @@ async function main() { trading.initError = err?.message ?? String(err); } - const actionQueue = []; - let pendingAction = null; - let lastPoly = null; - let lastRec = null; + const resolveMarket = createMarketResolver(CONFIG.polymarket, CONFIG.pollIntervalMs); + const keyboard = setupKeyboard({ tradingEnabled: trading.tradingEnabled }); + const priceLatch = createPriceLatch(); + const tracker = createTradeTracker(); - let stdinError = null; - try { - if (!process.stdin.isTTY) throw new Error("stdin não é TTY — rode com: node src/index5m.js"); - process.stdin.setRawMode(true); - process.stdin.resume(); - process.stdin.on("data", (key) => { - const ch = key.toString().toLowerCase(); - if (trading.tradingEnabled) { - if (pendingAction !== null) { - if (ch === "y") { actionQueue.push({ ...pendingAction }); pendingAction = null; } - else if (ch === "n" || key[0] === 0x1b) { pendingAction = null; } - } else { - if (ch === "b") pendingAction = { type: "buy" }; - else if (ch === "s") pendingAction = { type: "sell" }; - } - } - if (ch === "q" || key[0] === 0x03) process.exit(0); - }); - } catch (err) { - stdinError = err?.message ?? String(err); - } + const dumpedMarkets = new Set(); - let prevSpotPrice = null; + let prevSpotPrice = null; let prevCurrentPrice = null; - let usdcBalance = null; + let usdcBalance = null; let usdcBalanceError = null; - let usdcLastFetchMs = 0; - let priceToBeatState = { slug: null, value: null, setAtMs: null }; - let priceToBeatFetching = false; - - let tradeState = { slug: null, side: null, entryMarketPrice: null, priceToBeat: null, lastChainlinkPrice: null, hasSignal: false }; - let runningStats = { wins: 0, losses: 0, totalPnl: 0 }; - let recentOutcomes = []; - - const header = [ - "timestamp", "entry_minute", "time_left_min", "ofi_30s", "ofi_1m", "ofi_2m", - "roc1", "roc3", "ema_cross", "rsi", "signal", - "model_up", "model_down", "mkt_up", "mkt_down", - "edge_up", "edge_down", "recommendation", "outcome", "pnl" - ]; + let usdcLastFetchMs = 0; while (true) { const timing = getCandleWindowTiming(CONFIG.candleWindowMinutes); - const wsTick = ofiStream.getLast(); - const wsPrice = wsTick?.price ?? null; - const ofiData = ofiStream.getOfi(); - - const polymarketWsTick = polymarketLiveStream.getLast(); + const wsTick = ofiStream.getLast(); + const wsPrice = wsTick?.price ?? null; + const ofiData = ofiStream.getOfi(); + const polymarketWsTick = polymarketLiveStream.getLast(); const polymarketWsPrice = polymarketWsTick?.price ?? null; - const chainlinkWsTick = chainlinkStream.getLast(); - const chainlinkWsPrice = chainlinkWsTick?.price ?? null; + const chainlinkWsTick = chainlinkStream.getLast(); + const chainlinkWsPrice = chainlinkWsTick?.price ?? null; try { const chainlinkPromise = polymarketWsPrice !== null @@ -249,297 +103,166 @@ async function main() { fetchKlines({ interval: "1m", limit: 60 }), fetchLastPrice(), chainlinkPromise, - fetchPolymarketSnapshot() + fetchPolymarketSnapshot(resolveMarket, CONFIG.polymarket), ]); - const settlementMs = poly.ok && poly.market?.endDate ? new Date(poly.market.endDate).getTime() : null; + const settlementMs = poly.ok && poly.market?.endDate ? new Date(poly.market.endDate).getTime() : null; const settlementLeftMin = settlementMs ? (settlementMs - Date.now()) / 60_000 : null; - const timeLeftMin = settlementLeftMin ?? timing.remainingMinutes; + const timeLeftMin = settlementLeftMin ?? timing.remainingMinutes; - // Use only last N candles for short VWAP + // ── Indicators ──────────────────────────────────────────────────────── const vwapCandles = klines1m.slice(-CONFIG.vwapCandleWindow); - const allCloses = klines1m.map(c => c.close); - const vwapCloses = vwapCandles.map(c => c.close); + const allCloses = klines1m.map((c) => c.close); - const vwap = computeSessionVwap(vwapCandles); const vwapSeries = computeVwapSeries(vwapCandles); - const vwapNow = vwapSeries[vwapSeries.length - 1]; - - const lookback = CONFIG.vwapSlopeLookbackMinutes; - const vwapSlope = vwapSeries.length >= lookback ? (vwapNow - vwapSeries[vwapSeries.length - lookback]) / lookback : null; - const vwapDist = vwapNow ? (lastPrice - vwapNow) / vwapNow : null; - - // RSI with shorter period + const vwapNow = vwapSeries[vwapSeries.length - 1]; + const lookback = CONFIG.vwapSlopeLookbackMinutes; + const vwapSlope = vwapSeries.length >= lookback + ? (vwapNow - vwapSeries[vwapSeries.length - lookback]) / lookback + : null; + const vwapDist = vwapNow ? (lastPrice - vwapNow) / vwapNow : null; const rsiNow = computeRsi(allCloses, CONFIG.rsiPeriod); const rsiSeries = []; for (let i = 0; i < allCloses.length; i++) { - const sub = allCloses.slice(0, i + 1); - const r = computeRsi(sub, CONFIG.rsiPeriod); + const r = computeRsi(allCloses.slice(0, i + 1), CONFIG.rsiPeriod); if (r !== null) rsiSeries.push(r); } const rsiSlope = slopeLast(rsiSeries, 3); - // EMA crossover - const emaCross = computeEmaCross(allCloses, CONFIG.emaCrossFast, CONFIG.emaCrossSlow); - - // Heiken Ashi on recent candles - const ha = computeHeikenAshi(klines1m.slice(-10)); - const consec = countConsecutive(ha); - - // Momentum - const momentum = computeMomentum(klines1m); + const emaCross = computeEmaCross(allCloses, CONFIG.emaCrossFast, CONFIG.emaCrossSlow); + const ha = computeHeikenAshi(klines1m.slice(-10)); + const consec = countConsecutive(ha); + const momentum = computeMomentum(klines1m); const momentumScore = scoreMomentum(momentum); - - // Order flow const orderFlowScore = scoreOrderFlow(ofiData); - // Score direction (5m model) + // ── Signal ──────────────────────────────────────────────────────────── const scored = scoreDirection5m({ - orderFlow: orderFlowScore, - momentumScore, - emaCross, - rsi: rsiNow, - rsiSlope, - heikenColor: consec.color, - heikenCount: consec.count, - price: lastPrice, - vwap: vwapNow, - vwapSlope + orderFlow: orderFlowScore, momentumScore, emaCross, + rsi: rsiNow, rsiSlope, + heikenColor: consec.color, heikenCount: consec.count, + price: lastPrice, vwap: vwapNow, vwapSlope, }); - const timeAware = applyTimeAwareness5m(scored.rawUp, timeLeftMin, CONFIG.candleWindowMinutes); - - const marketUp = poly.ok ? poly.prices.up : null; - const marketDown = poly.ok ? poly.prices.down : null; + const timeAware = applyTimeAwareness5m(scored.rawUp, timeLeftMin, CONFIG.candleWindowMinutes); + const marketUp = poly.ok ? poly.prices.up : null; + const marketDown = poly.ok ? poly.prices.down : null; const edge = computeEdge({ modelUp: timeAware.adjustedUp, modelDown: timeAware.adjustedDown, marketYes: marketUp, marketNo: marketDown }); + const rec = decide5m({ remainingMinutes: timeLeftMin, edgeUp: edge.edgeUp, edgeDown: edge.edgeDown, modelUp: timeAware.adjustedUp, modelDown: timeAware.adjustedDown }); - const rec = decide5m({ remainingMinutes: timeLeftMin, edgeUp: edge.edgeUp, edgeDown: edge.edgeDown, modelUp: timeAware.adjustedUp, modelDown: timeAware.adjustedDown }); - - // --- Trading actions --- - lastPoly = poly; - lastRec = rec; + // ── Trading ─────────────────────────────────────────────────────────── const marketSlugNow = poly.ok ? String(poly.market?.slug ?? "") : ""; resetIfMarketChanged(marketSlugNow); - while (actionQueue.length && trading.tradingEnabled && poly.ok) { - const action = actionQueue.shift(); - if (action.type === "buy") { - const pos = getPosition(); - if (pos.active) { - setStatusMessage("Já existe posição aberta"); - } else { - const side = rec.action === "ENTER" ? rec.side : (timeAware.adjustedUp >= timeAware.adjustedDown ? "UP" : "DOWN"); - const tokenId = side === "UP" ? poly.tokens.upTokenId : poly.tokens.downTokenId; - const book = side === "UP" ? poly.orderbook.up : poly.orderbook.down; - const rawAsk = book?.bestAsk ?? (side === "UP" ? marketUp : marketDown); - const priceNum = rawAsk != null ? Math.min(rawAsk + 0.02, 0.97) : 0.5; - const entryRef = rawAsk ?? priceNum; - setStatusMessage(`Comprando ${side}...`); - const result = await buyMarketOrder({ client: trading.client, tokenId, amount: trading.tradeAmount, price: priceNum }); - if (result.ok) { - const balance = await fetchPositionBalance(trading.client, tokenId); - const shares = balance > 0 ? balance : trading.tradeAmount / entryRef; - recordBuy({ side, tokenId, shares, entryPrice: entryRef, invested: trading.tradeAmount, marketSlug: marketSlugNow, orderId: result.order?.orderID }); - const orderId = result.order?.orderID ?? result.order?.id ?? "-"; - const balanceStr = balance > 0 ? `shares: ${balance.toFixed(2)}` : "saldo 0 (ordem não preenchida?)"; - setStatusMessage(`COMPROU ${side} @ ${(entryRef * 100).toFixed(1)}¢ | $${trading.tradeAmount} | ${balanceStr} | ID: ${String(orderId).slice(0, 12)}`, 8000); - } else { - const errMsg = `Erro na compra: ${result.error}`; - setStatusMessage(errMsg, 15000); - fs.mkdirSync("./logs", { recursive: true }); - fs.appendFileSync("./logs/trade_errors.log", `${new Date().toISOString()} BUY ${side} ${errMsg}\n`); - } - } - } else if (action.type === "sell") { - const pos = getPosition(); - if (!pos.active) { - setStatusMessage("Nenhuma posição para vender"); - } else { - setStatusMessage(`Vendendo ${pos.side}...`); - const sellBook = pos.side === "UP" ? poly.orderbook.up : poly.orderbook.down; - const rawBid = sellBook?.bestBid ?? (pos.side === "UP" ? marketUp : marketDown); - const sellPriceNum = rawBid != null ? Math.max(rawBid - 0.02, 0.03) : 0.5; - const actualShares = await fetchPositionBalance(trading.client, pos.tokenId); - const sharesToSell = actualShares > 0 ? actualShares : pos.shares; - const result = await sellMarketOrder({ client: trading.client, tokenId: pos.tokenId, amount: sharesToSell, price: sellPriceNum }); - if (result.ok) { - const priceNum = rawBid ?? sellPriceNum; - const pnl = (sharesToSell * priceNum) - pos.invested; - const sign = pnl >= 0 ? "+" : ""; - setStatusMessage(`VENDEU ${pos.side} | P&L: ${sign}$${pnl.toFixed(2)}`, 8000); - recordSell(); - } else { - const errMsg = `Erro na venda: ${result.error}`; - setStatusMessage(errMsg, 15000); - fs.mkdirSync("./logs", { recursive: true }); - fs.appendFileSync("./logs/trade_errors.log", `${new Date().toISOString()} SELL ${pos.side} ${errMsg}\n`); - } - } + await processActionQueue(keyboard.actionQueue, { trading, poly, rec, timeAware, marketSlugNow }); + + if (trading.tradingEnabled && Date.now() - usdcLastFetchMs > 30_000) { + usdcLastFetchMs = Date.now(); + fetchUsdcBalance(trading.balanceAddress) + .then((bal) => { usdcBalance = bal; usdcBalanceError = null; }) + .catch((err) => { usdcBalanceError = err?.message ? err.message.slice(0, 40) : "erro"; }); + } + + // ── Derived display values ───────────────────────────────────────────── + const spotPrice = wsPrice ?? lastPrice; + const currentPrice = chainlink?.price ?? null; + const marketSlug = poly.ok ? String(poly.market?.slug ?? "") : ""; + const marketStartMs = poly.ok && poly.market?.eventStartTime + ? new Date(poly.market.eventStartTime).getTime() + : null; + const settlementMs5m = poly.ok && poly.market?.endDate ? new Date(poly.market.endDate).getTime() : null; + + const priceToBeat = priceLatch.update({ marketSlug, currentPrice, marketStartMs, market: poly.market ?? null }); + + const settled = tracker.update({ marketSlug, rec, marketUp, marketDown, currentPrice, priceToBeat }); + if (settled) { + const { slug, side, won, pnl } = settled; + appendCsvRow(CSV_PATH, CSV_HEADER, [ + new Date().toISOString(), "SETTLED", "0", "", "", "", "", "", "", "", "", + `${side}:${won ? "WIN" : "LOSS"}`, "", "", "", "", "", + `${won ? "WIN" : "LOSS"}:${side}`, won ? "WIN" : "LOSS", pnl.toFixed(4), + ]); + } + + if (poly.ok && poly.market && priceToBeat === null) { + const slug = safeFileSlug(poly.market.slug || poly.market.id || "market"); + if (slug && !dumpedMarkets.has(slug)) { + dumpedMarkets.add(slug); + try { + fs.mkdirSync("./logs", { recursive: true }); + fs.writeFileSync(path.join("./logs", `polymarket_market_${slug}.json`), JSON.stringify(poly.market, null, 2), "utf8"); + } catch { /* ignore */ } } } - // --- Display --- + // ── Display ─────────────────────────────────────────────────────────── const lastCandle = klines1m.length ? klines1m[klines1m.length - 1] : null; - const lastClose = lastCandle?.close ?? null; + const lastClose = lastCandle?.close ?? null; const close1mAgo = klines1m.length >= 2 ? klines1m[klines1m.length - 2]?.close ?? null : null; const close3mAgo = klines1m.length >= 4 ? klines1m[klines1m.length - 4]?.close ?? null : null; - const delta1m = lastClose !== null && close1mAgo !== null ? lastClose - close1mAgo : null; - const delta3m = lastClose !== null && close3mAgo !== null ? lastClose - close3mAgo : null; + const delta1m = lastClose !== null && close1mAgo !== null ? lastClose - close1mAgo : null; + const delta3m = lastClose !== null && close3mAgo !== null ? lastClose - close3mAgo : null; - const pLong = timeAware?.adjustedUp ?? null; + const pLong = timeAware?.adjustedUp ?? null; const pShort = timeAware?.adjustedDown ?? null; - const predictValue = `${ANSI.green}LONG${ANSI.reset} ${ANSI.green}${formatProbPct(pLong, 0)}${ANSI.reset} / ${ANSI.red}SHORT${ANSI.reset} ${ANSI.red}${formatProbPct(pShort, 0)}${ANSI.reset}`; - - const marketUpStr = `${marketUp ?? "-"}${marketUp == null ? "" : "\u00A2"}`; - const marketDownStr = `${marketDown ?? "-"}${marketDown == null ? "" : "\u00A2"}`; - const polyHeaderValue = `${ANSI.green}\u2191 UP${ANSI.reset} ${marketUpStr} | ${ANSI.red}\u2193 DOWN${ANSI.reset} ${marketDownStr}`; - // OFI display const ofi30Narrative = ofiNarrative(ofiData.ofi30s); - const ofi1Narrative = ofiNarrative(ofiData.ofi1m); - const ofi2Narrative = ofiNarrative(ofiData.ofi2m); + const ofi1Narrative = ofiNarrative(ofiData.ofi1m); + const ofi2Narrative = ofiNarrative(ofiData.ofi2m); const ofiValue = `30s:${colorByNarrative(ofiLabel(ofiData.ofi30s), ofi30Narrative)} | 1m:${colorByNarrative(ofiLabel(ofiData.ofi1m), ofi1Narrative)} | 2m:${colorByNarrative(ofiLabel(ofiData.ofi2m), ofi2Narrative)}`; - // EMA cross display - const emaLabel = emaCross === null ? "-" : emaCross.crossover !== "NONE" - ? `${emaCross.crossover} (${emaCross.expanding ? "expanding" : "flat"})` - : emaCross.bullish - ? `bullish${emaCross.expanding ? " (expanding)" : ""}` - : `bearish${emaCross.expanding ? " (expanding)" : ""}`; + const emaLabel = emaCross === null ? "-" + : emaCross.crossover !== "NONE" + ? `${emaCross.crossover} (${emaCross.expanding ? "expanding" : "flat"})` + : emaCross.bullish + ? `bullish${emaCross.expanding ? " (expanding)" : ""}` + : `bearish${emaCross.expanding ? " (expanding)" : ""}`; const emaNarrative = emaCross === null ? "NEUTRAL" : emaCross.bullish ? "LONG" : "SHORT"; - // Momentum display const momLabel = momentum === null ? "-" : (() => { - const r1 = momentum.roc1 !== null ? `${(momentum.roc1 * 100).toFixed(3)}%` : "-"; - const r3 = momentum.roc3 !== null ? `${(momentum.roc3 * 100).toFixed(3)}%` : "-"; - const acc = momentum.accel !== null ? (momentum.accel > 0 ? " \u2191accel" : momentum.accel < 0 ? " \u2193decel" : "") : ""; + const r1 = momentum.roc1 !== null ? `${(momentum.roc1 * 100).toFixed(3)}%` : "-"; + const r3 = momentum.roc3 !== null ? `${(momentum.roc3 * 100).toFixed(3)}%` : "-"; + const acc = momentum.accel !== null + ? (momentum.accel > 0 ? " \u2191accel" : momentum.accel < 0 ? " \u2193decel" : "") + : ""; return `1m:${r1} | 3m:${r3}${acc}`; })(); - const momNarrative = momentum?.roc1 != null ? narrativeFromSign(momentum.roc1) : "NEUTRAL"; - const rsiArrow = rsiSlope !== null && rsiSlope < 0 ? "\u2193" : rsiSlope !== null && rsiSlope > 0 ? "\u2191" : ""; - const rsiNarrative = narrativeFromSlope(rsiSlope); - const haNarrative = (consec.color ?? "").toLowerCase() === "green" ? "LONG" : (consec.color ?? "").toLowerCase() === "red" ? "SHORT" : "NEUTRAL"; + const momNarrative = momentum?.roc1 != null ? narrativeFromSign(momentum.roc1) : "NEUTRAL"; + const rsiArrow = rsiSlope !== null && rsiSlope < 0 ? "\u2193" : rsiSlope !== null && rsiSlope > 0 ? "\u2191" : ""; + const rsiNarrative = narrativeFromSlope(rsiSlope); + const haNarrative = (consec.color ?? "").toLowerCase() === "green" ? "LONG" : (consec.color ?? "").toLowerCase() === "red" ? "SHORT" : "NEUTRAL"; const vwapSlopeLabel = vwapSlope === null ? "-" : vwapSlope > 0 ? "UP" : vwapSlope < 0 ? "DOWN" : "FLAT"; const vwapNarrative = narrativeFromSign(vwapDist); - const delta1Narr = narrativeFromSign(delta1m); - const delta3Narr = narrativeFromSign(delta3m); + const delta1Narr = narrativeFromSign(delta1m); + const delta3Narr = narrativeFromSign(delta3m); - const signal = rec.action === "ENTER" ? (rec.side === "UP" ? "BUY UP" : "BUY DOWN") : "NO TRADE"; + const signal = rec.action === "ENTER" ? (rec.side === "UP" ? "BUY UP" : "BUY DOWN") : "NO TRADE"; const recColor = rec.action === "ENTER" ? ANSI.green : ANSI.gray; - const recLabel = rec.action === "ENTER" ? `\u25BA ${rec.side === "UP" ? "BUY UP" : "BUY DOWN"} [${rec.phase}\u00B7${rec.strength}]` : `NO TRADE [${rec.phase}]`; - - const spotPrice = wsPrice ?? lastPrice; - const currentPrice = chainlink?.price ?? null; - const marketSlug = poly.ok ? String(poly.market?.slug ?? "") : ""; - const marketStartMs = poly.ok && poly.market?.eventStartTime ? new Date(poly.market.eventStartTime).getTime() : null; - - if (marketSlug && priceToBeatState.slug !== marketSlug) { - priceToBeatState = { slug: marketSlug, value: null, setAtMs: null }; - } - if (priceToBeatState.slug && priceToBeatState.value === null && poly.ok && poly.market) { - const fromMarket = priceToBeatFromPolymarketMarket(poly.market); - if (fromMarket !== null) priceToBeatState = { slug: priceToBeatState.slug, value: fromMarket, setAtMs: Date.now(), source: "market" }; - } - if (priceToBeatState.slug && priceToBeatState.value === null && !priceToBeatFetching) { - const nowMs = Date.now(); - const okToLatch = marketStartMs === null ? true : nowMs >= marketStartMs; - if (okToLatch) { - const lateMs = marketStartMs !== null ? nowMs - marketStartMs : 0; - if (lateMs > 30_000 && marketStartMs !== null) { - priceToBeatFetching = true; - fetchChainlinkPriceAtMs(marketStartMs).then((p) => { - if (p !== null && priceToBeatState.slug === marketSlug && priceToBeatState.value === null) { - priceToBeatState = { slug: marketSlug, value: p, setAtMs: marketStartMs, source: "chainlink_historical" }; - } else if (p === null && currentPrice !== null && priceToBeatState.slug === marketSlug && priceToBeatState.value === null) { - priceToBeatState = { slug: marketSlug, value: Number(currentPrice), setAtMs: nowMs, source: "chainlink_latch" }; - } - priceToBeatFetching = false; - }).catch(() => { priceToBeatFetching = false; }); - } else if (currentPrice !== null) { - priceToBeatState = { slug: priceToBeatState.slug, value: Number(currentPrice), setAtMs: nowMs, source: "chainlink_latch" }; - } - } - } - const priceToBeat = priceToBeatState.slug === marketSlug ? priceToBeatState.value : null; - - // Trade outcome tracking - if (tradeState.slug !== null && tradeState.slug !== "" && marketSlug !== tradeState.slug) { - if (tradeState.hasSignal && tradeState.priceToBeat !== null && tradeState.lastChainlinkPrice !== null) { - const winner = tradeState.lastChainlinkPrice > tradeState.priceToBeat ? "UP" : "DOWN"; - const won = tradeState.side === winner; - const ep = tradeState.entryMarketPrice ?? 0.5; - const pnl = won ? (1 / ep) - 1 : -1; - if (won) runningStats.wins += 1; else runningStats.losses += 1; - runningStats.totalPnl += pnl; - recentOutcomes.unshift({ slug: tradeState.slug, side: tradeState.side, won, pnl, ts: new Date().toISOString() }); - if (recentOutcomes.length > 10) recentOutcomes.pop(); - appendCsvRow("./logs/signals_5m.csv", header, [ - new Date().toISOString(), "SETTLED", "0", "", "", "", "", "", "", "", "", - `${tradeState.side}:${won ? "WIN" : "LOSS"}`, "", "", "", "", "", - `${won ? "WIN" : "LOSS"}:${tradeState.side}`, won ? "WIN" : "LOSS", pnl.toFixed(4) - ]); - } - tradeState = { slug: marketSlug, side: null, entryMarketPrice: null, priceToBeat: null, lastChainlinkPrice: currentPrice, hasSignal: false }; - } else if (tradeState.slug === null || tradeState.slug === "") { tradeState.slug = marketSlug; } - if (!tradeState.hasSignal && rec.action === "ENTER" && marketSlug) { tradeState.side = rec.side; tradeState.entryMarketPrice = rec.side === "UP" ? marketUp : marketDown; tradeState.hasSignal = true; } - if (currentPrice !== null) tradeState.lastChainlinkPrice = currentPrice; - if (priceToBeat !== null) tradeState.priceToBeat = priceToBeat; - - if (poly.ok && poly.market && priceToBeatState.value === null) { - const slug = safeFileSlug(poly.market.slug || poly.market.id || "market"); - if (slug && !dumpedMarkets.has(slug)) { - dumpedMarkets.add(slug); - try { fs.mkdirSync("./logs", { recursive: true }); fs.writeFileSync(path.join("./logs", `polymarket_market_${slug}.json`), JSON.stringify(poly.market, null, 2), "utf8"); } catch { /* ignore */ } - } - } - - // --- USDC balance fetch (every 30s) --- - if (trading.tradingEnabled && Date.now() - usdcLastFetchMs > 30_000) { - usdcLastFetchMs = Date.now(); - fetchUsdcBalance(trading.balanceAddress).then((bal) => { - usdcBalance = bal; - usdcBalanceError = null; - }).catch((err) => { - usdcBalanceError = err?.message ? err.message.slice(0, 40) : "erro"; - }); - } + const recLabel = rec.action === "ENTER" + ? `\u25BA ${rec.side === "UP" ? "BUY UP" : "BUY DOWN"} [${rec.phase}\u00B7${rec.strength}]` + : `NO TRADE [${rec.phase}]`; const isNextMarket = marketStartMs !== null && marketStartMs > Date.now(); - const timeColor = isNextMarket ? ANSI.yellow : timeLeftMin >= 3 ? ANSI.green : timeLeftMin >= 1.5 ? ANSI.yellow : ANSI.red; - const liquidity = poly.ok ? (Number(poly.market?.liquidityNum) || Number(poly.market?.liquidity) || null) : null; - const settlementMs5m = poly.ok && poly.market?.endDate ? new Date(poly.market.endDate).getTime() : null; + const timeColor = isNextMarket ? ANSI.yellow : timeLeftMin >= 3 ? ANSI.green : timeLeftMin >= 1.5 ? ANSI.yellow : ANSI.red; + const liquidity = poly.ok ? (Number(poly.market?.liquidityNum) || Number(poly.market?.liquidity) || null) : null; - const clLine = colorPriceLine({ label: "", price: currentPrice, prevPrice: prevCurrentPrice, decimals: 2, prefix: "$" }); - const ptbDelta = (currentPrice !== null && priceToBeat !== null) ? currentPrice - priceToBeat : null; - const ptbStr = ptbDelta === null ? "" : ` (${ptbDelta > 0 ? ANSI.green + "+" : ptbDelta < 0 ? ANSI.red : ANSI.gray}$${Math.abs(ptbDelta).toFixed(2)}${ANSI.reset})`; - - const shortcutsHint = trading.tradingEnabled && !stdinError ? `${ANSI.dim}[B]${ANSI.reset} Comprar ${ANSI.dim}[S]${ANSI.reset} Vender ${ANSI.dim}[Q]${ANSI.reset} Sair` : `${ANSI.dim}[Q]${ANSI.reset} Sair`; - const confirmHint = (() => { - if (!pendingAction) return null; - if (pendingAction.type === "buy") { - const side = rec.action === "ENTER" ? rec.side : (timeAware.adjustedUp >= timeAware.adjustedDown ? "UP" : "DOWN"); - const sc = side === "UP" ? ANSI.green : ANSI.red; - const mp = side === "UP" ? marketUp : marketDown; - const ps = mp != null ? `@ ${(mp * 100).toFixed(1)}\u00A2` : ""; - return `${ANSI.yellow}\u26A1 BUY ${sc}${side}${ANSI.reset} ${ANSI.yellow}${ps} $${trading.tradeAmount}${ANSI.reset} ${ANSI.white}[Y]${ANSI.reset} Sim ${ANSI.white}[N]${ANSI.reset} Cancelar`; - } - if (pendingAction.type === "sell") { - const pos = getPosition(); - if (!pos.active) return `${ANSI.gray}Sem posicao${ANSI.reset} ${ANSI.white}[N]${ANSI.reset}`; - const sc = pos.side === "UP" ? ANSI.green : ANSI.red; - return `${ANSI.yellow}\u26A1 VENDER ${sc}${pos.side}${ANSI.reset} ${ANSI.yellow}${pos.shares.toFixed(2)} sh${ANSI.reset} ${ANSI.white}[Y]${ANSI.reset} Sim ${ANSI.white}[N]${ANSI.reset} Cancelar`; - } - return null; - })(); + const clLine = colorPriceLine({ label: "", price: currentPrice, prevPrice: prevCurrentPrice, decimals: 2, prefix: "$" }); + const ptbDelta = currentPrice !== null && priceToBeat !== null ? currentPrice - priceToBeat : null; + const ptbStr = ptbDelta === null ? "" + : ` (${ptbDelta > 0 ? ANSI.green + "+" : ptbDelta < 0 ? ANSI.red : ANSI.gray}$${Math.abs(ptbDelta).toFixed(2)}${ANSI.reset})`; + + const shortcutsHint = trading.tradingEnabled && !keyboard.stdinError + ? `${ANSI.dim}[B]${ANSI.reset} Comprar ${ANSI.dim}[S]${ANSI.reset} Vender ${ANSI.dim}[Q]${ANSI.reset} Sair` + : `${ANSI.dim}[Q]${ANSI.reset} Sair`; + const confirmHint = keyboard.getConfirmHint({ rec, timeAware, marketUp, marketDown, tradeAmount: trading.tradeAmount }); const intervalLine = (marketStartMs !== null && settlementMs5m !== null) ? kv("Intervalo:", `${isNextMarket ? ANSI.yellow : ""}${fmtEtHHMM(marketStartMs)} \u2192 ${fmtEtHHMM(settlementMs5m)} ET${isNextMarket ? ANSI.reset : ""}`) : null; - const pos = getPosition(); - const posPrice = pos.active ? (pos.side === "UP" ? marketUp : marketDown) : null; - const currentMktPrice = posPrice != null ? posPrice : null; - const exitEval = evaluateExit({ + const pos = getPosition(); + const currentMktPrice = pos.active ? (pos.side === "UP" ? marketUp : marketDown) : null; + const exitEval = evaluateExit({ position: pos, modelUp: timeAware.adjustedUp, modelDown: timeAware.adjustedDown, currentMarketPrice: currentMktPrice, timeLeftMin, takeProfitPct: CONFIG.trading.takeProfitPct, @@ -564,44 +287,44 @@ async function main() { chainlinkLine: `${clLine}${ptbStr}`, priceToBeat, intervalLine, - marketUpStr: marketUp != null ? `${(marketUp * 100).toFixed(1)}\u00A2` : "-", + marketUpStr: marketUp != null ? `${(marketUp * 100).toFixed(1)}\u00A2` : "-", marketDownStr: marketDown != null ? `${(marketDown * 100).toFixed(1)}\u00A2` : "-", timeLeftMin, timeColor, liquidity, indicators: [ - { label: "Order Flow", value: ofiValue }, - { label: "Momentum", value: colorByNarrative(momLabel, momNarrative) }, - { label: "EMA Cross", value: colorByNarrative(emaLabel, emaNarrative) }, - { label: "RSI", value: colorByNarrative(`${formatNumber(rsiNow, 1)} ${rsiArrow}`, rsiNarrative) }, - { label: "Heiken Ashi", value: colorByNarrative(`${consec.color ?? "-"} x${consec.count}`, haNarrative) }, - { label: "VWAP", value: colorByNarrative(`${formatNumber(vwapNow, 0)} (${formatPct(vwapDist, 2)}) ${vwapSlopeLabel}`, vwapNarrative) }, + { label: "Order Flow", value: ofiValue }, + { label: "Momentum", value: colorByNarrative(momLabel, momNarrative) }, + { label: "EMA Cross", value: colorByNarrative(emaLabel, emaNarrative) }, + { label: "RSI", value: colorByNarrative(`${formatNumber(rsiNow, 1)} ${rsiArrow}`, rsiNarrative) }, + { label: "Heiken Ashi", value: colorByNarrative(`${consec.color ?? "-"} x${consec.count}`, haNarrative) }, + { label: "VWAP", value: colorByNarrative(`${formatNumber(vwapNow, 0)} (${formatPct(vwapDist, 2)}) ${vwapSlopeLabel}`, vwapNarrative) }, { label: "\u0394 1/3 min", value: `${colorByNarrative(formatSignedDelta(delta1m, lastClose), delta1Narr)} | ${colorByNarrative(formatSignedDelta(delta3m, lastClose), delta3Narr)}` }, ], - predictValue, + predictValue: `${ANSI.green}LONG${ANSI.reset} ${ANSI.green}${formatProbPct(pLong, 0)}${ANSI.reset} / ${ANSI.red}SHORT${ANSI.reset} ${ANSI.red}${formatProbPct(pShort, 0)}${ANSI.reset}`, recLine: `${recColor}${recLabel}${ANSI.reset}`, position: pos, currentMktPrice, exitEval, closedTrades: [], - runningStats, - recentOutcomes, + runningStats: tracker.getStats(), + recentOutcomes: tracker.getRecentOutcomes(), })); - prevSpotPrice = spotPrice ?? prevSpotPrice; + prevSpotPrice = spotPrice ?? prevSpotPrice; prevCurrentPrice = currentPrice ?? prevCurrentPrice; - appendCsvRow("./logs/signals_5m.csv", header, [ + appendCsvRow(CSV_PATH, CSV_HEADER, [ new Date().toISOString(), timing.elapsedMinutes.toFixed(3), timeLeftMin.toFixed(3), ofiData.ofi30s?.ofi?.toFixed(3) ?? "", - ofiData.ofi1m?.ofi?.toFixed(3) ?? "", - ofiData.ofi2m?.ofi?.toFixed(3) ?? "", + ofiData.ofi1m?.ofi?.toFixed(3) ?? "", + ofiData.ofi2m?.ofi?.toFixed(3) ?? "", momentum?.roc1?.toFixed(6) ?? "", momentum?.roc3?.toFixed(6) ?? "", emaCross?.crossover ?? "", - rsiNow?.toFixed(1) ?? "", + rsiNow?.toFixed(1) ?? "", signal, timeAware.adjustedUp, timeAware.adjustedDown, @@ -610,13 +333,13 @@ async function main() { edge.edgeUp, edge.edgeDown, rec.action === "ENTER" ? `${rec.side}:${rec.phase}:${rec.strength}` : "NO_TRADE", - "", - "" + "", // outcome + "", // pnl ]); } catch (err) { - console.log("\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"); + console.log("────────────────────────────"); console.log(`Error: ${err?.message ?? String(err)}`); - console.log("\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"); + console.log("────────────────────────────"); } await sleep(CONFIG.pollIntervalMs); diff --git a/src/trading/executor.js b/src/trading/executor.js new file mode 100644 index 00000000..7156e314 --- /dev/null +++ b/src/trading/executor.js @@ -0,0 +1,91 @@ +import fs from "node:fs"; +import { clamp } from "../utils.js"; +import { setStatusMessage } from "../display.js"; +import { buyMarketOrder, sellMarketOrder } from "./orders.js"; +import { getPosition, recordBuy, recordSell, fetchPositionBalance } from "./position.js"; + +function logError(msg) { + try { + fs.mkdirSync("./logs", { recursive: true }); + fs.appendFileSync("./logs/trade_errors.log", `${new Date().toISOString()} ${msg}\n`); + } catch { /* ignore */ } +} + +/** + * Drains the action queue produced by setupKeyboard(), executing each buy/sell + * against the Polymarket CLOB. + * + * @param {Array} actionQueue - mutated in place (items shifted out) + * @param {object} ctx + * @param {object} ctx.trading - { client, tradingEnabled, tradeAmount } + * @param {object} ctx.poly - fetchPolymarketSnapshot() result + * @param {object} ctx.rec - decide() / decide5m() result + * @param {object} ctx.timeAware - applyTimeAwareness() result + * @param {string} ctx.marketSlugNow + * @param {Function} [ctx.onSold] - called with { side, entryPrice, exitPrice, pnl, roi } after a sell + */ +export async function processActionQueue(actionQueue, { trading, poly, rec, timeAware, marketSlugNow, onSold }) { + while (actionQueue.length && trading.tradingEnabled && poly.ok) { + const action = actionQueue.shift(); + const marketUp = poly.prices.up; + const marketDown = poly.prices.down; + + if (action.type === "buy") { + const pos = getPosition(); + if (pos.active) { + setStatusMessage("Já existe posição aberta"); + continue; + } + const side = rec.action === "ENTER" + ? rec.side + : (timeAware.adjustedUp >= timeAware.adjustedDown ? "UP" : "DOWN"); + const tokenId = side === "UP" ? poly.tokens.upTokenId : poly.tokens.downTokenId; + const book = side === "UP" ? poly.orderbook.up : poly.orderbook.down; + const rawAsk = book?.bestAsk ?? (side === "UP" ? marketUp : marketDown); + const priceNum = rawAsk != null ? clamp(rawAsk + 0.02, 0, 0.97) : 0.5; + const entryRef = rawAsk ?? priceNum; + + setStatusMessage(`Comprando ${side}...`); + const result = await buyMarketOrder({ client: trading.client, tokenId, amount: trading.tradeAmount, price: priceNum }); + if (result.ok) { + const balance = await fetchPositionBalance(trading.client, tokenId); + const shares = balance > 0 ? balance : trading.tradeAmount / entryRef; + recordBuy({ side, tokenId, shares, entryPrice: entryRef, invested: trading.tradeAmount, marketSlug: marketSlugNow, orderId: result.order?.orderID }); + const orderId = result.order?.orderID ?? result.order?.id ?? "-"; + const balanceStr = balance > 0 ? `shares: ${balance.toFixed(2)}` : "saldo 0 (ordem não preenchida?)"; + setStatusMessage(`COMPROU ${side} @ ${(entryRef * 100).toFixed(1)}¢ | $${trading.tradeAmount} | ${balanceStr} | ID: ${String(orderId).slice(0, 12)}`, 8000); + } else { + const errMsg = `Erro na compra: ${result.error}`; + setStatusMessage(errMsg, 15000); + logError(`BUY ${side} ${errMsg}`); + } + } else if (action.type === "sell") { + const pos = getPosition(); + if (!pos.active) { + setStatusMessage("Nenhuma posição para vender"); + continue; + } + setStatusMessage(`Vendendo ${pos.side}...`); + const sellBook = pos.side === "UP" ? poly.orderbook.up : poly.orderbook.down; + const rawBid = sellBook?.bestBid ?? (pos.side === "UP" ? marketUp : marketDown); + const sellPriceNum = rawBid != null ? clamp(rawBid - 0.02, 0.03, 1) : 0.5; + const actualShares = await fetchPositionBalance(trading.client, pos.tokenId); + const sharesToSell = actualShares > 0 ? actualShares : pos.shares; + + const result = await sellMarketOrder({ client: trading.client, tokenId: pos.tokenId, amount: sharesToSell, price: sellPriceNum }); + if (result.ok) { + const exitPrice = rawBid ?? sellPriceNum; + const pnl = (sharesToSell * exitPrice) - pos.invested; + const roi = (pnl / pos.invested) * 100; + const sign = pnl >= 0 ? "+" : ""; + setStatusMessage(`VENDEU ${pos.side} | P&L: ${sign}$${pnl.toFixed(2)}`, 8000); + recordSell(); + onSold?.({ side: pos.side, entryPrice: pos.entryPrice, exitPrice, pnl, roi }); + } else { + const errMsg = `Erro na venda: ${result.error}`; + setStatusMessage(errMsg, 15000); + logError(`SELL ${pos.side} ${errMsg}`); + } + } + } +} diff --git a/src/trading/keyboard.js b/src/trading/keyboard.js new file mode 100644 index 00000000..495acb63 --- /dev/null +++ b/src/trading/keyboard.js @@ -0,0 +1,70 @@ +import { ANSI } from "../display.js"; +import { getPosition } from "./position.js"; + +/** + * Sets up raw-mode stdin, an action queue, and pending-confirmation state. + * + * Returns: + * actionQueue — array drained by processActionQueue() each tick + * getConfirmHint — builds the confirmation line shown on the display + * stdinError — non-null string if TTY setup failed + */ +export function setupKeyboard({ tradingEnabled }) { + const actionQueue = []; + let pendingAction = null; + let stdinError = null; + + try { + if (!process.stdin.isTTY) throw new Error("stdin não é TTY — rode diretamente com node (sem pipe)"); + process.stdin.setRawMode(true); + process.stdin.resume(); + process.stdin.on("data", (key) => { + const ch = key.toString().toLowerCase(); + if (tradingEnabled) { + if (pendingAction !== null) { + if (ch === "y") { actionQueue.push({ ...pendingAction }); pendingAction = null; } + else if (ch === "n" || key[0] === 0x1b) { pendingAction = null; } + } else { + if (ch === "b") pendingAction = { type: "buy" }; + else if (ch === "s") pendingAction = { type: "sell" }; + } + } + if (ch === "q" || key[0] === 0x03) process.exit(0); + }); + } catch (err) { + stdinError = err?.message ?? String(err); + } + + /** + * Builds the confirmation hint string for the current pending action. + * Returns null if nothing is pending. + * + * @param {object} ctx + * @param {object} ctx.rec - decide() result + * @param {object} ctx.timeAware - applyTimeAwareness() result + * @param {number|null} ctx.marketUp + * @param {number|null} ctx.marketDown + * @param {number} ctx.tradeAmount + */ + function getConfirmHint({ rec, timeAware, marketUp, marketDown, tradeAmount }) { + if (!pendingAction) return null; + if (pendingAction.type === "buy") { + const side = rec.action === "ENTER" + ? rec.side + : (timeAware.adjustedUp >= timeAware.adjustedDown ? "UP" : "DOWN"); + const sc = side === "UP" ? ANSI.green : ANSI.red; + const mp = side === "UP" ? marketUp : marketDown; + const ps = mp != null ? `@ ${(mp * 100).toFixed(1)}\u00A2` : ""; + return `${ANSI.yellow}\u26A1 BUY ${sc}${side}${ANSI.reset} ${ANSI.yellow}${ps} $${tradeAmount}${ANSI.reset} ${ANSI.white}[Y]${ANSI.reset} Sim ${ANSI.white}[N]${ANSI.reset} Cancelar`; + } + if (pendingAction.type === "sell") { + const pos = getPosition(); + if (!pos.active) return `${ANSI.gray}Sem posicao${ANSI.reset} ${ANSI.white}[N]${ANSI.reset}`; + const sc = pos.side === "UP" ? ANSI.green : ANSI.red; + return `${ANSI.yellow}\u26A1 VENDER ${sc}${pos.side}${ANSI.reset} ${ANSI.yellow}${pos.shares.toFixed(2)} sh${ANSI.reset} ${ANSI.white}[Y]${ANSI.reset} Sim ${ANSI.white}[N]${ANSI.reset} Cancelar`; + } + return null; + } + + return { actionQueue, getConfirmHint, get stdinError() { return stdinError; } }; +} diff --git a/src/trading/priceLatch.js b/src/trading/priceLatch.js new file mode 100644 index 00000000..b8d7fa56 --- /dev/null +++ b/src/trading/priceLatch.js @@ -0,0 +1,74 @@ +import { fetchChainlinkPriceAtMs } from "../data/chainlink.js"; +import { priceToBeatFromPolymarketMarket } from "../display.js"; + +/** + * Stateful manager that latches the Chainlink BTC/USD price at the moment a + * new market opens. This is the "price to beat" shown on the display. + * + * Strategy (in priority order): + * 1. Read the reference price embedded in the Polymarket market object. + * 2. If the app started late (>30s after open), fetch the historical + * Chainlink price at the exact open timestamp. + * 3. Otherwise latch the live Chainlink price on the first tick. + * + * Usage: + * const latch = createPriceLatch(); + * // inside the poll loop: + * const priceToBeat = latch.update({ marketSlug, currentPrice, marketStartMs, market }); + */ +export function createPriceLatch() { + let state = { slug: null, value: null, setAtMs: null, source: null }; + let fetching = false; + + /** + * @param {string} marketSlug + * @param {number|null} currentPrice - live Chainlink price this tick + * @param {number|null} marketStartMs - market open timestamp (ms) + * @param {object|null} market - raw Polymarket market object + * @returns {number|null} latched price, or null if not yet available + */ + function update({ marketSlug, currentPrice, marketStartMs, market }) { + // Reset when the market slug changes + if (marketSlug && state.slug !== marketSlug) { + state = { slug: marketSlug, value: null, setAtMs: null, source: null }; + } + + // 1. Try reading reference price from the market object itself + if (state.slug && state.value === null && market) { + const fromMarket = priceToBeatFromPolymarketMarket(market); + if (fromMarket !== null) { + state = { slug: state.slug, value: fromMarket, setAtMs: Date.now(), source: "market" }; + return state.value; + } + } + + // 2 & 3. Latch from Chainlink + if (state.slug && state.value === null && !fetching) { + const nowMs = Date.now(); + const okToLatch = marketStartMs === null ? true : nowMs >= marketStartMs; + if (!okToLatch) return null; + + const lateMs = marketStartMs !== null ? nowMs - marketStartMs : 0; + if (lateMs > 30_000 && marketStartMs !== null) { + // App started late — fetch historical price at the open + fetching = true; + const slugAtFetch = state.slug; + fetchChainlinkPriceAtMs(marketStartMs).then((p) => { + if (state.slug !== slugAtFetch || state.value !== null) return; + if (p !== null) { + state = { slug: slugAtFetch, value: p, setAtMs: marketStartMs, source: "chainlink_historical" }; + } else if (currentPrice !== null) { + state = { slug: slugAtFetch, value: Number(currentPrice), setAtMs: nowMs, source: "chainlink_latch" }; + } + }).catch(() => { /* ignore — will retry next tick */ }) + .finally(() => { fetching = false; }); + } else if (currentPrice !== null) { + state = { slug: state.slug, value: Number(currentPrice), setAtMs: nowMs, source: "chainlink_latch" }; + } + } + + return state.slug === marketSlug ? state.value : null; + } + + return { update }; +} diff --git a/src/trading/tracker.js b/src/trading/tracker.js new file mode 100644 index 00000000..5b5de95a --- /dev/null +++ b/src/trading/tracker.js @@ -0,0 +1,86 @@ +/** + * Per-market signal outcome tracker. + * + * Tracks which direction the model recommended when entering a market, then + * computes win/loss when the market slug changes (i.e. the market settled). + * + * The settled result is returned from update() so the caller can write it to + * the CSV in whatever format the app requires. + * + * Usage: + * const tracker = createTradeTracker(); + * // inside the poll loop: + * const settled = tracker.update({ marketSlug, rec, marketUp, marketDown, currentPrice, priceToBeat }); + * if (settled) appendCsvRow(..., buildSettledRow(settled)); + */ +export function createTradeTracker() { + let tradeState = { + slug: null, + side: null, + entryMarketPrice: null, + priceToBeat: null, + lastChainlinkPrice: null, + hasSignal: false, + }; + let runningStats = { wins: 0, losses: 0, totalPnl: 0 }; + let recentOutcomes = []; // { slug, side, won, pnl, ts }[] + + /** + * @param {object} ctx + * @param {string} ctx.marketSlug + * @param {object} ctx.rec - decide() result + * @param {number|null} ctx.marketUp + * @param {number|null} ctx.marketDown + * @param {number|null} ctx.currentPrice - live Chainlink price + * @param {number|null} ctx.priceToBeat + * + * @returns {{ slug, side, won, pnl, ts } | null} settled outcome, or null + */ + function update({ marketSlug, rec, marketUp, marketDown, currentPrice, priceToBeat }) { + let settled = null; + + if (tradeState.slug !== null && tradeState.slug !== "" && marketSlug !== tradeState.slug) { + // Market changed — evaluate the previous market's outcome + if (tradeState.hasSignal && tradeState.priceToBeat !== null && tradeState.lastChainlinkPrice !== null) { + const winner = tradeState.lastChainlinkPrice > tradeState.priceToBeat ? "UP" : "DOWN"; + const won = tradeState.side === winner; + const ep = tradeState.entryMarketPrice ?? 0.5; + const pnl = won ? (1 / ep) - 1 : -1; + if (won) runningStats.wins += 1; else runningStats.losses += 1; + runningStats.totalPnl += pnl; + const outcome = { slug: tradeState.slug, side: tradeState.side, won, pnl, ts: new Date().toISOString() }; + recentOutcomes.unshift(outcome); + if (recentOutcomes.length > 10) recentOutcomes.pop(); + settled = outcome; + } + tradeState = { + slug: marketSlug, + side: null, + entryMarketPrice: null, + priceToBeat: null, + lastChainlinkPrice: currentPrice, + hasSignal: false, + }; + } else if (tradeState.slug === null || tradeState.slug === "") { + tradeState.slug = marketSlug; + } + + // Latch the first signal for this market + if (!tradeState.hasSignal && rec.action === "ENTER" && marketSlug) { + tradeState.side = rec.side; + tradeState.entryMarketPrice = rec.side === "UP" ? marketUp : marketDown; + tradeState.hasSignal = true; + } + + if (currentPrice !== null) tradeState.lastChainlinkPrice = currentPrice; + if (priceToBeat !== null) tradeState.priceToBeat = priceToBeat; + + return settled; + } + + return { + update, + getStats: () => ({ ...runningStats }), + getRecentOutcomes: () => recentOutcomes, + }; +} From fbe4223baff155266938160624da7bb3a64bebaf Mon Sep 17 00:00:00 2001 From: Jean Carraro Date: Mon, 6 Apr 2026 16:22:46 -0300 Subject: [PATCH 09/49] Fix sell: divide fetchPositionBalance by 1e6 to convert raw token units to shares getBalanceAllowance returns balance in 6-decimal raw token units (like USDC), not in human-readable shares. The sell order was sending the raw value directly, causing the CLOB API to multiply it by 1e6 internally and reject with: balance: 2134231, order amount: 2134231000000 Co-Authored-By: Claude Sonnet 4.6 --- src/trading/position.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/trading/position.js b/src/trading/position.js index dfdeb446..3ff3d0f6 100644 --- a/src/trading/position.js +++ b/src/trading/position.js @@ -156,7 +156,8 @@ export async function fetchPositionBalance(client, tokenId) { asset_type: AssetType.CONDITIONAL, token_id: tokenId, }); - return Number(res?.balance ?? 0); + // getBalanceAllowance returns raw token units (6 decimals); convert to shares + return Number(res?.balance ?? 0) / 1e6; } catch { return 0; } From a8e21011550572ccf94c240be93f55cbf4a35b22 Mon Sep 17 00:00:00 2001 From: Jean Carraro Date: Mon, 6 Apr 2026 16:55:33 -0300 Subject: [PATCH 10/49] Add dry-run study logger for signal performance analysis MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Each app now writes ./logs/dryrun_15m.csv and ./logs/dryrun_5m.csv. Ticks accumulate in memory per market; on slug change (settlement) the entire buffer is flushed with outcome, btc_at_settlement, direction_correct, and signal_roi filled in retroactively — enabling end-of-day analysis of which signals were correct and what ROI they would have produced if followed immediately. Co-Authored-By: Claude Sonnet 4.6 --- CLAUDE.md | 26 +++++++++ src/dryRun.js | 141 +++++++++++++++++++++++++++++++++++++++++++++++++ src/index.js | 43 +++++++++++++++ src/index5m.js | 43 +++++++++++++++ 4 files changed, 253 insertions(+) create mode 100644 src/dryRun.js diff --git a/CLAUDE.md b/CLAUDE.md index 76015f67..1ed26e48 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -104,4 +104,30 @@ Called once at startup via `applyGlobalProxyFromEnv()`. Reads `HTTPS_PROXY`/`HTT - Terminal screen refreshed every second via ANSI escape codes (`\x1b[H` + per-line `\x1b[K` + `\x1b[J`), rendered inside an alternate screen buffer. - `./logs/signals.csv` — one row per poll tick (15m mode) with regime, signal, model probabilities, market prices, edge, and recommendation. - `./logs/signals_5m.csv` — one row per poll tick (5m mode) with OFI, momentum, EMA cross, RSI, model probs, edge, and recommendation. +- `./logs/dryrun_15m.csv` — dry-run study log for the 15m app (see below). +- `./logs/dryrun_5m.csv` — dry-run study log for the 5m app (see below). - `./logs/polymarket_market_.json` — raw Polymarket market JSON dumped once per new market slug. + +### Dry-run study logger (`src/dryRun.js`) + +Enabled automatically in both apps — no extra flags needed. Each app creates one logger instance at startup: + +``` +createDryRunLogger15m("./logs/dryrun_15m.csv") // used by index.js +createDryRunLogger5m("./logs/dryrun_5m.csv") // used by index5m.js +``` + +**How it works:** ticks accumulate in memory (one object per poll second) keyed to the current market slug. When the slug changes (market settled), the entire buffer is flushed to CSV in one `appendFileSync` call with four outcome columns filled in retroactively: + +| Column | Description | +|---|---| +| `outcome` | `UP` or `DOWN` — which side actually won (BTC above/below open price) | +| `btc_at_settlement` | Final Chainlink BTC/USD price at the moment of flush | +| `direction_correct` | `1` if signal matched outcome, `0` if not, empty if `NO TRADE` | +| `signal_roi` | Hypothetical ROI if you had entered at that tick's market price. E.g. BUY UP at 0.65¢ → WIN: `+0.5385`; LOSS: `-1.0000`. Empty for `NO TRADE`. | + +`process.on("exit")` flushes any in-progress market buffer so data is not lost on Ctrl+C / Q. + +**15m columns:** `timestamp, market_slug, time_left_min, btc_price, market_up, market_down, regime, signal, model_up, model_down, edge_up, edge_down, rec_detail, rsi, rsi_slope, macd_hist, macd_label, ha_color, ha_count, vwap, vwap_dist_pct, vwap_slope, outcome, btc_at_settlement, direction_correct, signal_roi` + +**5m columns:** `timestamp, market_slug, time_left_min, btc_price, market_up, market_down, signal, model_up, model_down, edge_up, edge_down, rec_detail, ofi_30s, ofi_1m, ofi_2m, roc1, roc3, ema_cross, rsi, ha_color, ha_count, vwap, vwap_dist_pct, vwap_slope, outcome, btc_at_settlement, direction_correct, signal_roi` diff --git a/src/dryRun.js b/src/dryRun.js new file mode 100644 index 00000000..e433a214 --- /dev/null +++ b/src/dryRun.js @@ -0,0 +1,141 @@ +/** + * Dry-run study logger. + * + * Accumulates per-tick snapshots in memory for the current market. When the + * market slug changes (settlement), flushes the buffer to CSV with outcome + * columns filled in retroactively: + * + * outcome — "UP" or "DOWN" (which side actually won) + * btc_at_settlement — final Chainlink BTC/USD price + * direction_correct — 1 if signal matched outcome, 0 if not, "" if NO TRADE + * signal_roi — hypothetical ROI if entered at that tick's market price + * + * Usage: + * const logger = createDryRunLogger15m("./logs/dryrun_15m.csv"); + * // inside the poll loop (after priceLatch.update): + * logger.tick({ slug, priceToBeat, btcPrice, signalSide, entryPrice, dataValues }); + * // on process exit: + * logger.flushNow(); + */ +import fs from "node:fs"; +import path from "node:path"; +import { ensureDir } from "./utils.js"; + +export const HEADER_15M = [ + "timestamp", "market_slug", "time_left_min", + "btc_price", "market_up", "market_down", + "regime", "signal", "model_up", "model_down", "edge_up", "edge_down", "rec_detail", + "rsi", "rsi_slope", "macd_hist", "macd_label", "ha_color", "ha_count", + "vwap", "vwap_dist_pct", "vwap_slope", + "outcome", "btc_at_settlement", "direction_correct", "signal_roi", +]; + +export const HEADER_5M = [ + "timestamp", "market_slug", "time_left_min", + "btc_price", "market_up", "market_down", + "signal", "model_up", "model_down", "edge_up", "edge_down", "rec_detail", + "ofi_30s", "ofi_1m", "ofi_2m", "roc1", "roc3", "ema_cross", + "rsi", "ha_color", "ha_count", "vwap", "vwap_dist_pct", "vwap_slope", + "outcome", "btc_at_settlement", "direction_correct", "signal_roi", +]; + +function csvEscape(v) { + if (v === null || v === undefined) return ""; + const s = String(v); + if (s.includes(",") || s.includes('"') || s.includes("\n")) { + return `"${s.replaceAll('"', '""')}"`; + } + return s; +} + +function toCsvLine(values) { + return values.map(csvEscape).join(","); +} + +function fmt(v, decimals = 4) { + if (v === null || v === undefined || (typeof v === "number" && Number.isNaN(v))) return null; + return Number(v).toFixed(decimals); +} + +function createBuffer(csvPath, header) { + let currentSlug = null; + let lastPriceToBeat = null; + let lastBtcPrice = null; + let buffer = []; // { dataValues, signalSide: "UP"|"DOWN"|null, entryPrice: number|null }[] + + function _ensureHeader() { + ensureDir(path.dirname(csvPath)); + if (!fs.existsSync(csvPath)) { + fs.writeFileSync(csvPath, header.join(",") + "\n", "utf8"); + } + } + + function _flush() { + if (buffer.length === 0) return; + + _ensureHeader(); + + const ptb = lastPriceToBeat; + const btcFinal = lastBtcPrice; + const canComputeOutcome = ptb !== null && btcFinal !== null; + const outcome = canComputeOutcome ? (btcFinal > ptb ? "UP" : "DOWN") : null; + + const lines = buffer.map(({ dataValues, signalSide, entryPrice }) => { + let correct = ""; + let roi = ""; + if (outcome !== null && signalSide !== null) { + correct = signalSide === outcome ? "1" : "0"; + roi = entryPrice !== null && entryPrice > 0 + ? (signalSide === outcome ? fmt((1 / entryPrice) - 1, 4) : "-1.0000") + : ""; + } + return toCsvLine([ + ...dataValues, + outcome ?? "", + btcFinal !== null ? fmt(btcFinal, 2) : "", + correct, + roi, + ]); + }); + + fs.appendFileSync(csvPath, lines.join("\n") + "\n", "utf8"); + buffer = []; + } + + /** + * Record one tick. Call after all indicators and priceLatch.update() are computed. + * + * @param {string} slug - current market slug + * @param {number|null} priceToBeat - latched BTC open price (may be null early in market) + * @param {number|null} btcPrice - live Chainlink/Polymarket BTC price + * @param {"UP"|"DOWN"|null} signalSide - direction of the signal (null if NO TRADE) + * @param {number|null} entryPrice - market price for signalSide (null if NO TRADE) + * @param {Array} dataValues - pre-settlement CSV columns (all except outcome cols) + */ + function tick({ slug, priceToBeat, btcPrice, signalSide, entryPrice, dataValues }) { + if (slug !== currentSlug && currentSlug !== null) { + _flush(); + } + currentSlug = slug; + if (priceToBeat !== null) lastPriceToBeat = priceToBeat; + if (btcPrice !== null) lastBtcPrice = btcPrice; + buffer.push({ dataValues, signalSide, entryPrice }); + } + + /** Flush current buffer immediately (call on process exit). */ + function flushNow() { + _flush(); + } + + return { tick, flushNow }; +} + +/** Create a dry-run logger for the 15-minute assistant. */ +export function createDryRunLogger15m(csvPath) { + return createBuffer(csvPath, HEADER_15M); +} + +/** Create a dry-run logger for the 5-minute assistant. */ +export function createDryRunLogger5m(csvPath) { + return createBuffer(csvPath, HEADER_5M); +} diff --git a/src/index.js b/src/index.js index 507d7f88..c35520ea 100644 --- a/src/index.js +++ b/src/index.js @@ -29,6 +29,7 @@ import { setupKeyboard } from "./trading/keyboard.js"; import { processActionQueue } from "./trading/executor.js"; import { createPriceLatch } from "./trading/priceLatch.js"; import { createTradeTracker } from "./trading/tracker.js"; +import { createDryRunLogger15m } from "./dryRun.js"; applyGlobalProxyFromEnv(); @@ -70,6 +71,9 @@ async function main() { const tracker = createTradeTracker(); const dumpedMarkets = new Set(); + const dryRun = createDryRunLogger15m("./logs/dryrun_15m.csv"); + process.on("exit", () => dryRun.flushNow()); + let closedTrades = []; // { side, entryPrice, exitPrice, pnl, roi, ts }[] const onSold = ({ side, entryPrice, exitPrice, pnl, roi }) => { @@ -311,6 +315,45 @@ async function main() { "", // outcome — filled in the SETTLED row "", // pnl — filled in the SETTLED row ]); + + // ── Dry-run study log ───────────────────────────────────────────────── + { + const signalSide = rec.action === "ENTER" ? rec.side : null; + const entryPrice = signalSide === "UP" ? marketUp : signalSide === "DOWN" ? marketDown : null; + const vwapSlopeLbl = vwapSlope === null ? "" : vwapSlope > 0 ? "UP" : vwapSlope < 0 ? "DOWN" : "FLAT"; + const macdHistVal = macd?.hist ?? null; + dryRun.tick({ + slug: marketSlugNow, + priceToBeat, + btcPrice: currentPrice, + signalSide, + entryPrice, + dataValues: [ + new Date().toISOString(), + marketSlugNow, + timeLeftMin !== null ? timeLeftMin.toFixed(3) : "", + currentPrice !== null ? currentPrice.toFixed(2) : "", + marketUp !== null ? marketUp.toFixed(4) : "", + marketDown !== null ? marketDown.toFixed(4) : "", + regimeInfo.regime, + signal, + timeAware.adjustedUp !== null ? timeAware.adjustedUp.toFixed(4) : "", + timeAware.adjustedDown !== null ? timeAware.adjustedDown.toFixed(4) : "", + edge.edgeUp !== null ? edge.edgeUp.toFixed(4) : "", + edge.edgeDown !== null ? edge.edgeDown.toFixed(4) : "", + rec.action === "ENTER" ? `${rec.side}:${rec.phase}:${rec.strength}` : "NO_TRADE", + rsiNow !== null ? rsiNow.toFixed(1) : "", + rsiSlope !== null ? rsiSlope.toFixed(4) : "", + macdHistVal !== null ? macdHistVal.toFixed(6) : "", + macdLabel, + consec.color ?? "", + consec.count, + vwapNow !== null ? vwapNow.toFixed(0) : "", + vwapDist !== null ? (vwapDist * 100).toFixed(4) : "", + vwapSlope !== null ? vwapSlope.toFixed(6) : "", + ], + }); + } } catch (err) { console.log("────────────────────────────"); console.log(`Error: ${err?.message ?? String(err)}`); diff --git a/src/index5m.js b/src/index5m.js index 8cb4aaed..d4653aad 100644 --- a/src/index5m.js +++ b/src/index5m.js @@ -30,6 +30,7 @@ import { setupKeyboard } from "./trading/keyboard.js"; import { processActionQueue } from "./trading/executor.js"; import { createPriceLatch } from "./trading/priceLatch.js"; import { createTradeTracker } from "./trading/tracker.js"; +import { createDryRunLogger5m } from "./dryRun.js"; applyGlobalProxyFromEnv(); @@ -74,6 +75,8 @@ async function main() { const tracker = createTradeTracker(); const dumpedMarkets = new Set(); + const dryRun = createDryRunLogger5m("./logs/dryrun_5m.csv"); + process.on("exit", () => dryRun.flushNow()); let prevSpotPrice = null; let prevCurrentPrice = null; @@ -336,6 +339,46 @@ async function main() { "", // outcome "", // pnl ]); + + // ── Dry-run study log ───────────────────────────────────────────────── + { + const signalSide = rec.action === "ENTER" ? rec.side : null; + const entryPrice = signalSide === "UP" ? marketUp : signalSide === "DOWN" ? marketDown : null; + const vwapSlopeLbl = vwapSlope === null ? "" : vwapSlope > 0 ? "UP" : vwapSlope < 0 ? "DOWN" : "FLAT"; + dryRun.tick({ + slug: marketSlugNow, + priceToBeat, + btcPrice: currentPrice, + signalSide, + entryPrice, + dataValues: [ + new Date().toISOString(), + marketSlugNow, + timeLeftMin !== null ? timeLeftMin.toFixed(3) : "", + currentPrice !== null ? currentPrice.toFixed(2) : "", + marketUp !== null ? marketUp.toFixed(4) : "", + marketDown !== null ? marketDown.toFixed(4) : "", + signal, + timeAware.adjustedUp !== null ? timeAware.adjustedUp.toFixed(4) : "", + timeAware.adjustedDown !== null ? timeAware.adjustedDown.toFixed(4) : "", + edge.edgeUp !== null ? edge.edgeUp.toFixed(4) : "", + edge.edgeDown !== null ? edge.edgeDown.toFixed(4) : "", + rec.action === "ENTER" ? `${rec.side}:${rec.phase}:${rec.strength}` : "NO_TRADE", + ofiData.ofi30s?.ofi !== undefined ? ofiData.ofi30s.ofi.toFixed(3) : "", + ofiData.ofi1m?.ofi !== undefined ? ofiData.ofi1m.ofi.toFixed(3) : "", + ofiData.ofi2m?.ofi !== undefined ? ofiData.ofi2m.ofi.toFixed(3) : "", + momentum?.roc1 !== null && momentum?.roc1 !== undefined ? momentum.roc1.toFixed(6) : "", + momentum?.roc3 !== null && momentum?.roc3 !== undefined ? momentum.roc3.toFixed(6) : "", + emaCross?.crossover ?? "", + rsiNow !== null ? rsiNow.toFixed(1) : "", + consec.color ?? "", + consec.count, + vwapNow !== null ? vwapNow.toFixed(0) : "", + vwapDist !== null ? (vwapDist * 100).toFixed(4) : "", + vwapSlope !== null ? vwapSlope.toFixed(6) : "", + ], + }); + } } catch (err) { console.log("────────────────────────────"); console.log(`Error: ${err?.message ?? String(err)}`); From efbb165edd6bd47c93e666e876aeb7b96081f5fd Mon Sep 17 00:00:00 2001 From: Jean Carraro Date: Tue, 7 Apr 2026 08:49:58 -0300 Subject: [PATCH 11/49] Refine signal strategy based on dry-run analysis MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 15m: reduce VWAP weight from +2 to +1 (too laggy over 240 candles), remove one-sided failedVwapReclaim bias, add indicator conflict detection — when HA + MACD + RSI majority disagrees with VWAP direction, suppress signal instead of entering against momentum. 5m: add HA + OFI alignment filter (both must disagree to suppress), raise MID/LATE thresholds to avoid low-conviction late entries, add 30s signal cooldown to prevent flip-flop (stable signals showed 62.5% win rate vs 40% for unstable). Co-Authored-By: Claude Opus 4.6 --- src/engines/edge.js | 7 ++++++- src/engines/edge5m.js | 20 ++++++++++++++----- src/engines/probability.js | 39 +++++++++++++++++++++++++++++++------- src/index.js | 6 +----- src/index5m.js | 17 ++++++++++++++++- 5 files changed, 70 insertions(+), 19 deletions(-) diff --git a/src/engines/edge.js b/src/engines/edge.js index f9b30b6a..feaa8764 100644 --- a/src/engines/edge.js +++ b/src/engines/edge.js @@ -20,7 +20,7 @@ export function computeEdge({ modelUp, modelDown, marketYes, marketNo }) { }; } -export function decide({ remainingMinutes, edgeUp, edgeDown, modelUp = null, modelDown = null }) { +export function decide({ remainingMinutes, edgeUp, edgeDown, modelUp = null, modelDown = null, conflicted = false }) { const phase = remainingMinutes > 10 ? "EARLY" : remainingMinutes > 5 ? "MID" : "LATE"; const threshold = phase === "EARLY" ? 0.05 : phase === "MID" ? 0.1 : 0.2; @@ -31,6 +31,11 @@ export function decide({ remainingMinutes, edgeUp, edgeDown, modelUp = null, mod return { action: "NO_TRADE", side: null, phase, reason: "missing_market_data" }; } + // Indicator conflict: HA + MACD + RSI majority disagrees with VWAP direction + if (conflicted) { + return { action: "NO_TRADE", side: null, phase, reason: "indicator_conflict" }; + } + const bestSide = edgeUp > edgeDown ? "UP" : "DOWN"; const bestEdge = bestSide === "UP" ? edgeUp : edgeDown; const bestModel = bestSide === "UP" ? modelUp : modelDown; diff --git a/src/engines/edge5m.js b/src/engines/edge5m.js index bed9936b..977c16d4 100644 --- a/src/engines/edge5m.js +++ b/src/engines/edge5m.js @@ -1,16 +1,14 @@ // Edge detection and decision engine for 5m mode. // Reuses computeEdge from edge.js — only the decision thresholds change. -import { clamp } from "../utils.js"; - export { computeEdge } from "./edge.js"; -export function decide5m({ remainingMinutes, edgeUp, edgeDown, modelUp = null, modelDown = null }) { +export function decide5m({ remainingMinutes, edgeUp, edgeDown, modelUp = null, modelDown = null, heikenColor = null, ofi1m = null }) { // Phases tuned for 5-minute window const phase = remainingMinutes > 3 ? "EARLY" : remainingMinutes > 1.5 ? "MID" : "LATE"; - const threshold = phase === "EARLY" ? 0.04 : phase === "MID" ? 0.08 : 0.15; - const minProb = phase === "EARLY" ? 0.54 : phase === "MID" ? 0.58 : 0.62; + const threshold = phase === "EARLY" ? 0.04 : phase === "MID" ? 0.12 : 0.25; + const minProb = phase === "EARLY" ? 0.54 : phase === "MID" ? 0.62 : 0.70; if (edgeUp === null || edgeDown === null) { return { action: "NO_TRADE", side: null, phase, reason: "missing_market_data" }; @@ -20,6 +18,18 @@ export function decide5m({ remainingMinutes, edgeUp, edgeDown, modelUp = null, m const bestEdge = bestSide === "UP" ? edgeUp : edgeDown; const bestModel = bestSide === "UP" ? modelUp : modelDown; + // Alignment filter: if BOTH HA and OFI disagree with direction, reject. + // This catches the low-conviction flip-flop signals that lose money. + if (heikenColor !== null && ofi1m !== null) { + const haAgainst = (bestSide === "UP" && heikenColor === "red") || + (bestSide === "DOWN" && heikenColor === "green"); + const ofiAgainst = (bestSide === "UP" && ofi1m < -0.05) || + (bestSide === "DOWN" && ofi1m > 0.05); + if (haAgainst && ofiAgainst) { + return { action: "NO_TRADE", side: null, phase, reason: "alignment_conflict" }; + } + } + if (bestEdge < threshold) { return { action: "NO_TRADE", side: null, phase, reason: `edge_below_${threshold}` }; } diff --git a/src/engines/probability.js b/src/engines/probability.js index 3e729711..1b6f2e25 100644 --- a/src/engines/probability.js +++ b/src/engines/probability.js @@ -10,27 +10,30 @@ export function scoreDirection(inputs) { macd, heikenColor, heikenCount, - failedVwapReclaim } = inputs; let up = 1; let down = 1; + // VWAP position — weight 1 (reduced from 2: too laggy over 240 candles) if (price !== null && vwap !== null) { - if (price > vwap) up += 2; - if (price < vwap) down += 2; + if (price > vwap) up += 1; + if (price < vwap) down += 1; } + // VWAP slope — weight 1 (reduced from 2) if (vwapSlope !== null) { - if (vwapSlope > 0) up += 2; - if (vwapSlope < 0) down += 2; + if (vwapSlope > 0) up += 1; + if (vwapSlope < 0) down += 1; } + // RSI — weight 2 if (rsi !== null && rsiSlope !== null) { if (rsi > 55 && rsiSlope > 0) up += 2; if (rsi < 45 && rsiSlope < 0) down += 2; } + // MACD — weight 2 (expanding histogram) + 1 (line direction) if (macd?.hist !== null && macd?.histDelta !== null) { const expandingGreen = macd.hist > 0 && macd.histDelta > 0; const expandingRed = macd.hist < 0 && macd.histDelta < 0; @@ -41,15 +44,37 @@ export function scoreDirection(inputs) { if (macd.macd < 0) down += 1; } + // Heiken Ashi — weight 1 if (heikenColor) { if (heikenColor === "green" && heikenCount >= 2) up += 1; if (heikenColor === "red" && heikenCount >= 2) down += 1; } - if (failedVwapReclaim === true) down += 3; + // ── Conflict detection ──────────────────────────────────────────────── + // Count non-VWAP indicator directions. If the majority disagree with + // the tentative scoring direction, flag as conflicted → decide() will + // reject the trade instead of entering against the indicators. + let indicatorUp = 0; + let indicatorDown = 0; + + if (heikenColor === "green") indicatorUp += 1; + if (heikenColor === "red") indicatorDown += 1; + + if (macd?.hist > 0) indicatorUp += 1; + if (macd?.hist < 0) indicatorDown += 1; + + if (rsi !== null) { + if (rsi > 50) indicatorUp += 1; + if (rsi < 50) indicatorDown += 1; + } const rawUp = up / (up + down); - return { upScore: up, downScore: down, rawUp }; + const tentativeSide = rawUp >= 0.5 ? "UP" : "DOWN"; + const conflicted = + (tentativeSide === "DOWN" && indicatorUp >= 2 && indicatorDown <= 1) || + (tentativeSide === "UP" && indicatorDown >= 2 && indicatorUp <= 1); + + return { upScore: up, downScore: down, rawUp, conflicted }; } export function applyTimeAwareness(rawUp, remainingMinutes, windowMinutes) { diff --git a/src/index.js b/src/index.js index c35520ea..2127fc5d 100644 --- a/src/index.js +++ b/src/index.js @@ -142,9 +142,6 @@ async function main() { const vwapCrossCount = countVwapCrosses(closes, vwapSeries, 20); const volumeRecent = candles.slice(-20).reduce((a, c) => a + c.volume, 0); const volumeAvg = candles.slice(-120).reduce((a, c) => a + c.volume, 0) / 6; - const failedVwapReclaim = vwapNow !== null && vwapSeries.length >= 3 - ? closes[closes.length - 1] < vwapNow && closes[closes.length - 2] > vwapSeries[vwapSeries.length - 2] - : false; // ── Signal ──────────────────────────────────────────────────────────── const regimeInfo = detectRegime({ price: lastPrice, vwap: vwapNow, vwapSlope, vwapCrossCount, volumeRecent, volumeAvg }); @@ -153,14 +150,13 @@ async function main() { price: lastPrice, vwap: vwapNow, vwapSlope, rsi: rsiNow, rsiSlope, macd, heikenColor: consec.color, heikenCount: consec.count, - failedVwapReclaim, }); const timeAware = applyTimeAwareness(scored.rawUp, timeLeftMin, CONFIG.candleWindowMinutes); const marketUp = poly.ok ? poly.prices.up : null; const marketDown = poly.ok ? poly.prices.down : null; const edge = computeEdge({ modelUp: timeAware.adjustedUp, modelDown: timeAware.adjustedDown, marketYes: marketUp, marketNo: marketDown }); - const rec = decide({ remainingMinutes: timeLeftMin, edgeUp: edge.edgeUp, edgeDown: edge.edgeDown, modelUp: timeAware.adjustedUp, modelDown: timeAware.adjustedDown }); + const rec = decide({ remainingMinutes: timeLeftMin, edgeUp: edge.edgeUp, edgeDown: edge.edgeDown, modelUp: timeAware.adjustedUp, modelDown: timeAware.adjustedDown, conflicted: scored.conflicted }); // ── Trading ─────────────────────────────────────────────────────────── const marketSlugNow = poly.ok ? String(poly.market?.slug ?? "") : ""; diff --git a/src/index5m.js b/src/index5m.js index d4653aad..2de7b16c 100644 --- a/src/index5m.js +++ b/src/index5m.js @@ -78,6 +78,7 @@ async function main() { const dryRun = createDryRunLogger5m("./logs/dryrun_5m.csv"); process.on("exit", () => dryRun.flushNow()); + let signalCooldown = { side: null, ts: 0, slug: null }; let prevSpotPrice = null; let prevCurrentPrice = null; let usdcBalance = null; @@ -151,7 +152,21 @@ async function main() { const marketUp = poly.ok ? poly.prices.up : null; const marketDown = poly.ok ? poly.prices.down : null; const edge = computeEdge({ modelUp: timeAware.adjustedUp, modelDown: timeAware.adjustedDown, marketYes: marketUp, marketNo: marketDown }); - const rec = decide5m({ remainingMinutes: timeLeftMin, edgeUp: edge.edgeUp, edgeDown: edge.edgeDown, modelUp: timeAware.adjustedUp, modelDown: timeAware.adjustedDown }); + const ofi1mVal = ofiData.ofi1m?.ofi ?? null; + let rec = decide5m({ remainingMinutes: timeLeftMin, edgeUp: edge.edgeUp, edgeDown: edge.edgeDown, modelUp: timeAware.adjustedUp, modelDown: timeAware.adjustedDown, heikenColor: consec.color, ofi1m: ofi1mVal }); + + // ── Signal cooldown (prevent flip-flop) ─────────────────────────────── + if (rec.action === "ENTER") { + if (signalCooldown.slug !== marketSlugNow) { + signalCooldown = { side: null, ts: 0, slug: marketSlugNow }; + } + if (signalCooldown.side !== null && signalCooldown.side !== rec.side && Date.now() - signalCooldown.ts < 30_000) { + rec = { action: "NO_TRADE", side: null, phase: rec.phase, reason: "cooldown" }; + } + if (rec.action === "ENTER") { + signalCooldown = { side: rec.side, ts: Date.now(), slug: marketSlugNow }; + } + } // ── Trading ─────────────────────────────────────────────────────────── const marketSlugNow = poly.ok ? String(poly.market?.slug ?? "") : ""; From ae3d47172216054d442de18d2174d9435a64dfe1 Mon Sep 17 00:00:00 2001 From: Jean Carraro Date: Tue, 7 Apr 2026 08:52:46 -0300 Subject: [PATCH 12/49] Fix: move marketSlugNow declaration before cooldown block Co-Authored-By: Claude Sonnet 4.6 --- src/index5m.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/index5m.js b/src/index5m.js index 2de7b16c..d3d5c6ef 100644 --- a/src/index5m.js +++ b/src/index5m.js @@ -155,6 +155,9 @@ async function main() { const ofi1mVal = ofiData.ofi1m?.ofi ?? null; let rec = decide5m({ remainingMinutes: timeLeftMin, edgeUp: edge.edgeUp, edgeDown: edge.edgeDown, modelUp: timeAware.adjustedUp, modelDown: timeAware.adjustedDown, heikenColor: consec.color, ofi1m: ofi1mVal }); + // ── Trading ─────────────────────────────────────────────────────────── + const marketSlugNow = poly.ok ? String(poly.market?.slug ?? "") : ""; + // ── Signal cooldown (prevent flip-flop) ─────────────────────────────── if (rec.action === "ENTER") { if (signalCooldown.slug !== marketSlugNow) { @@ -167,9 +170,6 @@ async function main() { signalCooldown = { side: rec.side, ts: Date.now(), slug: marketSlugNow }; } } - - // ── Trading ─────────────────────────────────────────────────────────── - const marketSlugNow = poly.ok ? String(poly.market?.slug ?? "") : ""; resetIfMarketChanged(marketSlugNow); await processActionQueue(keyboard.actionQueue, { trading, poly, rec, timeAware, marketSlugNow }); From f61efe5ca9604c7bb78d88d35c7560112f1b8afc Mon Sep 17 00:00:00 2001 From: Jean Carraro Date: Tue, 7 Apr 2026 17:19:23 -0300 Subject: [PATCH 13/49] Add staleness watchdog to all WebSocket streams Force reconnect when no message arrives within threshold: - Binance trade / OFI: 30s (trades arrive every ~100ms) - Polymarket live WS: 60s (Chainlink updates every few seconds) - Chainlink on-chain WS: 120s (on-chain events less frequent) Fixes zombie connections that stay "open" but stop delivering data, common on WSL2 and through proxies during long-running sessions. Co-Authored-By: Claude Opus 4.6 --- src/data/binanceWs.js | 13 +++++++++++++ src/data/binanceWsOfi.js | 13 +++++++++++++ src/data/chainlinkWs.js | 13 +++++++++++++ src/data/polymarketLiveWs.js | 13 +++++++++++++ 4 files changed, 52 insertions(+) diff --git a/src/data/binanceWs.js b/src/data/binanceWs.js index aa4d8d05..5823fec2 100644 --- a/src/data/binanceWs.js +++ b/src/data/binanceWs.js @@ -12,24 +12,35 @@ function buildWsUrl(symbol) { return `wss://stream.binance.com:9443/ws/${s}@trade`; } +const STALE_MS = 30_000; // force reconnect if no message in 30s +const CHECK_MS = 10_000; + export function startBinanceTradeStream({ symbol = CONFIG.symbol, onUpdate } = {}) { let ws = null; let closed = false; let reconnectMs = 500; let lastPrice = null; let lastTs = null; + let lastMessageAt = 0; + let watchdog = null; const connect = () => { if (closed) return; + if (watchdog) { clearInterval(watchdog); watchdog = null; } const url = buildWsUrl(symbol); ws = new WebSocket(url, { agent: wsAgentForUrl(url) }); ws.on("open", () => { reconnectMs = 500; + lastMessageAt = Date.now(); + watchdog = setInterval(() => { + if (Date.now() - lastMessageAt > STALE_MS) scheduleReconnect(); + }, CHECK_MS); }); ws.on("message", (buf) => { + lastMessageAt = Date.now(); try { const msg = JSON.parse(buf.toString()); const p = toNumber(msg.p); @@ -44,6 +55,7 @@ export function startBinanceTradeStream({ symbol = CONFIG.symbol, onUpdate } = { const scheduleReconnect = () => { if (closed) return; + if (watchdog) { clearInterval(watchdog); watchdog = null; } try { ws?.terminate(); } catch { @@ -67,6 +79,7 @@ export function startBinanceTradeStream({ symbol = CONFIG.symbol, onUpdate } = { }, close() { closed = true; + if (watchdog) { clearInterval(watchdog); watchdog = null; } try { ws?.close(); } catch { diff --git a/src/data/binanceWsOfi.js b/src/data/binanceWsOfi.js index 1c5ea569..fbb391b2 100644 --- a/src/data/binanceWsOfi.js +++ b/src/data/binanceWsOfi.js @@ -15,12 +15,17 @@ function buildWsUrl(symbol) { return `wss://stream.binance.com:9443/ws/${s}@trade`; } +const STALE_MS = 30_000; +const CHECK_MS = 10_000; + export function startBinanceOfiStream({ symbol = CONFIG.symbol } = {}) { let ws = null; let closed = false; let reconnectMs = 500; let lastPrice = null; let lastTs = null; + let lastMessageAt = 0; + let watchdog = null; // Ring buffer of recent trades: { price, qty, isBuyerMaker, ts } const trades = []; @@ -59,15 +64,21 @@ export function startBinanceOfiStream({ symbol = CONFIG.symbol } = {}) { const connect = () => { if (closed) return; + if (watchdog) { clearInterval(watchdog); watchdog = null; } const url = buildWsUrl(symbol); ws = new WebSocket(url, { agent: wsAgentForUrl(url) }); ws.on("open", () => { reconnectMs = 500; + lastMessageAt = Date.now(); + watchdog = setInterval(() => { + if (Date.now() - lastMessageAt > STALE_MS) scheduleReconnect(); + }, CHECK_MS); }); ws.on("message", (buf) => { + lastMessageAt = Date.now(); try { const msg = JSON.parse(buf.toString()); const p = toNumber(msg.p); @@ -93,6 +104,7 @@ export function startBinanceOfiStream({ symbol = CONFIG.symbol } = {}) { const scheduleReconnect = () => { if (closed) return; + if (watchdog) { clearInterval(watchdog); watchdog = null; } try { ws?.terminate(); } catch { /* ignore */ } ws = null; const wait = reconnectMs; @@ -123,6 +135,7 @@ export function startBinanceOfiStream({ symbol = CONFIG.symbol } = {}) { }, close() { closed = true; + if (watchdog) { clearInterval(watchdog); watchdog = null; } try { ws?.close(); } catch { /* ignore */ } ws = null; } diff --git a/src/data/chainlinkWs.js b/src/data/chainlinkWs.js index 8a64690a..40e3d12e 100644 --- a/src/data/chainlinkWs.js +++ b/src/data/chainlinkWs.js @@ -24,6 +24,9 @@ function toNumber(x) { return Number.isFinite(n) ? n : null; } +const STALE_MS = 120_000; // on-chain events are less frequent +const CHECK_MS = 15_000; + export function startChainlinkPriceStream({ aggregator = CONFIG.chainlink.btcUsdAggregator, decimals = 8, @@ -43,6 +46,8 @@ export function startChainlinkPriceStream({ let closed = false; let reconnectMs = 500; let urlIndex = 0; + let lastMessageAt = 0; + let watchdog = null; let lastPrice = null; let lastUpdatedAt = null; @@ -52,6 +57,7 @@ export function startChainlinkPriceStream({ const connect = () => { if (closed) return; + if (watchdog) { clearInterval(watchdog); watchdog = null; } const url = wssUrls[urlIndex % wssUrls.length]; urlIndex += 1; @@ -68,6 +74,7 @@ export function startChainlinkPriceStream({ const scheduleReconnect = () => { if (closed) return; + if (watchdog) { clearInterval(watchdog); watchdog = null; } try { ws?.terminate(); } catch { @@ -82,6 +89,10 @@ export function startChainlinkPriceStream({ ws.on("open", () => { reconnectMs = 500; + lastMessageAt = Date.now(); + watchdog = setInterval(() => { + if (Date.now() - lastMessageAt > STALE_MS) scheduleReconnect(); + }, CHECK_MS); const id = nextId++; send({ jsonrpc: "2.0", @@ -98,6 +109,7 @@ export function startChainlinkPriceStream({ }); ws.on("message", (buf) => { + lastMessageAt = Date.now(); let msg; try { msg = JSON.parse(buf.toString()); @@ -147,6 +159,7 @@ export function startChainlinkPriceStream({ }, close() { closed = true; + if (watchdog) { clearInterval(watchdog); watchdog = null; } try { if (ws && subId) { ws.send(JSON.stringify({ jsonrpc: "2.0", id: nextId++, method: "eth_unsubscribe", params: [subId] })); diff --git a/src/data/polymarketLiveWs.js b/src/data/polymarketLiveWs.js index 58a44031..8ef3ef99 100644 --- a/src/data/polymarketLiveWs.js +++ b/src/data/polymarketLiveWs.js @@ -22,6 +22,9 @@ function toFiniteNumber(x) { return Number.isFinite(n) ? n : null; } +const STALE_MS = 60_000; // Chainlink updates every few seconds +const CHECK_MS = 10_000; + export function startPolymarketChainlinkPriceStream({ wsUrl = CONFIG.polymarket.liveDataWsUrl, symbolIncludes = "btc", @@ -39,12 +42,15 @@ export function startPolymarketChainlinkPriceStream({ let ws = null; let closed = false; let reconnectMs = 500; + let lastMessageAt = 0; + let watchdog = null; let lastPrice = null; let lastUpdatedAt = null; const connect = () => { if (closed) return; + if (watchdog) { clearInterval(watchdog); watchdog = null; } ws = new WebSocket(wsUrl, { handshakeTimeout: 10_000, @@ -53,6 +59,7 @@ export function startPolymarketChainlinkPriceStream({ const scheduleReconnect = () => { if (closed) return; + if (watchdog) { clearInterval(watchdog); watchdog = null; } try { ws?.terminate(); } catch { @@ -66,6 +73,10 @@ export function startPolymarketChainlinkPriceStream({ ws.on("open", () => { reconnectMs = 500; + lastMessageAt = Date.now(); + watchdog = setInterval(() => { + if (Date.now() - lastMessageAt > STALE_MS) scheduleReconnect(); + }, CHECK_MS); try { ws.send( JSON.stringify({ @@ -79,6 +90,7 @@ export function startPolymarketChainlinkPriceStream({ }); ws.on("message", (buf) => { + lastMessageAt = Date.now(); const msg = typeof buf === "string" ? buf : buf?.toString?.() ?? ""; if (!msg || !msg.trim()) return; @@ -118,6 +130,7 @@ export function startPolymarketChainlinkPriceStream({ }, close() { closed = true; + if (watchdog) { clearInterval(watchdog); watchdog = null; } try { ws?.close(); } catch { From 07a4ae2ef6c4c457f4c2a77941e905698e8cb05b Mon Sep 17 00:00:00 2001 From: Jean Carraro Date: Tue, 7 Apr 2026 18:02:58 -0300 Subject: [PATCH 14/49] Restructure dry-run as paper-trading simulator with position tracking Replace the passive tick logger with an active trading simulator that opens/closes virtual positions using the same exit logic as real trading (TP, SL, signal flip, time decay). Adds a per-trade journal CSV for easy performance analysis. Co-Authored-By: Claude Opus 4.6 --- CLAUDE.md | 58 ++++++--- IDEAS.md | 36 ++++++ src/dryRun.js | 329 +++++++++++++++++++++++++++++++++++++++++-------- src/index.js | 19 +-- src/index5m.js | 17 +-- 5 files changed, 374 insertions(+), 85 deletions(-) create mode 100644 IDEAS.md diff --git a/CLAUDE.md b/CLAUDE.md index 1ed26e48..5ee1f405 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -104,30 +104,58 @@ Called once at startup via `applyGlobalProxyFromEnv()`. Reads `HTTPS_PROXY`/`HTT - Terminal screen refreshed every second via ANSI escape codes (`\x1b[H` + per-line `\x1b[K` + `\x1b[J`), rendered inside an alternate screen buffer. - `./logs/signals.csv` — one row per poll tick (15m mode) with regime, signal, model probabilities, market prices, edge, and recommendation. - `./logs/signals_5m.csv` — one row per poll tick (5m mode) with OFI, momentum, EMA cross, RSI, model probs, edge, and recommendation. -- `./logs/dryrun_15m.csv` — dry-run study log for the 15m app (see below). -- `./logs/dryrun_5m.csv` — dry-run study log for the 5m app (see below). +- `./logs/dryrun_15m.csv` — paper-trading tick-by-tick log for the 15m app (see below). +- `./logs/dryrun_5m.csv` — paper-trading tick-by-tick log for the 5m app (see below). +- `./logs/dryrun_15m_trades.csv` — per-trade journal (one row per completed trade) for 15m. +- `./logs/dryrun_5m_trades.csv` — per-trade journal (one row per completed trade) for 5m. - `./logs/polymarket_market_.json` — raw Polymarket market JSON dumped once per new market slug. -### Dry-run study logger (`src/dryRun.js`) +### Paper-trading simulator (`src/dryRun.js`) -Enabled automatically in both apps — no extra flags needed. Each app creates one logger instance at startup: +Enabled automatically in both apps — no extra flags needed. Each app creates one simulator at startup: ``` -createDryRunLogger15m("./logs/dryrun_15m.csv") // used by index.js -createDryRunLogger5m("./logs/dryrun_5m.csv") // used by index5m.js +createDryRunSimulator15m("./logs/dryrun_15m.csv", CONFIG.trading) // used by index.js +createDryRunSimulator5m("./logs/dryrun_5m.csv", CONFIG.trading) // used by index5m.js ``` -**How it works:** ticks accumulate in memory (one object per poll second) keyed to the current market slug. When the slug changes (market settled), the entire buffer is flushed to CSV in one `appendFileSync` call with four outcome columns filled in retroactively: +**How it works:** the simulator maintains a virtual position and mirrors real trading logic: -| Column | Description | +1. **BUY** — when the bot emits an `ENTER` signal and no virtual position is open, it simulates a buy at the current market price (using `CONFIG.trading.tradeAmount` as the virtual investment). +2. **HOLD** — while a position is open, each tick evaluates exit conditions using the same `evaluateExit` logic as real trading (take profit, stop loss, signal flip, time decay). +3. **SELL** — when an exit condition triggers, the position is closed at the current market price. ROI and PNL are recorded. +4. **SETTLEMENT** — when the market slug changes (settlement), any open position resolves at $1 (if the held side won) or $0 (if it lost). + +After selling, the simulator can re-enter on a new signal within the same market. + +**Exit conditions** (same as real trading): +| Condition | Trigger | |---|---| -| `outcome` | `UP` or `DOWN` — which side actually won (BTC above/below open price) | -| `btc_at_settlement` | Final Chainlink BTC/USD price at the moment of flush | -| `direction_correct` | `1` if signal matched outcome, `0` if not, empty if `NO TRADE` | -| `signal_roi` | Hypothetical ROI if you had entered at that tick's market price. E.g. BUY UP at 0.65¢ → WIN: `+0.5385`; LOSS: `-1.0000`. Empty for `NO TRADE`. | +| `TAKE_PROFIT` | ROI ≥ `takeProfitPct` AND model confirms reversal | +| `STOP_LOSS` | ROI ≤ `-stopLossPct` AND model confirms reversal | +| `SIGNAL_FLIP` | Model opposite-side probability ≥ `signalFlipMinProb` | +| `TIME_DECAY` | < 1.5 min left, losing > 5%, entry was ≥ 50¢ | +| `SETTLED_WIN` / `SETTLED_LOSS` | Market ended, position resolved | -`process.on("exit")` flushes any in-progress market buffer so data is not lost on Ctrl+C / Q. +**Output files:** -**15m columns:** `timestamp, market_slug, time_left_min, btc_price, market_up, market_down, regime, signal, model_up, model_down, edge_up, edge_down, rec_detail, rsi, rsi_slope, macd_hist, macd_label, ha_color, ha_count, vwap, vwap_dist_pct, vwap_slope, outcome, btc_at_settlement, direction_correct, signal_roi` +The tick CSV logs every second with all indicators + simulation state. The trades CSV logs one row per completed trade for easy analysis. -**5m columns:** `timestamp, market_slug, time_left_min, btc_price, market_up, market_down, signal, model_up, model_down, edge_up, edge_down, rec_detail, ofi_30s, ofi_1m, ofi_2m, roc1, roc3, ema_cross, rsi, ha_color, ha_count, vwap, vwap_dist_pct, vwap_slope, outcome, btc_at_settlement, direction_correct, signal_roi` +**Tick CSV simulation columns:** + +| Column | Description | +|---|---| +| `sim_action` | `WAIT` (no position, no signal), `BUY`, `HOLD`, `SELL` | +| `sim_side` | `UP` or `DOWN` — which side is held | +| `sim_entry_price` | Price at which the virtual position was opened | +| `sim_current_price` | Current market price of the held side | +| `sim_roi_pct` | Current ROI % of the open position (or final ROI on SELL) | +| `sim_exit_reason` | Exit reason on SELL rows (TP, SL, FLIP, TIME_DECAY, SETTLED) | +| `sim_pnl` | Realized PNL in virtual USD (only on SELL rows) | +| `sim_cum_pnl` | Running cumulative PNL across all trades | +| `outcome` | `UP` or `DOWN` — which side actually won (retroactive) | +| `btc_at_settlement` | Final Chainlink BTC/USD price (retroactive) | + +**Trades CSV columns:** `entry_time, exit_time, market_slug, side, entry_price, exit_price, shares, invested, exit_value, pnl, roi_pct, exit_reason, duration_s` + +`process.on("exit")` flushes any in-progress market (settles open position) so data is not lost on Ctrl+C / Q. diff --git a/IDEAS.md b/IDEAS.md new file mode 100644 index 00000000..4a020004 --- /dev/null +++ b/IDEAS.md @@ -0,0 +1,36 @@ +# Ideas & TODOs + +## Trading API Layer (shared across bots) + +Expor o trading layer atual como um servidor HTTP local (Express) com endpoints REST: + +``` +POST /order/buy { tokenId, amount, price } +POST /order/sell { tokenId, amount, price } +GET /balance +GET /position/:tokenId +``` + +**Motivação:** permite criar novos bots em Python (ou qualquer linguagem) sem reimplementar +autenticação EIP-712, L2 HMAC, GnosisSafe detection, order slippage logic — que já estão +testados e funcionando em `src/trading/`. + +**Caso de uso imediato:** weather bot em Python que consome modelos de previsão do tempo +(Open-Meteo ensemble: GFS, ECMWF, ICON), calcula edge vs preços da Polymarket, e delega +execução de ordens para este servidor JS. + +--- + +## Weather Bot (Python) + +Bot separado (novo repositório Python) para apostar em mercados de clima na Polymarket. + +**Fontes de forecast gratuitas:** +- Open-Meteo — 15+ modelos ensemble, horário, global, sem API key +- NOAA/NWS API — probabilistic forecasts para EUA + +**Fluxo:** +1. Consumir Open-Meteo ensemble → distribuição de probabilidade para o evento +2. Puxar mercados de weather da Polymarket (Gamma API) +3. Calcular edge (modelo prob vs preço Polymarket) +4. Executar via Trading API Layer acima (ou `py-clob-client` diretamente) diff --git a/src/dryRun.js b/src/dryRun.js index e433a214..de53eb23 100644 --- a/src/dryRun.js +++ b/src/dryRun.js @@ -1,44 +1,63 @@ /** - * Dry-run study logger. + * Paper-trading simulator (dry-run v2). * - * Accumulates per-tick snapshots in memory for the current market. When the - * market slug changes (settlement), flushes the buffer to CSV with outcome - * columns filled in retroactively: + * Simulates real trading: enters on first ENTER signal, tracks the position, + * evaluates exits using the same TP/SL/signal-flip/time-decay logic as real + * trading, and logs every tick with full position + simulation state. * - * outcome — "UP" or "DOWN" (which side actually won) - * btc_at_settlement — final Chainlink BTC/USD price - * direction_correct — 1 if signal matched outcome, 0 if not, "" if NO TRADE - * signal_roi — hypothetical ROI if entered at that tick's market price + * On market settlement (slug change), open positions resolve at $1 (win) or + * $0 (loss) based on whether the held side matches the actual outcome. + * + * Also writes a per-trade journal to a separate CSV for easy analysis. * * Usage: - * const logger = createDryRunLogger15m("./logs/dryrun_15m.csv"); - * // inside the poll loop (after priceLatch.update): - * logger.tick({ slug, priceToBeat, btcPrice, signalSide, entryPrice, dataValues }); + * const sim = createDryRunSimulator15m("./logs/dryrun_15m.csv", config); + * // inside the poll loop: + * sim.tick({ slug, priceToBeat, btcPrice, rec, modelUp, modelDown, + * marketUp, marketDown, timeLeftMin, dataValues }); * // on process exit: - * logger.flushNow(); + * sim.flushNow(); */ import fs from "node:fs"; import path from "node:path"; import { ensureDir } from "./utils.js"; -export const HEADER_15M = [ +// ── Headers ───────────────────────────────────────────────────────────────── + +const SIM_COLS = [ + "sim_action", "sim_side", "sim_entry_price", "sim_current_price", + "sim_roi_pct", "sim_exit_reason", "sim_pnl", "sim_cum_pnl", +]; + +const OUTCOME_COLS = ["outcome", "btc_at_settlement"]; + +const INDICATOR_COLS_15M = [ "timestamp", "market_slug", "time_left_min", "btc_price", "market_up", "market_down", "regime", "signal", "model_up", "model_down", "edge_up", "edge_down", "rec_detail", "rsi", "rsi_slope", "macd_hist", "macd_label", "ha_color", "ha_count", "vwap", "vwap_dist_pct", "vwap_slope", - "outcome", "btc_at_settlement", "direction_correct", "signal_roi", ]; -export const HEADER_5M = [ +const INDICATOR_COLS_5M = [ "timestamp", "market_slug", "time_left_min", "btc_price", "market_up", "market_down", "signal", "model_up", "model_down", "edge_up", "edge_down", "rec_detail", "ofi_30s", "ofi_1m", "ofi_2m", "roc1", "roc3", "ema_cross", "rsi", "ha_color", "ha_count", "vwap", "vwap_dist_pct", "vwap_slope", - "outcome", "btc_at_settlement", "direction_correct", "signal_roi", ]; +export const HEADER_15M = [...INDICATOR_COLS_15M, ...SIM_COLS, ...OUTCOME_COLS]; +export const HEADER_5M = [...INDICATOR_COLS_5M, ...SIM_COLS, ...OUTCOME_COLS]; + +const TRADE_JOURNAL_HEADER = [ + "entry_time", "exit_time", "market_slug", "side", + "entry_price", "exit_price", "shares", "invested", + "exit_value", "pnl", "roi_pct", "exit_reason", "duration_s", +]; + +// ── CSV helpers ───────────────────────────────────────────────────────────── + function csvEscape(v) { if (v === null || v === undefined) return ""; const s = String(v); @@ -57,44 +76,132 @@ function fmt(v, decimals = 4) { return Number(v).toFixed(decimals); } -function createBuffer(csvPath, header) { +// ── Exit evaluation (inlined to avoid position.js module-level side effects) ─ + +function evaluateSimExit({ pos, modelUp, modelDown, currentMarketPrice, timeLeftMin, config }) { + if (!pos.active || currentMarketPrice == null) { + return { shouldSell: false, reason: null, roiPct: null }; + } + + const currentValue = pos.shares * currentMarketPrice; + const pnlUsdc = currentValue - pos.invested; + const roiPct = (pnlUsdc / pos.invested) * 100; + + const oppositeProb = (modelUp != null && modelDown != null) + ? (pos.side === "UP" ? modelDown : modelUp) + : null; + const modelConfirmsReversal = oppositeProb != null && oppositeProb >= config.signalFlipMinProb; + + // Take profit — only if model also confirms reversal + if (roiPct >= config.takeProfitPct && modelConfirmsReversal) { + return { shouldSell: true, reason: "TAKE_PROFIT", roiPct }; + } + + // Stop loss — only if model also confirms reversal + if (roiPct <= -config.stopLossPct && modelConfirmsReversal) { + return { shouldSell: true, reason: "STOP_LOSS", roiPct }; + } + + // Signal flipped with enough conviction + if (modelConfirmsReversal) { + return { shouldSell: true, reason: "SIGNAL_FLIP", roiPct }; + } + + // Time decay — only for expensive entries (≥ 50¢) + const entryWasCheap = pos.entryPrice < 0.50; + if (timeLeftMin != null && timeLeftMin < 1.5 && roiPct < -5 && !entryWasCheap) { + return { shouldSell: true, reason: "TIME_DECAY", roiPct }; + } + + return { shouldSell: false, reason: null, roiPct }; +} + +// ── Simulator core ────────────────────────────────────────────────────────── + +function createSimulator(csvPath, header, config) { + const tradesPath = csvPath.replace(/\.csv$/, "_trades.csv"); + let currentSlug = null; let lastPriceToBeat = null; let lastBtcPrice = null; - let buffer = []; // { dataValues, signalSide: "UP"|"DOWN"|null, entryPrice: number|null }[] + let buffer = []; // { dataValues[], simCols[] }[] + + // Virtual position + let pos = { active: false, side: null, entryPrice: 0, shares: 0, invested: 0, marketSlug: null, entryTime: null }; + let cumulativePnl = 0; + + function _resetPos() { + pos = { active: false, side: null, entryPrice: 0, shares: 0, invested: 0, marketSlug: null, entryTime: null }; + } + + function _ensureHeader(filePath, headerRow) { + ensureDir(path.dirname(filePath)); + if (!fs.existsSync(filePath)) { + fs.writeFileSync(filePath, headerRow.join(",") + "\n", "utf8"); + } + } + + function _logTrade({ exitPrice, exitValue, pnl, roiPct, reason, exitTime }) { + _ensureHeader(tradesPath, TRADE_JOURNAL_HEADER); + const durationS = pos.entryTime ? Math.round((exitTime - pos.entryTime) / 1000) : ""; + const row = toCsvLine([ + pos.entryTime ? new Date(pos.entryTime).toISOString() : "", + new Date(exitTime).toISOString(), + pos.marketSlug, + pos.side, + fmt(pos.entryPrice, 4), + fmt(exitPrice, 4), + fmt(pos.shares, 4), + fmt(pos.invested, 2), + fmt(exitValue, 4), + fmt(pnl, 4), + fmt(roiPct, 2), + reason, + durationS, + ]); + fs.appendFileSync(tradesPath, row + "\n", "utf8"); + } + + function _settlePosition() { + if (!pos.active) return; - function _ensureHeader() { - ensureDir(path.dirname(csvPath)); - if (!fs.existsSync(csvPath)) { - fs.writeFileSync(csvPath, header.join(",") + "\n", "utf8"); + const ptb = lastPriceToBeat; + const btcFinal = lastBtcPrice; + if (ptb == null || btcFinal == null) { + // Can't determine outcome — force close at last known market price (best effort) + _resetPos(); + return; } + + const outcome = btcFinal > ptb ? "UP" : "DOWN"; + const won = pos.side === outcome; + const resolutionPrice = won ? 1.0 : 0.0; + const exitValue = pos.shares * resolutionPrice; + const pnl = exitValue - pos.invested; + const roiPct = (pnl / pos.invested) * 100; + const reason = won ? "SETTLED_WIN" : "SETTLED_LOSS"; + + cumulativePnl += pnl; + _logTrade({ exitPrice: resolutionPrice, exitValue, pnl, roiPct, reason, exitTime: Date.now() }); + _resetPos(); } function _flush() { if (buffer.length === 0) return; - _ensureHeader(); + _ensureHeader(csvPath, header); const ptb = lastPriceToBeat; const btcFinal = lastBtcPrice; - const canComputeOutcome = ptb !== null && btcFinal !== null; - const outcome = canComputeOutcome ? (btcFinal > ptb ? "UP" : "DOWN") : null; - - const lines = buffer.map(({ dataValues, signalSide, entryPrice }) => { - let correct = ""; - let roi = ""; - if (outcome !== null && signalSide !== null) { - correct = signalSide === outcome ? "1" : "0"; - roi = entryPrice !== null && entryPrice > 0 - ? (signalSide === outcome ? fmt((1 / entryPrice) - 1, 4) : "-1.0000") - : ""; - } + const canCompute = ptb !== null && btcFinal !== null; + const outcome = canCompute ? (btcFinal > ptb ? "UP" : "DOWN") : null; + + const lines = buffer.map(({ dataValues, simCols }) => { return toCsvLine([ ...dataValues, + ...simCols, outcome ?? "", btcFinal !== null ? fmt(btcFinal, 2) : "", - correct, - roi, ]); }); @@ -103,39 +210,155 @@ function createBuffer(csvPath, header) { } /** - * Record one tick. Call after all indicators and priceLatch.update() are computed. + * Record one tick and run the paper-trading simulation. * - * @param {string} slug - current market slug - * @param {number|null} priceToBeat - latched BTC open price (may be null early in market) - * @param {number|null} btcPrice - live Chainlink/Polymarket BTC price - * @param {"UP"|"DOWN"|null} signalSide - direction of the signal (null if NO TRADE) - * @param {number|null} entryPrice - market price for signalSide (null if NO TRADE) - * @param {Array} dataValues - pre-settlement CSV columns (all except outcome cols) + * @param {string} slug - current market slug + * @param {number|null} priceToBeat - latched BTC open price + * @param {number|null} btcPrice - live Chainlink/Polymarket BTC price + * @param {Object} rec - recommendation { action, side, phase, strength } + * @param {number|null} modelUp - time-aware model probability for UP + * @param {number|null} modelDown - time-aware model probability for DOWN + * @param {number|null} marketUp - current market price for UP side + * @param {number|null} marketDown - current market price for DOWN side + * @param {number|null} timeLeftMin - minutes remaining in the market + * @param {Array} dataValues - indicator CSV columns (everything before sim columns) */ - function tick({ slug, priceToBeat, btcPrice, signalSide, entryPrice, dataValues }) { + function tick({ slug, priceToBeat, btcPrice, rec, modelUp, modelDown, marketUp, marketDown, timeLeftMin, dataValues }) { + // ── Market changed → settle open position and flush buffer ────────── if (slug !== currentSlug && currentSlug !== null) { + _settlePosition(); _flush(); } + currentSlug = slug; if (priceToBeat !== null) lastPriceToBeat = priceToBeat; if (btcPrice !== null) lastBtcPrice = btcPrice; - buffer.push({ dataValues, signalSide, entryPrice }); + + // ── Simulation logic ──────────────────────────────────────────────── + let simAction = "WAIT"; + let simSide = ""; + let simEntryPrice = ""; + let simCurrentPrice = ""; + let simRoiPct = ""; + let simExitReason = ""; + let simPnl = ""; + + if (pos.active) { + const currentMktPrice = pos.side === "UP" ? marketUp : marketDown; + + // Evaluate exit + const exitEval = evaluateSimExit({ + pos, modelUp, modelDown, + currentMarketPrice: currentMktPrice, + timeLeftMin, + config, + }); + + if (exitEval.shouldSell && currentMktPrice != null) { + // ── SELL ──────────────────────────────────────────────────────── + const exitValue = pos.shares * currentMktPrice; + const pnl = exitValue - pos.invested; + const roiPct = (pnl / pos.invested) * 100; + + cumulativePnl += pnl; + + simAction = "SELL"; + simSide = pos.side; + simEntryPrice = fmt(pos.entryPrice, 4); + simCurrentPrice = fmt(currentMktPrice, 4); + simRoiPct = fmt(roiPct, 2); + simExitReason = exitEval.reason; + simPnl = fmt(pnl, 4); + + _logTrade({ exitPrice: currentMktPrice, exitValue, pnl, roiPct, reason: exitEval.reason, exitTime: Date.now() }); + _resetPos(); + } else { + // ── HOLD ──────────────────────────────────────────────────────── + simAction = "HOLD"; + simSide = pos.side; + simEntryPrice = fmt(pos.entryPrice, 4); + simCurrentPrice = currentMktPrice != null ? fmt(currentMktPrice, 4) : ""; + simRoiPct = exitEval.roiPct != null ? fmt(exitEval.roiPct, 2) : ""; + } + } else if (rec.action === "ENTER" && rec.side) { + // ── BUY ─────────────────────────────────────────────────────────── + const entryMktPrice = rec.side === "UP" ? marketUp : marketDown; + if (entryMktPrice != null && entryMktPrice > 0) { + const shares = config.tradeAmount / entryMktPrice; + pos = { + active: true, + side: rec.side, + entryPrice: entryMktPrice, + shares, + invested: config.tradeAmount, + marketSlug: slug, + entryTime: Date.now(), + }; + + simAction = "BUY"; + simSide = rec.side; + simEntryPrice = fmt(entryMktPrice, 4); + simCurrentPrice = fmt(entryMktPrice, 4); + simRoiPct = "0.00"; + } + } + + const simCols = [ + simAction, + simSide, + simEntryPrice, + simCurrentPrice, + simRoiPct, + simExitReason, + simPnl, + fmt(cumulativePnl, 4), + ]; + + buffer.push({ dataValues, simCols }); } /** Flush current buffer immediately (call on process exit). */ function flushNow() { + _settlePosition(); _flush(); } - return { tick, flushNow }; + /** Get cumulative stats for display purposes. */ + function getStats() { + return { cumulativePnl, positionActive: pos.active, positionSide: pos.side }; + } + + return { tick, flushNow, getStats }; } -/** Create a dry-run logger for the 15-minute assistant. */ -export function createDryRunLogger15m(csvPath) { - return createBuffer(csvPath, HEADER_15M); +// ── Factory functions ─────────────────────────────────────────────────────── + +/** + * Create a paper-trading simulator for the 15-minute assistant. + * @param {string} csvPath - path for the tick-by-tick CSV + * @param {{ tradeAmount?: number, takeProfitPct?: number, stopLossPct?: number, signalFlipMinProb?: number }} [tradingConfig] + */ +export function createDryRunSimulator15m(csvPath, tradingConfig = {}) { + const config = { + tradeAmount: tradingConfig.tradeAmount ?? 5, + takeProfitPct: tradingConfig.takeProfitPct ?? 20, + stopLossPct: tradingConfig.stopLossPct ?? 25, + signalFlipMinProb: tradingConfig.signalFlipMinProb ?? 0.58, + }; + return createSimulator(csvPath, HEADER_15M, config); } -/** Create a dry-run logger for the 5-minute assistant. */ -export function createDryRunLogger5m(csvPath) { - return createBuffer(csvPath, HEADER_5M); +/** + * Create a paper-trading simulator for the 5-minute assistant. + * @param {string} csvPath - path for the tick-by-tick CSV + * @param {{ tradeAmount?: number, takeProfitPct?: number, stopLossPct?: number, signalFlipMinProb?: number }} [tradingConfig] + */ +export function createDryRunSimulator5m(csvPath, tradingConfig = {}) { + const config = { + tradeAmount: tradingConfig.tradeAmount ?? 5, + takeProfitPct: tradingConfig.takeProfitPct ?? 20, + stopLossPct: tradingConfig.stopLossPct ?? 25, + signalFlipMinProb: tradingConfig.signalFlipMinProb ?? 0.58, + }; + return createSimulator(csvPath, HEADER_5M, config); } diff --git a/src/index.js b/src/index.js index 2127fc5d..d1ccf716 100644 --- a/src/index.js +++ b/src/index.js @@ -29,7 +29,7 @@ import { setupKeyboard } from "./trading/keyboard.js"; import { processActionQueue } from "./trading/executor.js"; import { createPriceLatch } from "./trading/priceLatch.js"; import { createTradeTracker } from "./trading/tracker.js"; -import { createDryRunLogger15m } from "./dryRun.js"; +import { createDryRunSimulator15m } from "./dryRun.js"; applyGlobalProxyFromEnv(); @@ -71,7 +71,7 @@ async function main() { const tracker = createTradeTracker(); const dumpedMarkets = new Set(); - const dryRun = createDryRunLogger15m("./logs/dryrun_15m.csv"); + const dryRun = createDryRunSimulator15m("./logs/dryrun_15m.csv", CONFIG.trading); process.on("exit", () => dryRun.flushNow()); let closedTrades = []; // { side, entryPrice, exitPrice, pnl, roi, ts }[] @@ -312,18 +312,19 @@ async function main() { "", // pnl — filled in the SETTLED row ]); - // ── Dry-run study log ───────────────────────────────────────────────── + // ── Dry-run paper-trading simulator ──────────────────────────────────── { - const signalSide = rec.action === "ENTER" ? rec.side : null; - const entryPrice = signalSide === "UP" ? marketUp : signalSide === "DOWN" ? marketDown : null; - const vwapSlopeLbl = vwapSlope === null ? "" : vwapSlope > 0 ? "UP" : vwapSlope < 0 ? "DOWN" : "FLAT"; - const macdHistVal = macd?.hist ?? null; + const macdHistVal = macd?.hist ?? null; dryRun.tick({ slug: marketSlugNow, priceToBeat, btcPrice: currentPrice, - signalSide, - entryPrice, + rec, + modelUp: timeAware.adjustedUp, + modelDown: timeAware.adjustedDown, + marketUp, + marketDown, + timeLeftMin, dataValues: [ new Date().toISOString(), marketSlugNow, diff --git a/src/index5m.js b/src/index5m.js index d3d5c6ef..80bf5e74 100644 --- a/src/index5m.js +++ b/src/index5m.js @@ -30,7 +30,7 @@ import { setupKeyboard } from "./trading/keyboard.js"; import { processActionQueue } from "./trading/executor.js"; import { createPriceLatch } from "./trading/priceLatch.js"; import { createTradeTracker } from "./trading/tracker.js"; -import { createDryRunLogger5m } from "./dryRun.js"; +import { createDryRunSimulator5m } from "./dryRun.js"; applyGlobalProxyFromEnv(); @@ -75,7 +75,7 @@ async function main() { const tracker = createTradeTracker(); const dumpedMarkets = new Set(); - const dryRun = createDryRunLogger5m("./logs/dryrun_5m.csv"); + const dryRun = createDryRunSimulator5m("./logs/dryrun_5m.csv", CONFIG.trading); process.on("exit", () => dryRun.flushNow()); let signalCooldown = { side: null, ts: 0, slug: null }; @@ -355,17 +355,18 @@ async function main() { "", // pnl ]); - // ── Dry-run study log ───────────────────────────────────────────────── + // ── Dry-run paper-trading simulator ──────────────────────────────────── { - const signalSide = rec.action === "ENTER" ? rec.side : null; - const entryPrice = signalSide === "UP" ? marketUp : signalSide === "DOWN" ? marketDown : null; - const vwapSlopeLbl = vwapSlope === null ? "" : vwapSlope > 0 ? "UP" : vwapSlope < 0 ? "DOWN" : "FLAT"; dryRun.tick({ slug: marketSlugNow, priceToBeat, btcPrice: currentPrice, - signalSide, - entryPrice, + rec, + modelUp: timeAware.adjustedUp, + modelDown: timeAware.adjustedDown, + marketUp, + marketDown, + timeLeftMin, dataValues: [ new Date().toISOString(), marketSlugNow, From 3cfdf555efd523e6632840600a8dda2d52344d44 Mon Sep 17 00:00:00 2001 From: Jean Carraro Date: Sun, 12 Apr 2026 22:33:48 -0300 Subject: [PATCH 15/49] Replace signal stats with paper-trading trade summary in console display - dryRun.getStats() now tracks wins, losses, totalTrades and recentTrades - Both index.js and index5m.js use dryRun stats for the HISTORICO panel - Display renamed to TRADES (paper): shows trade count, W/L, WR%, PNL - Each recent trade row shows time, side, P&L, ROI%, and exit reason code - Removed onSold/closedTrades signal-based tracking from index.js Co-Authored-By: Claude Sonnet 4.6 --- .dockerignore | 6 +++ CLAUDE.md | 19 ++++++--- Dockerfile | 15 +++++++ docker-compose.yml | 28 +++++++++++++ src/config.js | 9 +++++ src/config5m.js | 15 ++++++- src/display.js | 25 +++++------- src/dryRun.js | 71 +++++++++++++++++++++++++++++---- src/index.js | 36 +++++++++++------ src/index5m.js | 27 +++++++++++-- src/trading/client.js | 9 ++++- src/trading/position.js | 32 +++++++++------ src/trading/redeem.js | 88 +++++++++++++++++++++++++++++++++++++++++ 13 files changed, 323 insertions(+), 57 deletions(-) create mode 100644 .dockerignore create mode 100644 Dockerfile create mode 100644 docker-compose.yml create mode 100644 src/trading/redeem.js diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 00000000..b61257ce --- /dev/null +++ b/.dockerignore @@ -0,0 +1,6 @@ +node_modules +logs +.env +.env.* +*.md +IDEAS.md diff --git a/CLAUDE.md b/CLAUDE.md index 5ee1f405..c7be4a52 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -67,10 +67,11 @@ All modules are reusable by future bots targeting other markets or strategies. - **client.js** — Initializes `ClobClient` with L1 (EIP-712) + L2 (HMAC) auth. Derives API credentials on first run via `createOrDeriveApiKey()`. Caches the client singleton. Auto-detects `POLY_GNOSIS_SAFE` when `POLYMARKET_SIGNATURE_TYPE=1` but the funder address is a GnosisSafe contract. Exposes `balanceAddress` (funder or EOA) for USDC balance queries. - **orders.js** — `buyMarketOrder()` and `sellMarketOrder()` wrappers around `client.createAndPostMarketOrder()` using `OrderType.FAK` (Fill and Kill — partial fills accepted). Buy price = `bestAsk + 0.02`; sell price = `bestBid - 0.02`, both clamped to valid range. Returns `{ ok, order }` or `{ ok: false, error }`. -- **position.js** — In-memory position state: `recordBuy()`, `recordSell()`, `getPosition()`, `computeROI()`, `resetIfMarketChanged()`. `fetchPositionBalance()` syncs shares from chain via `getBalanceAllowance()` (used before selling to get actual on-chain balance). `fetchUsdcBalance(address)` reads USDC.e balance directly from Polygon blockchain (not the CLOB API, which only tracks deposited collateral). `evaluateExit()` recommends exits: TP and SL only trigger when the model also confirms reversal (`oppositeProb >= signalFlipMinProb`); TIME_DECAY only applies when entry price ≥ 0.50 (cheap entries are held to resolution). +- **position.js** — In-memory position state: `recordBuy()`, `recordSell()`, `getPosition()`, `computeROI()`, `resetIfMarketChanged()`. `fetchPositionBalance()` syncs shares from chain via `getBalanceAllowance()` (used before selling to get actual on-chain balance). `fetchUsdcBalance(address)` reads USDC.e balance directly from Polygon blockchain (not the CLOB API, which only tracks deposited collateral). `evaluateExit()` recommends exits: TP triggers when model confirms reversal (`oppositeProb >= signalFlipMinProb`); SL requires a stricter `stopLossMinProb` threshold AND the position to have aged at least `stopLossMinDurationS` seconds (both configurable, 5m uses tighter values); TIME_DECAY only applies when entry price ≥ 0.50 (cheap entries are held to resolution). - **keyboard.js** — `setupKeyboard({ tradingEnabled })` sets up stdin raw mode and returns `{ actionQueue, getConfirmHint(ctx), stdinError }`. The action queue is drained each tick by the executor. `getConfirmHint` builds the [Y]/[N] confirmation line shown on the display. - **executor.js** — `processActionQueue(queue, ctx)` drains the keyboard action queue and executes buy/sell orders: selects side, picks bestAsk/bestBid with slippage, calls `buyMarketOrder`/`sellMarketOrder`, fetches on-chain balance, calls `recordBuy`/`recordSell`, logs errors to `./logs/trade_errors.log`. Calls optional `onSold` callback after a successful sell. - **priceLatch.js** — `createPriceLatch()` returns `{ update(ctx) }`. Manages the state machine that latches the Chainlink BTC/USD reference price at market open (used as the "price to beat" on the display). Reads from the market object first, then fetches historical Chainlink if the app started late (>30s after open), otherwise latches the live price. +- **redeem.js** — `redeemSettledPositions({ wallet, conditionId, marketSlug })`. Called automatically on every market slug change when `tradingEnabled` is true. Calls `ConditionalTokens.redeemPositions(USDC_E, ZERO_BYTES32, conditionId, [1, 2])` on Polygon to convert any winning tokens back to USDC. Redeeming both index sets is safe: the CTF contract pays out only for positions actually held; losing tokens return $0. Logs to `./logs/trade_orders.log`. Fire-and-forget — does not block the poll loop. - **tracker.js** — `createTradeTracker()` returns `{ update(ctx), getStats(), getRecentOutcomes() }`. Tracks the first signal seen per market; when the market slug changes (settlement), computes win/loss and P&L based on the final Chainlink price vs the latched reference. Returns a `settled` object from `update()` so the caller writes it to the CSV in its own format. Both main loops listen for keypresses when trading is enabled: **[B]** buy the recommended side, **[S]** sell 100% of position, **[Q]** quit. Actions are queued and processed inside the main loop where market data is available. @@ -95,9 +96,13 @@ Called once at startup via `applyGlobalProxyFromEnv()`. Reads `HTTPS_PROXY`/`HTT | `POLYMARKET_FUNDER` | (derived from key) | Polymarket profile address (proxy/GnosisSafe wallet) | | `POLYMARKET_SIGNATURE_TYPE` | `0` | `0`=EOA, `1`=POLY_PROXY (auto-detects GnosisSafe), `2`=GNOSIS_SAFE | | `POLYMARKET_TRADE_AMOUNT` | `5` | USDC amount per trade | +| `DRY_RUN` | `false` | Set to `true` to run in paper-trading-only mode (no real orders, no redemption, even if private key is set) | | `TRADE_TAKE_PROFIT_PCT` | `20` | ROI % to recommend take-profit (requires model reversal) | | `TRADE_STOP_LOSS_PCT` | `25` | ROI % loss to recommend stop-loss (requires model reversal) | | `TRADE_SIGNAL_FLIP_PROB` | `0.58` | Min opposite-side probability to consider model reversed | +| `TRADE_SL_MIN_PROB` | `0.58` (15m) / `0.65` (5m) | Min opposite-side probability specifically to trigger stop-loss (can be stricter than flip prob) | +| `TRADE_SL_MIN_DURATION_S` | `0` (15m) / `120` (5m) | Minimum seconds a position must be held before stop-loss can fire | +| `TRADE_FLIP_COOLDOWN_S` | `60` (15m) / `90` (5m) | Seconds to wait after a SIGNAL_FLIP before re-entering the same market | ## Output @@ -126,17 +131,21 @@ createDryRunSimulator5m("./logs/dryrun_5m.csv", CONFIG.trading) // used by in 3. **SELL** — when an exit condition triggers, the position is closed at the current market price. ROI and PNL are recorded. 4. **SETTLEMENT** — when the market slug changes (settlement), any open position resolves at $1 (if the held side won) or $0 (if it lost). -After selling, the simulator can re-enter on a new signal within the same market. +After selling, the simulator can re-enter on a new signal within the same market, subject to the post-flip cooldown. **Exit conditions** (same as real trading): | Condition | Trigger | |---|---| -| `TAKE_PROFIT` | ROI ≥ `takeProfitPct` AND model confirms reversal | -| `STOP_LOSS` | ROI ≤ `-stopLossPct` AND model confirms reversal | -| `SIGNAL_FLIP` | Model opposite-side probability ≥ `signalFlipMinProb` | +| `TAKE_PROFIT` | ROI ≥ `takeProfitPct` AND model confirms reversal (`oppositeProb >= signalFlipMinProb`) | +| `STOP_LOSS` | ROI ≤ `-stopLossPct` AND `oppositeProb >= stopLossMinProb` AND position age ≥ `stopLossMinDurationS` | +| `SIGNAL_FLIP` | Model opposite-side probability ≥ `signalFlipMinProb` (and no TP/SL threshold crossed) | | `TIME_DECAY` | < 1.5 min left, losing > 5%, entry was ≥ 50¢ | | `SETTLED_WIN` / `SETTLED_LOSS` | Market ended, position resolved | +**Post-flip cooldown:** after a `SIGNAL_FLIP` exit the simulator will not open a new position for `flipCooldownS` seconds (60s on 15m, 90s on 5m). The cooldown resets when a new market starts. + +**Stop-loss guards:** the 5m simulator uses `stopLossMinProb = 0.65` (stricter than the base `signalFlipMinProb = 0.58`) and `stopLossMinDurationS = 120` to avoid being stopped out in the first ~2 minutes of a volatile move. Dry-run analysis showed 85 stops averaging only 79 seconds, despite an 86% settled win rate — most would have recovered if held longer. + **Output files:** The tick CSV logs every second with all indicators + simulation state. The trades CSV logs one row per completed trade for easy analysis. diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 00000000..807c7ec6 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,15 @@ +FROM node:22-alpine + +WORKDIR /app + +# Install dependencies first (cached layer) +COPY package*.json ./ +RUN npm ci --omit=dev + +# Copy source +COPY src/ ./src/ + +# Logs dir (will be overridden by volume mount, but good to have) +RUN mkdir -p logs + +CMD ["node", "src/index.js"] diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 00000000..e56d934d --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,28 @@ +services: + bot-15m: + build: . + container_name: polymarket-bot-15m + restart: unless-stopped + env_file: .env + command: node src/index.js + volumes: + - ./logs:/app/logs + logging: + driver: json-file + options: + max-size: "10m" + max-file: "3" + + bot-5m: + build: . + container_name: polymarket-bot-5m + restart: unless-stopped + env_file: .env + command: node src/index5m.js + volumes: + - ./logs:/app/logs + logging: + driver: json-file + options: + max-size: "10m" + max-file: "3" diff --git a/src/config.js b/src/config.js index 66c951f5..5b2f673d 100644 --- a/src/config.js +++ b/src/config.js @@ -34,6 +34,15 @@ export const CONFIG = { takeProfitPct: Number(process.env.TRADE_TAKE_PROFIT_PCT || "20"), // vender ao atingir +20% ROI stopLossPct: Number(process.env.TRADE_STOP_LOSS_PCT || "25"), // vender ao atingir -25% ROI signalFlipMinProb: Number(process.env.TRADE_SIGNAL_FLIP_PROB || "0.58"), // prob oposta que indica inversão + // Stop-loss guards: require higher conviction + minimum hold time before stopping out + stopLossMinProb: Number(process.env.TRADE_SL_MIN_PROB || "0.58"), // min opposite prob to trigger SL + stopLossMinDurationS: Number(process.env.TRADE_SL_MIN_DURATION_S || "0"), // seconds position must age before SL fires + // Cooldown after a SIGNAL_FLIP before re-entering the same market + flipCooldownS: Number(process.env.TRADE_FLIP_COOLDOWN_S || "60"), + // Consecutive ticks model must confirm reversal before SIGNAL_FLIP fires + flipConfirmTicks: Number(process.env.TRADE_FLIP_CONFIRM_TICKS || "2"), + // When true: paper-trading only — no real orders even if private key is set + dryRunOnly: (process.env.DRY_RUN || "").toLowerCase() === "true", }, chainlink: { diff --git a/src/config5m.js b/src/config5m.js index 469af824..002f5429 100644 --- a/src/config5m.js +++ b/src/config5m.js @@ -22,5 +22,18 @@ export const CONFIG = { ...BASE.polymarket, seriesId: "10684", seriesSlug: "btc-up-or-down-5m" - } + }, + + // 5m-specific trading overrides + trading: { + ...BASE.trading, + // Higher conviction required before stopping out (vs 0.58 base) + stopLossMinProb: Number(process.env.TRADE_SL_MIN_PROB || "0.65"), + // Position must be held at least 2 minutes before SL can fire + stopLossMinDurationS: Number(process.env.TRADE_SL_MIN_DURATION_S || "120"), + // Longer cooldown between flips on the faster 5m timeframe + flipCooldownS: Number(process.env.TRADE_FLIP_COOLDOWN_S || "90"), + // Require 3 consecutive confirming ticks before exiting on signal flip + flipConfirmTicks: Number(process.env.TRADE_FLIP_CONFIRM_TICKS || "3"), + }, }; diff --git a/src/display.js b/src/display.js index cd6b69bd..3c4ed508 100644 --- a/src/display.js +++ b/src/display.js @@ -377,33 +377,28 @@ export function buildScreen(d) { } const rightHist = []; - rightHist.push(section("HISTORICO")); - // Signal stats + rightHist.push(section("TRADES (paper)")); + // Paper-trading stats const rs = d.runningStats ?? { wins: 0, losses: 0, totalPnl: 0 }; const total = rs.wins + rs.losses; const wr = total > 0 ? `${((rs.wins / total) * 100).toFixed(0)}%` : "-"; const pc = rs.totalPnl > 0 ? ANSI.green : rs.totalPnl < 0 ? ANSI.red : ANSI.gray; const ps = rs.totalPnl > 0 ? "+" : ""; - rightHist.push(`W:${ANSI.green}${rs.wins}${ANSI.reset} L:${ANSI.red}${rs.losses}${ANSI.reset} WR:${wr} ${pc}${ps}${rs.totalPnl.toFixed(2)} USDC${ANSI.reset}`); + rightHist.push(`${ANSI.dim}Trades: ${ANSI.reset}${total} W:${ANSI.green}${rs.wins}${ANSI.reset} L:${ANSI.red}${rs.losses}${ANSI.reset} WR:${wr}`); + rightHist.push(`${ANSI.dim}PNL:${ANSI.reset} ${pc}${ps}$${rs.totalPnl.toFixed(2)}${ANSI.reset}`); - // Closed trades + // Recent closed trades if (d.closedTrades?.length) { - for (const t of d.closedTrades.slice(0, 3)) { + for (const t of d.closedTrades.slice(0, 4)) { const color = t.pnl >= 0 ? ANSI.green : ANSI.red; const pSign = t.pnl >= 0 ? "+" : ""; const rSign = t.roi >= 0 ? "+" : ""; const sl = t.side === "UP" ? `${ANSI.green}\u2191UP${ANSI.reset}` : `${ANSI.red}\u2193DN${ANSI.reset}`; const ts = new Date(t.ts).toLocaleTimeString("pt-BR", { hour: "2-digit", minute: "2-digit" }); - rightHist.push(`${ANSI.dim}${ts}${ANSI.reset} ${sl} ${color}${pSign}$${t.pnl.toFixed(2)} ${rSign}${t.roi.toFixed(0)}%${ANSI.reset}`); - } - } - - // Recent outcomes (signal-based) - if (d.recentOutcomes?.length) { - for (const o of d.recentOutcomes.slice(0, 2)) { - const color = o.won ? ANSI.green : ANSI.red; - const label = o.won ? "WIN" : "LOSS"; - rightHist.push(`${color}${label}${ANSI.reset} ${ANSI.dim}${o.side}${ANSI.reset} ${color}${o.pnl > 0 ? "+" : ""}${o.pnl.toFixed(2)}${ANSI.reset}`); + const reasonShort = t.reason === "TAKE_PROFIT" ? "TP" : t.reason === "STOP_LOSS" ? "SL" + : t.reason === "SIGNAL_FLIP" ? "FLIP" : t.reason === "TIME_DECAY" ? "TD" + : t.reason === "SETTLED_WIN" ? "WIN" : t.reason === "SETTLED_LOSS" ? "LOSS" : (t.reason ?? ""); + rightHist.push(`${ANSI.dim}${ts}${ANSI.reset} ${sl} ${color}${pSign}$${t.pnl.toFixed(2)} ${rSign}${t.roi.toFixed(0)}%${ANSI.reset} ${ANSI.dim}${reasonShort}${ANSI.reset}`); } } diff --git a/src/dryRun.js b/src/dryRun.js index de53eb23..d40716e6 100644 --- a/src/dryRun.js +++ b/src/dryRun.js @@ -92,13 +92,19 @@ function evaluateSimExit({ pos, modelUp, modelDown, currentMarketPrice, timeLeft : null; const modelConfirmsReversal = oppositeProb != null && oppositeProb >= config.signalFlipMinProb; + // Stop-loss guards: higher prob threshold + minimum hold time + const slMinProb = config.stopLossMinProb ?? config.signalFlipMinProb; + const slConfirmed = oppositeProb != null && oppositeProb >= slMinProb; + const positionAgeS = pos.entryTime ? (Date.now() - pos.entryTime) / 1000 : Infinity; + const slAgedEnough = positionAgeS >= (config.stopLossMinDurationS ?? 0); + // Take profit — only if model also confirms reversal if (roiPct >= config.takeProfitPct && modelConfirmsReversal) { return { shouldSell: true, reason: "TAKE_PROFIT", roiPct }; } - // Stop loss — only if model also confirms reversal - if (roiPct <= -config.stopLossPct && modelConfirmsReversal) { + // Stop loss — requires stricter prob + minimum hold time + if (roiPct <= -config.stopLossPct && slConfirmed && slAgedEnough) { return { shouldSell: true, reason: "STOP_LOSS", roiPct }; } @@ -130,6 +136,20 @@ function createSimulator(csvPath, header, config) { let pos = { active: false, side: null, entryPrice: 0, shares: 0, invested: 0, marketSlug: null, entryTime: null }; let cumulativePnl = 0; + // Trade stats + let wins = 0; + let losses = 0; + let totalTrades = 0; + let recentTrades = []; // { side, pnl, roi, ts, reason }[] + + // Cooldown tracking: timestamp (ms) of last SIGNAL_FLIP exit, reset on new market + let lastFlipTime = null; + const flipCooldownMs = (config.flipCooldownS ?? 0) * 1000; + + // Consecutive-tick confirmation counter for SIGNAL_FLIP + let flipConfirmCount = 0; + const flipConfirmTicks = config.flipConfirmTicks ?? 1; + function _resetPos() { pos = { active: false, side: null, entryPrice: 0, shares: 0, invested: 0, marketSlug: null, entryTime: null }; } @@ -160,6 +180,12 @@ function createSimulator(csvPath, header, config) { durationS, ]); fs.appendFileSync(tradesPath, row + "\n", "utf8"); + + // Update in-memory stats + totalTrades += 1; + if (pnl >= 0) wins += 1; else losses += 1; + recentTrades.unshift({ side: pos.side, pnl, roi: roiPct, ts: exitTime, reason }); + if (recentTrades.length > 10) recentTrades.pop(); } function _settlePosition() { @@ -228,6 +254,8 @@ function createSimulator(csvPath, header, config) { if (slug !== currentSlug && currentSlug !== null) { _settlePosition(); _flush(); + lastFlipTime = null; // reset cooldown on new market + flipConfirmCount = 0; // reset flip confirmation on new market } currentSlug = slug; @@ -247,13 +275,24 @@ function createSimulator(csvPath, header, config) { const currentMktPrice = pos.side === "UP" ? marketUp : marketDown; // Evaluate exit - const exitEval = evaluateSimExit({ + const rawEval = evaluateSimExit({ pos, modelUp, modelDown, currentMarketPrice: currentMktPrice, timeLeftMin, config, }); + // Apply consecutive-tick confirmation gate for SIGNAL_FLIP + let exitEval = rawEval; + if (rawEval.shouldSell && rawEval.reason === "SIGNAL_FLIP") { + flipConfirmCount++; + if (flipConfirmCount < flipConfirmTicks) { + exitEval = { shouldSell: false, reason: null, roiPct: rawEval.roiPct }; + } + } else { + flipConfirmCount = 0; + } + if (exitEval.shouldSell && currentMktPrice != null) { // ── SELL ──────────────────────────────────────────────────────── const exitValue = pos.shares * currentMktPrice; @@ -270,6 +309,11 @@ function createSimulator(csvPath, header, config) { simExitReason = exitEval.reason; simPnl = fmt(pnl, 4); + if (exitEval.reason === "SIGNAL_FLIP") { + lastFlipTime = Date.now(); + } + flipConfirmCount = 0; + _logTrade({ exitPrice: currentMktPrice, exitValue, pnl, roiPct, reason: exitEval.reason, exitTime: Date.now() }); _resetPos(); } else { @@ -281,8 +325,11 @@ function createSimulator(csvPath, header, config) { simRoiPct = exitEval.roiPct != null ? fmt(exitEval.roiPct, 2) : ""; } } else if (rec.action === "ENTER" && rec.side) { - // ── BUY ─────────────────────────────────────────────────────────── - const entryMktPrice = rec.side === "UP" ? marketUp : marketDown; + // ── BUY — skip if still within post-flip cooldown ───────────────── + const inCooldown = flipCooldownMs > 0 && lastFlipTime !== null && + (Date.now() - lastFlipTime) < flipCooldownMs; + + const entryMktPrice = !inCooldown ? (rec.side === "UP" ? marketUp : marketDown) : null; if (entryMktPrice != null && entryMktPrice > 0) { const shares = config.tradeAmount / entryMktPrice; pos = { @@ -295,6 +342,8 @@ function createSimulator(csvPath, header, config) { entryTime: Date.now(), }; + flipConfirmCount = 0; + simAction = "BUY"; simSide = rec.side; simEntryPrice = fmt(entryMktPrice, 4); @@ -325,7 +374,7 @@ function createSimulator(csvPath, header, config) { /** Get cumulative stats for display purposes. */ function getStats() { - return { cumulativePnl, positionActive: pos.active, positionSide: pos.side }; + return { wins, losses, totalTrades, cumulativePnl, positionActive: pos.active, positionSide: pos.side, recentTrades }; } return { tick, flushNow, getStats }; @@ -344,6 +393,10 @@ export function createDryRunSimulator15m(csvPath, tradingConfig = {}) { takeProfitPct: tradingConfig.takeProfitPct ?? 20, stopLossPct: tradingConfig.stopLossPct ?? 25, signalFlipMinProb: tradingConfig.signalFlipMinProb ?? 0.58, + stopLossMinProb: tradingConfig.stopLossMinProb ?? null, + stopLossMinDurationS: tradingConfig.stopLossMinDurationS ?? 0, + flipCooldownS: tradingConfig.flipCooldownS ?? 60, + flipConfirmTicks: tradingConfig.flipConfirmTicks ?? 2, }; return createSimulator(csvPath, HEADER_15M, config); } @@ -351,7 +404,7 @@ export function createDryRunSimulator15m(csvPath, tradingConfig = {}) { /** * Create a paper-trading simulator for the 5-minute assistant. * @param {string} csvPath - path for the tick-by-tick CSV - * @param {{ tradeAmount?: number, takeProfitPct?: number, stopLossPct?: number, signalFlipMinProb?: number }} [tradingConfig] + * @param {{ tradeAmount?: number, takeProfitPct?: number, stopLossPct?: number, signalFlipMinProb?: number, stopLossMinProb?: number, stopLossMinDurationS?: number, flipCooldownS?: number }} [tradingConfig] */ export function createDryRunSimulator5m(csvPath, tradingConfig = {}) { const config = { @@ -359,6 +412,10 @@ export function createDryRunSimulator5m(csvPath, tradingConfig = {}) { takeProfitPct: tradingConfig.takeProfitPct ?? 20, stopLossPct: tradingConfig.stopLossPct ?? 25, signalFlipMinProb: tradingConfig.signalFlipMinProb ?? 0.58, + stopLossMinProb: tradingConfig.stopLossMinProb ?? 0.65, + stopLossMinDurationS: tradingConfig.stopLossMinDurationS ?? 120, + flipCooldownS: tradingConfig.flipCooldownS ?? 90, + flipConfirmTicks: tradingConfig.flipConfirmTicks ?? 3, }; return createSimulator(csvPath, HEADER_5M, config); } diff --git a/src/index.js b/src/index.js index d1ccf716..07110354 100644 --- a/src/index.js +++ b/src/index.js @@ -30,6 +30,7 @@ import { processActionQueue } from "./trading/executor.js"; import { createPriceLatch } from "./trading/priceLatch.js"; import { createTradeTracker } from "./trading/tracker.js"; import { createDryRunSimulator15m } from "./dryRun.js"; +import { redeemSettledPositions } from "./trading/redeem.js"; applyGlobalProxyFromEnv(); @@ -74,18 +75,14 @@ async function main() { const dryRun = createDryRunSimulator15m("./logs/dryrun_15m.csv", CONFIG.trading); process.on("exit", () => dryRun.flushNow()); - let closedTrades = []; // { side, entryPrice, exitPrice, pnl, roi, ts }[] - - const onSold = ({ side, entryPrice, exitPrice, pnl, roi }) => { - closedTrades.unshift({ side, entryPrice, exitPrice, pnl, roi, ts: Date.now() }); - if (closedTrades.length > 10) closedTrades.pop(); - }; - let prevSpotPrice = null; let prevCurrentPrice = null; let usdcBalance = null; let usdcBalanceError = null; let usdcLastFetchMs = 0; + let flipConfirmCount = 0; + let prevMarketSlug = ""; + let prevConditionId = null; while (true) { const timing = getCandleWindowTiming(CONFIG.candleWindowMinutes); @@ -159,10 +156,20 @@ async function main() { const rec = decide({ remainingMinutes: timeLeftMin, edgeUp: edge.edgeUp, edgeDown: edge.edgeDown, modelUp: timeAware.adjustedUp, modelDown: timeAware.adjustedDown, conflicted: scored.conflicted }); // ── Trading ─────────────────────────────────────────────────────────── - const marketSlugNow = poly.ok ? String(poly.market?.slug ?? "") : ""; + const marketSlugNow = poly.ok ? String(poly.market?.slug ?? "") : ""; + const conditionIdNow = poly.ok ? (poly.market?.conditionId ?? null) : null; + + // On market change: redeem any settled tokens from the previous market + if (marketSlugNow && marketSlugNow !== prevMarketSlug && prevConditionId && trading.tradingEnabled) { + redeemSettledPositions({ wallet: trading.wallet, conditionId: prevConditionId, marketSlug: prevMarketSlug }) + .catch(() => {}); // fire-and-forget; errors are logged inside redeemSettledPositions + } + if (conditionIdNow) prevConditionId = conditionIdNow; + prevMarketSlug = marketSlugNow || prevMarketSlug; + resetIfMarketChanged(marketSlugNow); - await processActionQueue(keyboard.actionQueue, { trading, poly, rec, timeAware, marketSlugNow, onSold }); + await processActionQueue(keyboard.actionQueue, { trading, poly, rec, timeAware, marketSlugNow }); if (trading.tradingEnabled && Date.now() - usdcLastFetchMs > 30_000) { usdcLastFetchMs = Date.now(); @@ -251,7 +258,12 @@ async function main() { takeProfitPct: CONFIG.trading.takeProfitPct, stopLossPct: CONFIG.trading.stopLossPct, signalFlipMinProb: CONFIG.trading.signalFlipMinProb, + stopLossMinProb: CONFIG.trading.stopLossMinProb, + stopLossMinDurationS: CONFIG.trading.stopLossMinDurationS, + flipConfirmCount, + flipConfirmTicks: CONFIG.trading.flipConfirmTicks, }); + flipConfirmCount = pos.active ? (exitEval.flipConfirmCount ?? 0) : 0; const signal = rec.action === "ENTER" ? (rec.side === "UP" ? "BUY UP" : "BUY DOWN") : "NO TRADE"; @@ -287,9 +299,9 @@ async function main() { position: pos, currentMktPrice, exitEval, - closedTrades, - runningStats: tracker.getStats(), - recentOutcomes: tracker.getRecentOutcomes(), + closedTrades: dryRun.getStats().recentTrades, + runningStats: (() => { const s = dryRun.getStats(); return { wins: s.wins, losses: s.losses, totalPnl: s.cumulativePnl }; })(), + recentOutcomes: [], })); prevSpotPrice = spotPrice ?? prevSpotPrice; diff --git a/src/index5m.js b/src/index5m.js index 80bf5e74..f80d5410 100644 --- a/src/index5m.js +++ b/src/index5m.js @@ -31,6 +31,7 @@ import { processActionQueue } from "./trading/executor.js"; import { createPriceLatch } from "./trading/priceLatch.js"; import { createTradeTracker } from "./trading/tracker.js"; import { createDryRunSimulator5m } from "./dryRun.js"; +import { redeemSettledPositions } from "./trading/redeem.js"; applyGlobalProxyFromEnv(); @@ -84,6 +85,9 @@ async function main() { let usdcBalance = null; let usdcBalanceError = null; let usdcLastFetchMs = 0; + let flipConfirmCount = 0; + let prevMarketSlug = ""; + let prevConditionId = null; while (true) { const timing = getCandleWindowTiming(CONFIG.candleWindowMinutes); @@ -156,7 +160,17 @@ async function main() { let rec = decide5m({ remainingMinutes: timeLeftMin, edgeUp: edge.edgeUp, edgeDown: edge.edgeDown, modelUp: timeAware.adjustedUp, modelDown: timeAware.adjustedDown, heikenColor: consec.color, ofi1m: ofi1mVal }); // ── Trading ─────────────────────────────────────────────────────────── - const marketSlugNow = poly.ok ? String(poly.market?.slug ?? "") : ""; + const marketSlugNow = poly.ok ? String(poly.market?.slug ?? "") : ""; + const conditionIdNow = poly.ok ? (poly.market?.conditionId ?? null) : null; + + // On market change: redeem any settled tokens from the previous market + if (marketSlugNow && marketSlugNow !== prevMarketSlug && prevConditionId && trading.tradingEnabled) { + redeemSettledPositions({ wallet: trading.wallet, conditionId: prevConditionId, marketSlug: prevMarketSlug }) + .catch(() => {}); + } + if (conditionIdNow) prevConditionId = conditionIdNow; + prevMarketSlug = marketSlugNow || prevMarketSlug; + // ── Signal cooldown (prevent flip-flop) ─────────────────────────────── if (rec.action === "ENTER") { @@ -286,7 +300,12 @@ async function main() { takeProfitPct: CONFIG.trading.takeProfitPct, stopLossPct: CONFIG.trading.stopLossPct, signalFlipMinProb: CONFIG.trading.signalFlipMinProb, + stopLossMinProb: CONFIG.trading.stopLossMinProb, + stopLossMinDurationS: CONFIG.trading.stopLossMinDurationS, + flipConfirmCount, + flipConfirmTicks: CONFIG.trading.flipConfirmTicks, }); + flipConfirmCount = pos.active ? (exitEval.flipConfirmCount ?? 0) : 0; const modeTag = isNextMarket ? `${ANSI.yellow}[5m] [PROXIMO]${ANSI.reset}` : `${ANSI.yellow}[5m]${ANSI.reset}`; @@ -324,9 +343,9 @@ async function main() { position: pos, currentMktPrice, exitEval, - closedTrades: [], - runningStats: tracker.getStats(), - recentOutcomes: tracker.getRecentOutcomes(), + closedTrades: dryRun.getStats().recentTrades, + runningStats: (() => { const s = dryRun.getStats(); return { wins: s.wins, losses: s.losses, totalPnl: s.cumulativePnl }; })(), + recentOutcomes: [], })); prevSpotPrice = spotPrice ?? prevSpotPrice; diff --git a/src/trading/client.js b/src/trading/client.js index e529e3c0..7ae0124f 100644 --- a/src/trading/client.js +++ b/src/trading/client.js @@ -18,7 +18,12 @@ export async function initTradingClient(config) { const { privateKey, funder, signatureType, tradeAmount } = config.trading; if (!privateKey) { - _cached = { client: null, tradingEnabled: false, tradeAmount: 0 }; + _cached = { client: null, tradingEnabled: false, tradeAmount: 0, wallet: null }; + return _cached; + } + + if (config.trading.dryRunOnly) { + _cached = { client: null, tradingEnabled: false, tradeAmount: 0, wallet: null }; return _cached; } @@ -95,7 +100,7 @@ export async function initTradingClient(config) { // balanceAddress: onde está o USDC — o funder (proxy) ou o EOA const balanceAddress = funderAddr ?? _wallet.address; - _cached = { client, tradingEnabled: true, tradeAmount, balanceAddress }; + _cached = { client, tradingEnabled: true, tradeAmount, balanceAddress, wallet: _wallet }; return _cached; } diff --git a/src/trading/position.js b/src/trading/position.js index 3ff3d0f6..75bb0356 100644 --- a/src/trading/position.js +++ b/src/trading/position.js @@ -108,9 +108,9 @@ export function resetIfMarketChanged(currentSlug) { // Avalia se a posição aberta deve ser encerrada. // Retorna { shouldSell, reason, urgency } onde urgency é "HIGH" | "MEDIUM" | null -export function evaluateExit({ position, modelUp, modelDown, currentMarketPrice, timeLeftMin, takeProfitPct, stopLossPct, signalFlipMinProb }) { +export function evaluateExit({ position, modelUp, modelDown, currentMarketPrice, timeLeftMin, takeProfitPct, stopLossPct, signalFlipMinProb, stopLossMinProb = null, stopLossMinDurationS = 0, flipConfirmCount = 0, flipConfirmTicks = 1 }) { if (!position.active || currentMarketPrice == null) { - return { shouldSell: false, reason: null, urgency: null }; + return { shouldSell: false, reason: null, urgency: null, flipConfirmCount: 0 }; } const currentValue = position.shares * currentMarketPrice; @@ -122,32 +122,42 @@ export function evaluateExit({ position, modelUp, modelDown, currentMarketPrice, : null; const modelConfirmsReversal = oppositeProb != null && oppositeProb >= signalFlipMinProb; + // Effective minimum prob for stop-loss (may be stricter than signalFlipMinProb) + const slMinProb = stopLossMinProb ?? signalFlipMinProb; + const slConfirmed = oppositeProb != null && oppositeProb >= slMinProb; + const positionAgeS = position.timestamp ? (Date.now() - position.timestamp) / 1000 : Infinity; + const slAgedEnough = positionAgeS >= stopLossMinDurationS; + // 1. Take profit — só recomenda se o modelo também aponta reversão if (roiPct >= takeProfitPct && modelConfirmsReversal) { const urgency = oppositeProb >= 0.65 ? "HIGH" : "MEDIUM"; - return { shouldSell: true, reason: "TAKE_PROFIT", urgency, roiPct }; + return { shouldSell: true, reason: "TAKE_PROFIT", urgency, roiPct, flipConfirmCount: 0 }; } - // 2. Stop loss — só recomenda se o modelo também aponta reversão - if (roiPct <= -stopLossPct && modelConfirmsReversal) { + // 2. Stop loss — requer prob mais alta e duração mínima (se configurado) + if (roiPct <= -stopLossPct && slConfirmed && slAgedEnough) { const urgency = oppositeProb >= 0.65 ? "HIGH" : "MEDIUM"; - return { shouldSell: true, reason: "STOP_LOSS", urgency, roiPct }; + return { shouldSell: true, reason: "STOP_LOSS", urgency, roiPct, flipConfirmCount: 0 }; } - // 3. Sinal invertido com força suficiente, independente do ROI + // 3. Sinal invertido — requer N ticks consecutivos de confirmação if (modelConfirmsReversal) { - const urgency = oppositeProb >= 0.65 ? "HIGH" : "MEDIUM"; - return { shouldSell: true, reason: "SIGNAL_FLIPPED", urgency, roiPct }; + const newCount = flipConfirmCount + 1; + if (newCount >= flipConfirmTicks) { + const urgency = oppositeProb >= 0.65 ? "HIGH" : "MEDIUM"; + return { shouldSell: true, reason: "SIGNAL_FLIPPED", urgency, roiPct, flipConfirmCount: newCount }; + } + return { shouldSell: false, reason: null, urgency: null, roiPct, flipConfirmCount: newCount }; } // 4. Pouco tempo + perdendo — só aplica se a entrada foi cara (>= 50¢) // Posições baratas já têm o risco precificado; vale segurar até a resolução const entryWasCheap = position.entryPrice < 0.50; if (timeLeftMin != null && timeLeftMin < 1.5 && roiPct < -5 && !entryWasCheap) { - return { shouldSell: true, reason: "TIME_DECAY", urgency: "MEDIUM", roiPct }; + return { shouldSell: true, reason: "TIME_DECAY", urgency: "MEDIUM", roiPct, flipConfirmCount: 0 }; } - return { shouldSell: false, reason: null, urgency: null, roiPct }; + return { shouldSell: false, reason: null, urgency: null, roiPct, flipConfirmCount: 0 }; } export async function fetchPositionBalance(client, tokenId) { diff --git a/src/trading/redeem.js b/src/trading/redeem.js new file mode 100644 index 00000000..66a80db2 --- /dev/null +++ b/src/trading/redeem.js @@ -0,0 +1,88 @@ +/** + * Redeems settled Polymarket conditional tokens back to USDC. + * + * After a binary market resolves, winning tokens are redeemable for $1 each and + * losing tokens for $0. Neither is credited automatically — the CTF contract must + * be called explicitly. Without this, won USDC stays locked in unredeemed tokens + * and the bot's CLOB balance never recovers. + * + * Redeeming both index sets ([1, 2]) is safe: the CTF contract pays out only for + * tokens actually held; redeeming losing tokens costs a tiny amount of gas and + * returns nothing. + */ + +import { ethers } from "ethers"; +import fs from "node:fs"; +import { CONFIG } from "../config.js"; + +const POLYGON_NETWORK = ethers.Network.from(137); +const CTF_ADDRESS = "0x4D97DCd97eC945f40cF65F87097ACe5EA0476045"; // ConditionalTokens (Polygon) +const USDC_E_ADDRESS = "0x2791Bca1f2de4661ED88A30C99A7a9449Aa84174"; +const ZERO_BYTES32 = "0x" + "00".repeat(32); + +const CTF_ABI = [ + "function redeemPositions(address collateralToken, bytes32 parentCollectionId, bytes32 conditionId, uint256[] calldata indexSets) external", +]; + +function logRedeem(msg) { + try { + fs.appendFileSync("./logs/trade_orders.log", + `${new Date().toISOString()} [REDEEM] ${msg}\n`); + } catch { /* ignore */ } +} + +async function _getProvider() { + const rpcs = [ + ...(CONFIG.chainlink.polygonRpcUrls ?? []), + CONFIG.chainlink.polygonRpcUrl, + "https://polygon-bor-rpc.publicnode.com", + "https://rpc.ankr.com/polygon", + ].map(s => String(s || "").trim()).filter(Boolean); + + for (const rpc of rpcs) { + const p = new ethers.JsonRpcProvider(rpc, POLYGON_NETWORK, { staticNetwork: POLYGON_NETWORK }); + try { + await Promise.race([ + p.getBlockNumber(), + new Promise((_, r) => setTimeout(() => r(new Error("timeout")), 3000)), + ]); + return p; + } catch { + p.destroy(); + } + } + throw new Error("Nenhum RPC Polygon disponível para redemption"); +} + +/** + * Redeems all positions for a settled market. + * + * @param {{ wallet: import("ethers").Wallet, conditionId: string, marketSlug?: string }} params + * @returns {Promise<{ ok: boolean, txHash?: string, error?: string }>} + */ +export async function redeemSettledPositions({ wallet, conditionId, marketSlug = "" }) { + if (!wallet || !conditionId) { + return { ok: false, error: "missing wallet or conditionId" }; + } + + logRedeem(`Iniciando redemption slug=${marketSlug} conditionId=${conditionId}`); + + try { + const provider = await _getProvider(); + const connected = wallet.connect(provider); + const ctf = new ethers.Contract(CTF_ADDRESS, CTF_ABI, connected); + + // Redeem both outcomes: indexSet 1 = outcome 0 (Down/No), indexSet 2 = outcome 1 (Up/Yes) + const tx = await ctf.redeemPositions(USDC_E_ADDRESS, ZERO_BYTES32, conditionId, [1, 2]); + logRedeem(`Tx enviada: ${tx.hash} — aguardando confirmação...`); + + const receipt = await tx.wait(); + logRedeem(`Redemption confirmada: ${tx.hash} bloco=${receipt.blockNumber}`); + provider.destroy(); + return { ok: true, txHash: tx.hash }; + } catch (err) { + const msg = err?.message ?? String(err); + logRedeem(`Erro no redemption: ${msg}`); + return { ok: false, error: msg }; + } +} From 45170e2dff55bbd042a45b83695bf600d5ac659e Mon Sep 17 00:00:00 2001 From: Jean Carraro Date: Sun, 12 Apr 2026 22:42:46 -0300 Subject: [PATCH 16/49] Add POLYMARKET_LIVE_TRADING flag: default sim mode, show virtual position - New env var POLYMARKET_LIVE_TRADING (default: false = simulated mode) - When not set or != "true", real orders are blocked even with private key - dryRun.getStats() now exposes full virtual position (entryPrice, shares, etc.) - POSICAO section shows simulated open position when in sim mode - Status badge: yellow SIM vs green LIVE with balance only in live mode - Both index.js and index5m.js branch on liveTrading for position display Co-Authored-By: Claude Sonnet 4.6 --- src/config.js | 2 ++ src/display.js | 34 ++++++++++++++++--------- src/dryRun.js | 15 +++++++++-- src/index.js | 69 ++++++++++++++++++++++++++++++++++---------------- src/index5m.js | 69 ++++++++++++++++++++++++++++++++++---------------- 5 files changed, 131 insertions(+), 58 deletions(-) diff --git a/src/config.js b/src/config.js index 5b2f673d..e0e861ef 100644 --- a/src/config.js +++ b/src/config.js @@ -43,6 +43,8 @@ export const CONFIG = { flipConfirmTicks: Number(process.env.TRADE_FLIP_CONFIRM_TICKS || "2"), // When true: paper-trading only — no real orders even if private key is set dryRunOnly: (process.env.DRY_RUN || "").toLowerCase() === "true", + // When true: enables real order execution. Default false = simulated/paper mode. + liveTradingEnabled: (process.env.POLYMARKET_LIVE_TRADING || "").toLowerCase() === "true", }, chainlink: { diff --git a/src/display.js b/src/display.js index 3c4ed508..e6ed63d6 100644 --- a/src/display.js +++ b/src/display.js @@ -287,17 +287,24 @@ export function buildScreen(d) { // Trading status + shortcuts + clock on a single line const etStr = `${ANSI.dim}${fmtEtTime()} ${getBtcSession()}${ANSI.reset}`; - let tradingBadge = d.tradingEnabled - ? `${ANSI.green}● ATIVO${ANSI.reset} $${d.tradeAmount}` - : d.initError - ? `${ANSI.red}● ERRO${ANSI.reset}` - : `${ANSI.gray}● LEITURA${ANSI.reset}`; - if (d.tradingEnabled) { - if (d.usdcBalanceError) { - tradingBadge += ` ${ANSI.red}Saldo: ${d.usdcBalanceError}${ANSI.reset}`; - } else if (d.usdcBalance != null) { - tradingBadge += ` ${ANSI.dim}Saldo: ${ANSI.reset}${ANSI.white}$${Number(d.usdcBalance).toFixed(2)} USDC${ANSI.reset}`; + let tradingBadge; + if (d.liveTrading) { + // Live trading mode + if (d.tradingEnabled) { + tradingBadge = `${ANSI.green}● LIVE${ANSI.reset} $${d.tradeAmount}`; + if (d.usdcBalanceError) { + tradingBadge += ` ${ANSI.red}Saldo: ${d.usdcBalanceError}${ANSI.reset}`; + } else if (d.usdcBalance != null) { + tradingBadge += ` ${ANSI.dim}Saldo: ${ANSI.reset}${ANSI.white}$${Number(d.usdcBalance).toFixed(2)} USDC${ANSI.reset}`; + } + } else if (d.initError) { + tradingBadge = `${ANSI.red}● LIVE ERRO${ANSI.reset}`; + } else { + tradingBadge = `${ANSI.red}● LIVE (sem chave)${ANSI.reset}`; } + } else { + // Simulated mode + tradingBadge = `${ANSI.yellow}● SIM${ANSI.reset} $${d.tradeAmount}`; } const confirmOrKeys = d.confirmHint ?? d.shortcutsHint ?? ""; lines.push(`${tradingBadge} ${confirmOrKeys}${" ".repeat(Math.max(0, W - visLen(tradingBadge) - visLen(confirmOrKeys) - visLen(etStr) - 4))} ${etStr}`); @@ -349,9 +356,12 @@ export function buildScreen(d) { // ── BOTTOM ROW: Position left │ History right ── const leftPos = []; - leftPos.push(section("POSICAO")); + const posLabel = d.liveTrading ? "POSICAO" : "POSICAO (sim)"; + leftPos.push(section(posLabel)); - if (!d.tradingEnabled) { + if (!d.liveTrading && !d.position.active) { + leftPos.push(`${ANSI.gray}Aguardando sinal...${ANSI.reset}`); + } else if (d.liveTrading && !d.tradingEnabled) { leftPos.push(`${ANSI.gray}Trading desativado${ANSI.reset}`); } else if (!d.position.active) { leftPos.push(`${ANSI.gray}Nenhuma posicao aberta${ANSI.reset}`); diff --git a/src/dryRun.js b/src/dryRun.js index d40716e6..b0637855 100644 --- a/src/dryRun.js +++ b/src/dryRun.js @@ -372,9 +372,20 @@ function createSimulator(csvPath, header, config) { _flush(); } - /** Get cumulative stats for display purposes. */ + /** Get cumulative stats and current virtual position for display purposes. */ function getStats() { - return { wins, losses, totalTrades, cumulativePnl, positionActive: pos.active, positionSide: pos.side, recentTrades }; + return { + wins, losses, totalTrades, cumulativePnl, recentTrades, + position: { + active: pos.active, + side: pos.side, + entryPrice: pos.entryPrice, + shares: pos.shares, + invested: pos.invested, + entryTime: pos.entryTime, + marketSlug: pos.marketSlug, + }, + }; } return { tick, flushNow, getStats }; diff --git a/src/index.js b/src/index.js index 07110354..833b5ea5 100644 --- a/src/index.js +++ b/src/index.js @@ -59,12 +59,16 @@ async function main() { const polymarketLiveStream = startPolymarketChainlinkPriceStream({}); const chainlinkStream = startChainlinkPriceStream({}); + const liveTrading = CONFIG.trading.liveTradingEnabled; + let trading = { client: null, tradingEnabled: false, tradeAmount: 0, initError: null }; try { trading = await initTradingClient(CONFIG); } catch (err) { trading.initError = err?.message ?? String(err); } + // Override: only allow real orders when POLYMARKET_LIVE_TRADING=true + if (!liveTrading) trading.tradingEnabled = false; const resolveMarket = createMarketResolver(CONFIG.polymarket, CONFIG.pollIntervalMs); const keyboard = setupKeyboard({ tradingEnabled: trading.tradingEnabled }); @@ -250,20 +254,40 @@ async function main() { const ptbStr = ptbDelta === null ? "" : ` (${ptbDelta > 0 ? ANSI.green + "+" : ptbDelta < 0 ? ANSI.red : ANSI.gray}$${Math.abs(ptbDelta).toFixed(2)}${ANSI.reset})`; - const pos = getPosition(); - const currentMktPrice = pos.active ? (pos.side === "UP" ? marketUp : marketDown) : null; - const exitEval = evaluateExit({ - position: pos, modelUp: pLong, modelDown: pShort, - currentMarketPrice: currentMktPrice, timeLeftMin, - takeProfitPct: CONFIG.trading.takeProfitPct, - stopLossPct: CONFIG.trading.stopLossPct, - signalFlipMinProb: CONFIG.trading.signalFlipMinProb, - stopLossMinProb: CONFIG.trading.stopLossMinProb, - stopLossMinDurationS: CONFIG.trading.stopLossMinDurationS, - flipConfirmCount, - flipConfirmTicks: CONFIG.trading.flipConfirmTicks, - }); - flipConfirmCount = pos.active ? (exitEval.flipConfirmCount ?? 0) : 0; + const simStats = dryRun.getStats(); + + // Position and exit eval: real when live, simulated otherwise + let displayPos, displayCurrentMktPrice, displayExitEval; + if (liveTrading) { + displayPos = getPosition(); + displayCurrentMktPrice = displayPos.active ? (displayPos.side === "UP" ? marketUp : marketDown) : null; + displayExitEval = evaluateExit({ + position: displayPos, modelUp: pLong, modelDown: pShort, + currentMarketPrice: displayCurrentMktPrice, timeLeftMin, + takeProfitPct: CONFIG.trading.takeProfitPct, + stopLossPct: CONFIG.trading.stopLossPct, + signalFlipMinProb: CONFIG.trading.signalFlipMinProb, + stopLossMinProb: CONFIG.trading.stopLossMinProb, + stopLossMinDurationS: CONFIG.trading.stopLossMinDurationS, + flipConfirmCount, + flipConfirmTicks: CONFIG.trading.flipConfirmTicks, + }); + flipConfirmCount = displayPos.active ? (displayExitEval.flipConfirmCount ?? 0) : 0; + } else { + displayPos = simStats.position; + displayCurrentMktPrice = displayPos.active ? (displayPos.side === "UP" ? marketUp : marketDown) : null; + displayExitEval = evaluateExit({ + position: displayPos, modelUp: pLong, modelDown: pShort, + currentMarketPrice: displayCurrentMktPrice, timeLeftMin, + takeProfitPct: CONFIG.trading.takeProfitPct, + stopLossPct: CONFIG.trading.stopLossPct, + signalFlipMinProb: CONFIG.trading.signalFlipMinProb, + stopLossMinProb: CONFIG.trading.stopLossMinProb, + stopLossMinDurationS: CONFIG.trading.stopLossMinDurationS, + flipConfirmCount: 0, + flipConfirmTicks: 1, + }); + } const signal = rec.action === "ENTER" ? (rec.side === "UP" ? "BUY UP" : "BUY DOWN") : "NO TRADE"; @@ -271,11 +295,12 @@ async function main() { title: poly.ok ? (poly.market?.question ?? "-") : "-", modeTag: null, marketSlug, + liveTrading, tradingEnabled: trading.tradingEnabled, initError: trading.initError, - tradeAmount: trading.tradeAmount, - usdcBalance, - usdcBalanceError, + tradeAmount: CONFIG.trading.tradeAmount, + usdcBalance: liveTrading ? usdcBalance : null, + usdcBalanceError: liveTrading ? usdcBalanceError : null, confirmHint, shortcutsHint, binanceSpot: `${colorPriceLine({ label: "", price: spotPrice, prevPrice: prevSpotPrice, decimals: 0, prefix: "$" })}`, @@ -296,11 +321,11 @@ async function main() { ], predictValue: `${ANSI.green}LONG ${formatProbPct(pLong, 0)}${ANSI.reset} / ${ANSI.red}SHORT ${formatProbPct(pShort, 0)}${ANSI.reset}`, recLine: `${recColor}${recLabel}${ANSI.reset}`, - position: pos, - currentMktPrice, - exitEval, - closedTrades: dryRun.getStats().recentTrades, - runningStats: (() => { const s = dryRun.getStats(); return { wins: s.wins, losses: s.losses, totalPnl: s.cumulativePnl }; })(), + position: displayPos, + currentMktPrice: displayCurrentMktPrice, + exitEval: displayExitEval, + closedTrades: simStats.recentTrades, + runningStats: { wins: simStats.wins, losses: simStats.losses, totalPnl: simStats.cumulativePnl }, recentOutcomes: [], })); diff --git a/src/index5m.js b/src/index5m.js index f80d5410..80448a6a 100644 --- a/src/index5m.js +++ b/src/index5m.js @@ -63,12 +63,16 @@ async function main() { const polymarketLiveStream = startPolymarketChainlinkPriceStream({}); const chainlinkStream = startChainlinkPriceStream({}); + const liveTrading = CONFIG.trading.liveTradingEnabled; + let trading = { client: null, tradingEnabled: false, tradeAmount: 0, initError: null }; try { trading = await initTradingClient(CONFIG); } catch (err) { trading.initError = err?.message ?? String(err); } + // Override: only allow real orders when POLYMARKET_LIVE_TRADING=true + if (!liveTrading) trading.tradingEnabled = false; const resolveMarket = createMarketResolver(CONFIG.polymarket, CONFIG.pollIntervalMs); const keyboard = setupKeyboard({ tradingEnabled: trading.tradingEnabled }); @@ -292,20 +296,40 @@ async function main() { ? kv("Intervalo:", `${isNextMarket ? ANSI.yellow : ""}${fmtEtHHMM(marketStartMs)} \u2192 ${fmtEtHHMM(settlementMs5m)} ET${isNextMarket ? ANSI.reset : ""}`) : null; - const pos = getPosition(); - const currentMktPrice = pos.active ? (pos.side === "UP" ? marketUp : marketDown) : null; - const exitEval = evaluateExit({ - position: pos, modelUp: timeAware.adjustedUp, modelDown: timeAware.adjustedDown, - currentMarketPrice: currentMktPrice, timeLeftMin, - takeProfitPct: CONFIG.trading.takeProfitPct, - stopLossPct: CONFIG.trading.stopLossPct, - signalFlipMinProb: CONFIG.trading.signalFlipMinProb, - stopLossMinProb: CONFIG.trading.stopLossMinProb, - stopLossMinDurationS: CONFIG.trading.stopLossMinDurationS, - flipConfirmCount, - flipConfirmTicks: CONFIG.trading.flipConfirmTicks, - }); - flipConfirmCount = pos.active ? (exitEval.flipConfirmCount ?? 0) : 0; + const simStats = dryRun.getStats(); + + // Position and exit eval: real when live, simulated otherwise + let displayPos, displayCurrentMktPrice, displayExitEval; + if (liveTrading) { + displayPos = getPosition(); + displayCurrentMktPrice = displayPos.active ? (displayPos.side === "UP" ? marketUp : marketDown) : null; + displayExitEval = evaluateExit({ + position: displayPos, modelUp: timeAware.adjustedUp, modelDown: timeAware.adjustedDown, + currentMarketPrice: displayCurrentMktPrice, timeLeftMin, + takeProfitPct: CONFIG.trading.takeProfitPct, + stopLossPct: CONFIG.trading.stopLossPct, + signalFlipMinProb: CONFIG.trading.signalFlipMinProb, + stopLossMinProb: CONFIG.trading.stopLossMinProb, + stopLossMinDurationS: CONFIG.trading.stopLossMinDurationS, + flipConfirmCount, + flipConfirmTicks: CONFIG.trading.flipConfirmTicks, + }); + flipConfirmCount = displayPos.active ? (displayExitEval.flipConfirmCount ?? 0) : 0; + } else { + displayPos = simStats.position; + displayCurrentMktPrice = displayPos.active ? (displayPos.side === "UP" ? marketUp : marketDown) : null; + displayExitEval = evaluateExit({ + position: displayPos, modelUp: timeAware.adjustedUp, modelDown: timeAware.adjustedDown, + currentMarketPrice: displayCurrentMktPrice, timeLeftMin, + takeProfitPct: CONFIG.trading.takeProfitPct, + stopLossPct: CONFIG.trading.stopLossPct, + signalFlipMinProb: CONFIG.trading.signalFlipMinProb, + stopLossMinProb: CONFIG.trading.stopLossMinProb, + stopLossMinDurationS: CONFIG.trading.stopLossMinDurationS, + flipConfirmCount: 0, + flipConfirmTicks: 1, + }); + } const modeTag = isNextMarket ? `${ANSI.yellow}[5m] [PROXIMO]${ANSI.reset}` : `${ANSI.yellow}[5m]${ANSI.reset}`; @@ -313,11 +337,12 @@ async function main() { title: poly.ok ? (poly.market?.question ?? "-") : "-", modeTag, marketSlug, + liveTrading, tradingEnabled: trading.tradingEnabled, initError: trading.initError, - tradeAmount: trading.tradeAmount, - usdcBalance, - usdcBalanceError, + tradeAmount: CONFIG.trading.tradeAmount, + usdcBalance: liveTrading ? usdcBalance : null, + usdcBalanceError: liveTrading ? usdcBalanceError : null, confirmHint, shortcutsHint, binanceSpot: `${colorPriceLine({ label: "", price: spotPrice, prevPrice: prevSpotPrice, decimals: 0, prefix: "$" })}`, @@ -340,11 +365,11 @@ async function main() { ], predictValue: `${ANSI.green}LONG${ANSI.reset} ${ANSI.green}${formatProbPct(pLong, 0)}${ANSI.reset} / ${ANSI.red}SHORT${ANSI.reset} ${ANSI.red}${formatProbPct(pShort, 0)}${ANSI.reset}`, recLine: `${recColor}${recLabel}${ANSI.reset}`, - position: pos, - currentMktPrice, - exitEval, - closedTrades: dryRun.getStats().recentTrades, - runningStats: (() => { const s = dryRun.getStats(); return { wins: s.wins, losses: s.losses, totalPnl: s.cumulativePnl }; })(), + position: displayPos, + currentMktPrice: displayCurrentMktPrice, + exitEval: displayExitEval, + closedTrades: simStats.recentTrades, + runningStats: { wins: simStats.wins, losses: simStats.losses, totalPnl: simStats.cumulativePnl }, recentOutcomes: [], })); From fe27514319a6dafa9735d28cd8c20b0010bf8c26 Mon Sep 17 00:00:00 2001 From: Jean Carraro Date: Mon, 13 Apr 2026 22:49:23 -0300 Subject: [PATCH 17/49] Tighten exit thresholds based on 5-day dry-run analysis MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 15m: port stop-loss guards from 5m (minProb 0.58→0.65, minDuration 0→120s). The 5-day run showed 21 stops with 0 wins (−$10.22) — the permissive guards were cutting positions that would have recovered. 5m: reduce SIGNAL_FLIP sensitivity (signalFlipMinProb 0.58→0.62, flipConfirmTicks 3→5). The run showed 158 flip exits with only 3.8% win rate, despite a 92.8% settled-win rate — the lower threshold was catching transient blips across 0.58 that then reverted. Co-Authored-By: Claude Opus 4.6 --- CLAUDE.md | 11 +++++++---- src/config.js | 4 ++-- src/config5m.js | 10 +++++++--- src/dryRun.js | 8 ++++---- 4 files changed, 20 insertions(+), 13 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index c7be4a52..a4475534 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -99,10 +99,11 @@ Called once at startup via `applyGlobalProxyFromEnv()`. Reads `HTTPS_PROXY`/`HTT | `DRY_RUN` | `false` | Set to `true` to run in paper-trading-only mode (no real orders, no redemption, even if private key is set) | | `TRADE_TAKE_PROFIT_PCT` | `20` | ROI % to recommend take-profit (requires model reversal) | | `TRADE_STOP_LOSS_PCT` | `25` | ROI % loss to recommend stop-loss (requires model reversal) | -| `TRADE_SIGNAL_FLIP_PROB` | `0.58` | Min opposite-side probability to consider model reversed | -| `TRADE_SL_MIN_PROB` | `0.58` (15m) / `0.65` (5m) | Min opposite-side probability specifically to trigger stop-loss (can be stricter than flip prob) | -| `TRADE_SL_MIN_DURATION_S` | `0` (15m) / `120` (5m) | Minimum seconds a position must be held before stop-loss can fire | +| `TRADE_SIGNAL_FLIP_PROB` | `0.58` (15m) / `0.62` (5m) | Min opposite-side probability to consider model reversed | +| `TRADE_SL_MIN_PROB` | `0.65` | Min opposite-side probability specifically to trigger stop-loss (can be stricter than flip prob) | +| `TRADE_SL_MIN_DURATION_S` | `120` | Minimum seconds a position must be held before stop-loss can fire | | `TRADE_FLIP_COOLDOWN_S` | `60` (15m) / `90` (5m) | Seconds to wait after a SIGNAL_FLIP before re-entering the same market | +| `TRADE_FLIP_CONFIRM_TICKS` | `2` (15m) / `5` (5m) | Consecutive confirming ticks required before SIGNAL_FLIP exit fires | ## Output @@ -144,7 +145,9 @@ After selling, the simulator can re-enter on a new signal within the same market **Post-flip cooldown:** after a `SIGNAL_FLIP` exit the simulator will not open a new position for `flipCooldownS` seconds (60s on 15m, 90s on 5m). The cooldown resets when a new market starts. -**Stop-loss guards:** the 5m simulator uses `stopLossMinProb = 0.65` (stricter than the base `signalFlipMinProb = 0.58`) and `stopLossMinDurationS = 120` to avoid being stopped out in the first ~2 minutes of a volatile move. Dry-run analysis showed 85 stops averaging only 79 seconds, despite an 86% settled win rate — most would have recovered if held longer. +**Stop-loss guards:** both simulators use `stopLossMinProb = 0.65` (stricter than the `signalFlipMinProb` gate) and `stopLossMinDurationS = 120` to avoid being stopped out in the first ~2 minutes of a volatile move. Earlier 5m dry-run analysis showed stops averaging only 79 seconds despite an 86% settled win rate — most would have recovered if held longer. A later 5-day run on 15m confirmed the same pattern (21 stops, 0 wins, −$10), so the 5m guards were ported to the 15m base config. + +**Signal-flip guards (5m):** the 5m simulator raises `signalFlipMinProb` to `0.62` (vs `0.58` on 15m) and `flipConfirmTicks` to `5`. A 5-day dry-run showed 158 SIGNAL_FLIP exits with only 3.8% winning — the lower threshold was catching transient blips across 0.58 that then reverted, cutting positions that would have settled as wins (settled-only win rate is 92.8%). Higher conviction + longer confirmation persistence filters those blips. **Output files:** diff --git a/src/config.js b/src/config.js index e0e861ef..3a453e0e 100644 --- a/src/config.js +++ b/src/config.js @@ -35,8 +35,8 @@ export const CONFIG = { stopLossPct: Number(process.env.TRADE_STOP_LOSS_PCT || "25"), // vender ao atingir -25% ROI signalFlipMinProb: Number(process.env.TRADE_SIGNAL_FLIP_PROB || "0.58"), // prob oposta que indica inversão // Stop-loss guards: require higher conviction + minimum hold time before stopping out - stopLossMinProb: Number(process.env.TRADE_SL_MIN_PROB || "0.58"), // min opposite prob to trigger SL - stopLossMinDurationS: Number(process.env.TRADE_SL_MIN_DURATION_S || "0"), // seconds position must age before SL fires + stopLossMinProb: Number(process.env.TRADE_SL_MIN_PROB || "0.65"), // min opposite prob to trigger SL + stopLossMinDurationS: Number(process.env.TRADE_SL_MIN_DURATION_S || "120"), // seconds position must age before SL fires // Cooldown after a SIGNAL_FLIP before re-entering the same market flipCooldownS: Number(process.env.TRADE_FLIP_COOLDOWN_S || "60"), // Consecutive ticks model must confirm reversal before SIGNAL_FLIP fires diff --git a/src/config5m.js b/src/config5m.js index 002f5429..52eea6c0 100644 --- a/src/config5m.js +++ b/src/config5m.js @@ -27,13 +27,17 @@ export const CONFIG = { // 5m-specific trading overrides trading: { ...BASE.trading, - // Higher conviction required before stopping out (vs 0.58 base) + // Higher conviction required before treating a signal as reversed (flip/TP/SL gate). + // Raised from 0.58 after dry-run analysis: SIGNAL_FLIP fired 158× with only 3.8% win — + // the lower threshold was catching transient blips across 0.58 that then reverted. + signalFlipMinProb: Number(process.env.TRADE_SIGNAL_FLIP_PROB || "0.62"), + // Even higher conviction required before stopping out stopLossMinProb: Number(process.env.TRADE_SL_MIN_PROB || "0.65"), // Position must be held at least 2 minutes before SL can fire stopLossMinDurationS: Number(process.env.TRADE_SL_MIN_DURATION_S || "120"), // Longer cooldown between flips on the faster 5m timeframe flipCooldownS: Number(process.env.TRADE_FLIP_COOLDOWN_S || "90"), - // Require 3 consecutive confirming ticks before exiting on signal flip - flipConfirmTicks: Number(process.env.TRADE_FLIP_CONFIRM_TICKS || "3"), + // Require 5 consecutive confirming ticks before exiting on signal flip + flipConfirmTicks: Number(process.env.TRADE_FLIP_CONFIRM_TICKS || "5"), }, }; diff --git a/src/dryRun.js b/src/dryRun.js index b0637855..d86c9732 100644 --- a/src/dryRun.js +++ b/src/dryRun.js @@ -404,8 +404,8 @@ export function createDryRunSimulator15m(csvPath, tradingConfig = {}) { takeProfitPct: tradingConfig.takeProfitPct ?? 20, stopLossPct: tradingConfig.stopLossPct ?? 25, signalFlipMinProb: tradingConfig.signalFlipMinProb ?? 0.58, - stopLossMinProb: tradingConfig.stopLossMinProb ?? null, - stopLossMinDurationS: tradingConfig.stopLossMinDurationS ?? 0, + stopLossMinProb: tradingConfig.stopLossMinProb ?? 0.65, + stopLossMinDurationS: tradingConfig.stopLossMinDurationS ?? 120, flipCooldownS: tradingConfig.flipCooldownS ?? 60, flipConfirmTicks: tradingConfig.flipConfirmTicks ?? 2, }; @@ -422,11 +422,11 @@ export function createDryRunSimulator5m(csvPath, tradingConfig = {}) { tradeAmount: tradingConfig.tradeAmount ?? 5, takeProfitPct: tradingConfig.takeProfitPct ?? 20, stopLossPct: tradingConfig.stopLossPct ?? 25, - signalFlipMinProb: tradingConfig.signalFlipMinProb ?? 0.58, + signalFlipMinProb: tradingConfig.signalFlipMinProb ?? 0.62, stopLossMinProb: tradingConfig.stopLossMinProb ?? 0.65, stopLossMinDurationS: tradingConfig.stopLossMinDurationS ?? 120, flipCooldownS: tradingConfig.flipCooldownS ?? 90, - flipConfirmTicks: tradingConfig.flipConfirmTicks ?? 3, + flipConfirmTicks: tradingConfig.flipConfirmTicks ?? 5, }; return createSimulator(csvPath, HEADER_5M, config); } From 2c0cbc7cf28114e94d93f1de60b558d3d5da6d1b Mon Sep 17 00:00:00 2001 From: Jean Carraro Date: Tue, 14 Apr 2026 18:59:01 -0300 Subject: [PATCH 18/49] Add Telegram notifications for trade events and daily summary MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - New src/notify.js: notifyTrade, notifyStart, notifyDailySummary - dryRun.js: notifies BUY and SELL (including settlements) for both sims - executor.js: notifies real buy/sell orders - index.js / index5m.js: notifyStart on boot, daily summary at midnight ET - docker-compose.yml: --max-old-space-size=384 + mem_limit=512m on both bots Set TELEGRAM_BOT_TOKEN + TELEGRAM_CHAT_ID in .env to enable. All calls are fire-and-forget — silently no-ops if vars are unset. Co-Authored-By: Claude Sonnet 4.6 --- docker-compose.yml | 6 ++- src/dryRun.js | 20 +++++-- src/index.js | 13 ++++- src/index5m.js | 13 ++++- src/notify.js | 116 ++++++++++++++++++++++++++++++++++++++++ src/trading/executor.js | 5 +- 6 files changed, 165 insertions(+), 8 deletions(-) create mode 100644 src/notify.js diff --git a/docker-compose.yml b/docker-compose.yml index e56d934d..1d8ea2ad 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -4,7 +4,8 @@ services: container_name: polymarket-bot-15m restart: unless-stopped env_file: .env - command: node src/index.js + command: node --max-old-space-size=384 src/index.js + mem_limit: 512m volumes: - ./logs:/app/logs logging: @@ -18,7 +19,8 @@ services: container_name: polymarket-bot-5m restart: unless-stopped env_file: .env - command: node src/index5m.js + command: node --max-old-space-size=384 src/index5m.js + mem_limit: 512m volumes: - ./logs:/app/logs logging: diff --git a/src/dryRun.js b/src/dryRun.js index d86c9732..a90898ab 100644 --- a/src/dryRun.js +++ b/src/dryRun.js @@ -21,6 +21,7 @@ import fs from "node:fs"; import path from "node:path"; import { ensureDir } from "./utils.js"; +import { notifyTrade } from "./notify.js"; // ── Headers ───────────────────────────────────────────────────────────────── @@ -124,7 +125,7 @@ function evaluateSimExit({ pos, modelUp, modelDown, currentMarketPrice, timeLeft // ── Simulator core ────────────────────────────────────────────────────────── -function createSimulator(csvPath, header, config) { +function createSimulator(csvPath, header, config, label = "bot") { const tradesPath = csvPath.replace(/\.csv$/, "_trades.csv"); let currentSlug = null; @@ -186,6 +187,13 @@ function createSimulator(csvPath, header, config) { if (pnl >= 0) wins += 1; else losses += 1; recentTrades.unshift({ side: pos.side, pnl, roi: roiPct, ts: exitTime, reason }); if (recentTrades.length > 10) recentTrades.pop(); + + notifyTrade({ + bot: label, isLive: false, action: "SELL", + side: pos.side, market: pos.marketSlug, + entryPrice: pos.entryPrice, exitPrice, + roi: roiPct, pnl, cumPnl: cumulativePnl, reason, + }); } function _settlePosition() { @@ -349,6 +357,12 @@ function createSimulator(csvPath, header, config) { simEntryPrice = fmt(entryMktPrice, 4); simCurrentPrice = fmt(entryMktPrice, 4); simRoiPct = "0.00"; + + notifyTrade({ + bot: label, isLive: false, action: "BUY", + side: rec.side, market: slug, + entryPrice: entryMktPrice, invested: config.tradeAmount, + }); } } @@ -409,7 +423,7 @@ export function createDryRunSimulator15m(csvPath, tradingConfig = {}) { flipCooldownS: tradingConfig.flipCooldownS ?? 60, flipConfirmTicks: tradingConfig.flipConfirmTicks ?? 2, }; - return createSimulator(csvPath, HEADER_15M, config); + return createSimulator(csvPath, HEADER_15M, config, "15m"); } /** @@ -428,5 +442,5 @@ export function createDryRunSimulator5m(csvPath, tradingConfig = {}) { flipCooldownS: tradingConfig.flipCooldownS ?? 90, flipConfirmTicks: tradingConfig.flipConfirmTicks ?? 5, }; - return createSimulator(csvPath, HEADER_5M, config); + return createSimulator(csvPath, HEADER_5M, config, "5m"); } diff --git a/src/index.js b/src/index.js index 833b5ea5..5f618d35 100644 --- a/src/index.js +++ b/src/index.js @@ -31,6 +31,7 @@ import { createPriceLatch } from "./trading/priceLatch.js"; import { createTradeTracker } from "./trading/tracker.js"; import { createDryRunSimulator15m } from "./dryRun.js"; import { redeemSettledPositions } from "./trading/redeem.js"; +import { notifyStart, notifyDailySummary } from "./notify.js"; applyGlobalProxyFromEnv(); @@ -75,6 +76,8 @@ async function main() { const priceLatch = createPriceLatch(); const tracker = createTradeTracker(); + notifyStart("15m"); + const dumpedMarkets = new Set(); const dryRun = createDryRunSimulator15m("./logs/dryrun_15m.csv", CONFIG.trading); process.on("exit", () => dryRun.flushNow()); @@ -87,6 +90,7 @@ async function main() { let flipConfirmCount = 0; let prevMarketSlug = ""; let prevConditionId = null; + let lastDaySummaryEt = new Date().toLocaleDateString("sv", { timeZone: "America/New_York" }); while (true) { const timing = getCandleWindowTiming(CONFIG.candleWindowMinutes); @@ -173,7 +177,7 @@ async function main() { resetIfMarketChanged(marketSlugNow); - await processActionQueue(keyboard.actionQueue, { trading, poly, rec, timeAware, marketSlugNow }); + await processActionQueue(keyboard.actionQueue, { trading, poly, rec, timeAware, marketSlugNow, botLabel: "15m" }); if (trading.tradingEnabled && Date.now() - usdcLastFetchMs > 30_000) { usdcLastFetchMs = Date.now(); @@ -394,6 +398,13 @@ async function main() { console.log("────────────────────────────"); } + // Daily summary — fires once at the first tick of each new ET day + const todayEt = new Date().toLocaleDateString("sv", { timeZone: "America/New_York" }); + if (todayEt !== lastDaySummaryEt) { + notifyDailySummary("15m", dryRun.getStats()); + lastDaySummaryEt = todayEt; + } + await sleep(CONFIG.pollIntervalMs); } } diff --git a/src/index5m.js b/src/index5m.js index 80448a6a..8b89cd83 100644 --- a/src/index5m.js +++ b/src/index5m.js @@ -32,6 +32,7 @@ import { createPriceLatch } from "./trading/priceLatch.js"; import { createTradeTracker } from "./trading/tracker.js"; import { createDryRunSimulator5m } from "./dryRun.js"; import { redeemSettledPositions } from "./trading/redeem.js"; +import { notifyStart, notifyDailySummary } from "./notify.js"; applyGlobalProxyFromEnv(); @@ -80,6 +81,8 @@ async function main() { const tracker = createTradeTracker(); const dumpedMarkets = new Set(); + notifyStart("5m"); + const dryRun = createDryRunSimulator5m("./logs/dryrun_5m.csv", CONFIG.trading); process.on("exit", () => dryRun.flushNow()); @@ -92,6 +95,7 @@ async function main() { let flipConfirmCount = 0; let prevMarketSlug = ""; let prevConditionId = null; + let lastDaySummaryEt = new Date().toLocaleDateString("sv", { timeZone: "America/New_York" }); while (true) { const timing = getCandleWindowTiming(CONFIG.candleWindowMinutes); @@ -190,7 +194,7 @@ async function main() { } resetIfMarketChanged(marketSlugNow); - await processActionQueue(keyboard.actionQueue, { trading, poly, rec, timeAware, marketSlugNow }); + await processActionQueue(keyboard.actionQueue, { trading, poly, rec, timeAware, marketSlugNow, botLabel: "5m" }); if (trading.tradingEnabled && Date.now() - usdcLastFetchMs > 30_000) { usdcLastFetchMs = Date.now(); @@ -445,6 +449,13 @@ async function main() { console.log("────────────────────────────"); } + // Daily summary — fires once at the first tick of each new ET day + const todayEt = new Date().toLocaleDateString("sv", { timeZone: "America/New_York" }); + if (todayEt !== lastDaySummaryEt) { + notifyDailySummary("5m", dryRun.getStats()); + lastDaySummaryEt = todayEt; + } + await sleep(CONFIG.pollIntervalMs); } } diff --git a/src/notify.js b/src/notify.js new file mode 100644 index 00000000..7d362d4a --- /dev/null +++ b/src/notify.js @@ -0,0 +1,116 @@ +/** + * Telegram notification helper. + * + * All functions are fire-and-forget — they never throw and never block the + * main loop. Set TELEGRAM_BOT_TOKEN + TELEGRAM_CHAT_ID in .env to enable. + * If either var is missing, all calls are silent no-ops. + */ + +const TOKEN = process.env.TELEGRAM_BOT_TOKEN; +const CHAT_ID = process.env.TELEGRAM_CHAT_ID; + +const TIMEOUT_MS = 5_000; + +async function _send(text) { + if (!TOKEN || !CHAT_ID) return; + const controller = new AbortController(); + const t = setTimeout(() => controller.abort(), TIMEOUT_MS); + try { + await fetch(`https://api.telegram.org/bot${TOKEN}/sendMessage`, { + method: "POST", + headers: { "content-type": "application/json" }, + body: JSON.stringify({ chat_id: CHAT_ID, text, parse_mode: "HTML" }), + signal: controller.signal, + }); + } catch { + // never let notification failure affect the bot + } finally { + clearTimeout(t); + } +} + +/** Send a free-form message. Never awaited by callers — fire and forget. */ +export function notify(text) { + _send(text); +} + +/** + * Notify a simulated or real trade event (BUY or SELL/settlement). + * + * @param {object} p + * @param {string} p.bot - "15m" or "5m" + * @param {boolean} p.isLive - true = real money, false = sim + * @param {"BUY"|"SELL"} p.action + * @param {string} p.side - "UP" or "DOWN" + * @param {string} p.market - market slug + * @param {number} p.entryPrice + * @param {number} [p.exitPrice] + * @param {number} [p.roi] - ROI % (SELL only) + * @param {number} [p.pnl] - realized PNL (SELL only) + * @param {number} [p.cumPnl] - cumulative PNL after this trade (SELL only) + * @param {string} [p.reason] - exit reason (SELL only) + * @param {number} [p.invested] - amount invested (BUY only) + */ +export function notifyTrade({ bot, isLive, action, side, market, entryPrice, + exitPrice, roi, pnl, cumPnl, reason, invested }) { + const mode = isLive ? "LIVE" : "SIM"; + const slug = String(market ?? "").slice(-16); // keep last 16 chars to stay compact + const sideArrow = side === "UP" ? "↑ UP" : "↓ DOWN"; + + let text; + + if (action === "BUY") { + const icon = isLive ? "🟢" : "📈"; + const inv = invested != null ? `$${Number(invested).toFixed(2)}` : ""; + const ep = entryPrice != null ? `${(entryPrice * 100).toFixed(1)}¢` : "-"; + text = [ + `${icon} [${bot}·${mode}] BUY ${sideArrow}`, + `Mercado: ...${slug}`, + `Entrada: ${ep}${inv ? ` | Inv: ${inv}` : ""}`, + ].join("\n"); + + } else { + // SELL / settlement + const won = pnl != null && pnl >= 0; + const icon = isLive + ? (won ? "💰" : "🔴") + : (reason?.startsWith("SETTLED") ? (won ? "🏆" : "💀") : (won ? "📊" : "📉")); + + const ep = entryPrice != null ? `${(entryPrice * 100).toFixed(1)}¢` : "-"; + const xp = exitPrice != null ? `${(exitPrice * 100).toFixed(1)}¢` : "-"; + const roiStr = roi != null ? `${roi >= 0 ? "+" : ""}${Number(roi).toFixed(1)}%` : "-"; + const pnlStr = pnl != null ? `${pnl >= 0 ? "+" : ""}$${Math.abs(pnl).toFixed(2)}` : "-"; + const cumStr = cumPnl != null ? `${cumPnl >= 0 ? "+" : ""}$${Number(cumPnl).toFixed(2)}` : null; + + text = [ + `${icon} [${bot}·${mode}] SELL — ${reason ?? "MANUAL"}`, + `${sideArrow}: ${ep} → ${xp}`, + `ROI: ${roiStr} | PNL: ${pnlStr}`, + cumStr ? `Acum: ${cumStr}` : null, + ].filter(Boolean).join("\n"); + } + + _send(text); +} + +/** Notify that the bot process started (after crash/restart or fresh deploy). */ +export function notifyStart(bot) { + _send(`✅ Bot ${bot} iniciado`); +} + +/** + * Send a daily summary of paper-trading stats. + * + * @param {string} bot - "15m" or "5m" + * @param {{ wins: number, losses: number, totalTrades: number, cumulativePnl: number }} stats + */ +export function notifyDailySummary(bot, stats) { + const { wins, losses, totalTrades, cumulativePnl } = stats; + const wr = totalTrades > 0 ? `${((wins / totalTrades) * 100).toFixed(0)}%` : "-"; + const cum = `${cumulativePnl >= 0 ? "+" : ""}$${Number(cumulativePnl).toFixed(2)}`; + _send([ + `📋 [${bot}·SIM] Resumo do dia`, + `Trades: ${totalTrades} | W:${wins} L:${losses} | WR: ${wr}`, + `PNL acumulado: ${cum}`, + ].join("\n")); +} diff --git a/src/trading/executor.js b/src/trading/executor.js index 7156e314..dab32d11 100644 --- a/src/trading/executor.js +++ b/src/trading/executor.js @@ -3,6 +3,7 @@ import { clamp } from "../utils.js"; import { setStatusMessage } from "../display.js"; import { buyMarketOrder, sellMarketOrder } from "./orders.js"; import { getPosition, recordBuy, recordSell, fetchPositionBalance } from "./position.js"; +import { notifyTrade } from "../notify.js"; function logError(msg) { try { @@ -24,7 +25,7 @@ function logError(msg) { * @param {string} ctx.marketSlugNow * @param {Function} [ctx.onSold] - called with { side, entryPrice, exitPrice, pnl, roi } after a sell */ -export async function processActionQueue(actionQueue, { trading, poly, rec, timeAware, marketSlugNow, onSold }) { +export async function processActionQueue(actionQueue, { trading, poly, rec, timeAware, marketSlugNow, onSold, botLabel = "bot" }) { while (actionQueue.length && trading.tradingEnabled && poly.ok) { const action = actionQueue.shift(); const marketUp = poly.prices.up; @@ -54,6 +55,7 @@ export async function processActionQueue(actionQueue, { trading, poly, rec, time const orderId = result.order?.orderID ?? result.order?.id ?? "-"; const balanceStr = balance > 0 ? `shares: ${balance.toFixed(2)}` : "saldo 0 (ordem não preenchida?)"; setStatusMessage(`COMPROU ${side} @ ${(entryRef * 100).toFixed(1)}¢ | $${trading.tradeAmount} | ${balanceStr} | ID: ${String(orderId).slice(0, 12)}`, 8000); + notifyTrade({ bot: botLabel, isLive: true, action: "BUY", side, market: marketSlugNow, entryPrice: entryRef, invested: trading.tradeAmount }); } else { const errMsg = `Erro na compra: ${result.error}`; setStatusMessage(errMsg, 15000); @@ -79,6 +81,7 @@ export async function processActionQueue(actionQueue, { trading, poly, rec, time const roi = (pnl / pos.invested) * 100; const sign = pnl >= 0 ? "+" : ""; setStatusMessage(`VENDEU ${pos.side} | P&L: ${sign}$${pnl.toFixed(2)}`, 8000); + notifyTrade({ bot: botLabel, isLive: true, action: "SELL", side: pos.side, market: marketSlugNow, entryPrice: pos.entryPrice, exitPrice, roi, pnl, reason: "MANUAL" }); recordSell(); onSold?.({ side: pos.side, entryPrice: pos.entryPrice, exitPrice, pnl, roi }); } else { From fa9fc9bfb916028b85375f2a4a31d1981ba90d89 Mon Sep 17 00:00:00 2001 From: Jean Carraro Date: Wed, 15 Apr 2026 09:08:08 -0300 Subject: [PATCH 19/49] Disable 5m SIGNAL_FLIP, fix sign formatting, add trade stats to notifications - dryRun.js: add disableSignalFlip config (default true on 5m) after counterfactual showed 5m signal flips were cutting winners (+$9.70 if held) - notify.js: always show explicit +/- sign on PNL, ROI, and cumulative PNL via new fmtSignedUsd/fmtSignedPct helpers; include total trades / W / L / WR on SELL notifications - display.js, index.js, index5m.js: show explicit '-' sign (not just red color) on BTC vs price-to-beat delta, position ROI/PNL, trade history Co-Authored-By: Claude Opus 4.6 --- src/display.js | 14 +++++++------- src/dryRun.js | 4 +++- src/index.js | 2 +- src/index5m.js | 2 +- src/notify.js | 47 +++++++++++++++++++++++++++++++++++++---------- 5 files changed, 49 insertions(+), 20 deletions(-) diff --git a/src/display.js b/src/display.js index e6ed63d6..903801d8 100644 --- a/src/display.js +++ b/src/display.js @@ -376,8 +376,8 @@ export function buildScreen(d) { const pnl = val - p.invested; const roiPct = (pnl / p.invested) * 100; const c = pnl >= 0 ? ANSI.green : ANSI.red; - const s = pnl >= 0 ? "+" : ""; - leftPos.push(kv("ROI:", `${c}${s}${roiPct.toFixed(1)}%${ANSI.reset} P&L: ${c}${s}$${pnl.toFixed(2)}${ANSI.reset} Val: $${val.toFixed(2)}`)); + const s = pnl >= 0 ? "+" : "-"; + leftPos.push(kv("ROI:", `${c}${s}${Math.abs(roiPct).toFixed(1)}%${ANSI.reset} P&L: ${c}${s}$${Math.abs(pnl).toFixed(2)}${ANSI.reset} Val: $${val.toFixed(2)}`)); } if (d.exitEval?.shouldSell) { const uc = d.exitEval.urgency === "HIGH" ? ANSI.red : ANSI.yellow; @@ -393,22 +393,22 @@ export function buildScreen(d) { const total = rs.wins + rs.losses; const wr = total > 0 ? `${((rs.wins / total) * 100).toFixed(0)}%` : "-"; const pc = rs.totalPnl > 0 ? ANSI.green : rs.totalPnl < 0 ? ANSI.red : ANSI.gray; - const ps = rs.totalPnl > 0 ? "+" : ""; + const ps = rs.totalPnl >= 0 ? "+" : "-"; rightHist.push(`${ANSI.dim}Trades: ${ANSI.reset}${total} W:${ANSI.green}${rs.wins}${ANSI.reset} L:${ANSI.red}${rs.losses}${ANSI.reset} WR:${wr}`); - rightHist.push(`${ANSI.dim}PNL:${ANSI.reset} ${pc}${ps}$${rs.totalPnl.toFixed(2)}${ANSI.reset}`); + rightHist.push(`${ANSI.dim}PNL:${ANSI.reset} ${pc}${ps}$${Math.abs(rs.totalPnl).toFixed(2)}${ANSI.reset}`); // Recent closed trades if (d.closedTrades?.length) { for (const t of d.closedTrades.slice(0, 4)) { const color = t.pnl >= 0 ? ANSI.green : ANSI.red; - const pSign = t.pnl >= 0 ? "+" : ""; - const rSign = t.roi >= 0 ? "+" : ""; + const pSign = t.pnl >= 0 ? "+" : "-"; + const rSign = t.roi >= 0 ? "+" : "-"; const sl = t.side === "UP" ? `${ANSI.green}\u2191UP${ANSI.reset}` : `${ANSI.red}\u2193DN${ANSI.reset}`; const ts = new Date(t.ts).toLocaleTimeString("pt-BR", { hour: "2-digit", minute: "2-digit" }); const reasonShort = t.reason === "TAKE_PROFIT" ? "TP" : t.reason === "STOP_LOSS" ? "SL" : t.reason === "SIGNAL_FLIP" ? "FLIP" : t.reason === "TIME_DECAY" ? "TD" : t.reason === "SETTLED_WIN" ? "WIN" : t.reason === "SETTLED_LOSS" ? "LOSS" : (t.reason ?? ""); - rightHist.push(`${ANSI.dim}${ts}${ANSI.reset} ${sl} ${color}${pSign}$${t.pnl.toFixed(2)} ${rSign}${t.roi.toFixed(0)}%${ANSI.reset} ${ANSI.dim}${reasonShort}${ANSI.reset}`); + rightHist.push(`${ANSI.dim}${ts}${ANSI.reset} ${sl} ${color}${pSign}$${Math.abs(t.pnl).toFixed(2)} ${rSign}${Math.abs(t.roi).toFixed(0)}%${ANSI.reset} ${ANSI.dim}${reasonShort}${ANSI.reset}`); } } diff --git a/src/dryRun.js b/src/dryRun.js index a90898ab..38b155b6 100644 --- a/src/dryRun.js +++ b/src/dryRun.js @@ -110,7 +110,7 @@ function evaluateSimExit({ pos, modelUp, modelDown, currentMarketPrice, timeLeft } // Signal flipped with enough conviction - if (modelConfirmsReversal) { + if (!config.disableSignalFlip && modelConfirmsReversal) { return { shouldSell: true, reason: "SIGNAL_FLIP", roiPct }; } @@ -193,6 +193,7 @@ function createSimulator(csvPath, header, config, label = "bot") { side: pos.side, market: pos.marketSlug, entryPrice: pos.entryPrice, exitPrice, roi: roiPct, pnl, cumPnl: cumulativePnl, reason, + totalTrades, wins, losses, }); } @@ -441,6 +442,7 @@ export function createDryRunSimulator5m(csvPath, tradingConfig = {}) { stopLossMinDurationS: tradingConfig.stopLossMinDurationS ?? 120, flipCooldownS: tradingConfig.flipCooldownS ?? 90, flipConfirmTicks: tradingConfig.flipConfirmTicks ?? 5, + disableSignalFlip: tradingConfig.disableSignalFlip ?? true, }; return createSimulator(csvPath, HEADER_5M, config, "5m"); } diff --git a/src/index.js b/src/index.js index 5f618d35..6e64754e 100644 --- a/src/index.js +++ b/src/index.js @@ -256,7 +256,7 @@ async function main() { const clLine = colorPriceLine({ label: "", price: currentPrice, prevPrice: prevCurrentPrice, decimals: 2, prefix: "$" }); const ptbDelta = currentPrice !== null && priceToBeat !== null ? currentPrice - priceToBeat : null; const ptbStr = ptbDelta === null ? "" - : ` (${ptbDelta > 0 ? ANSI.green + "+" : ptbDelta < 0 ? ANSI.red : ANSI.gray}$${Math.abs(ptbDelta).toFixed(2)}${ANSI.reset})`; + : ` (${ptbDelta > 0 ? ANSI.green + "+" : ptbDelta < 0 ? ANSI.red + "-" : ANSI.gray}$${Math.abs(ptbDelta).toFixed(2)}${ANSI.reset})`; const simStats = dryRun.getStats(); diff --git a/src/index5m.js b/src/index5m.js index 8b89cd83..941e98da 100644 --- a/src/index5m.js +++ b/src/index5m.js @@ -289,7 +289,7 @@ async function main() { const clLine = colorPriceLine({ label: "", price: currentPrice, prevPrice: prevCurrentPrice, decimals: 2, prefix: "$" }); const ptbDelta = currentPrice !== null && priceToBeat !== null ? currentPrice - priceToBeat : null; const ptbStr = ptbDelta === null ? "" - : ` (${ptbDelta > 0 ? ANSI.green + "+" : ptbDelta < 0 ? ANSI.red : ANSI.gray}$${Math.abs(ptbDelta).toFixed(2)}${ANSI.reset})`; + : ` (${ptbDelta > 0 ? ANSI.green + "+" : ptbDelta < 0 ? ANSI.red + "-" : ANSI.gray}$${Math.abs(ptbDelta).toFixed(2)}${ANSI.reset})`; const shortcutsHint = trading.tradingEnabled && !keyboard.stdinError ? `${ANSI.dim}[B]${ANSI.reset} Comprar ${ANSI.dim}[S]${ANSI.reset} Vender ${ANSI.dim}[Q]${ANSI.reset} Sair` diff --git a/src/notify.js b/src/notify.js index 7d362d4a..56969415 100644 --- a/src/notify.js +++ b/src/notify.js @@ -34,6 +34,20 @@ export function notify(text) { _send(text); } +/** Format a dollar value with explicit + or - sign. */ +function fmtSignedUsd(v) { + if (v == null || Number.isNaN(v)) return "-"; + const sign = v >= 0 ? "+" : "-"; + return `${sign}$${Math.abs(Number(v)).toFixed(2)}`; +} + +/** Format a percentage with explicit + or - sign. */ +function fmtSignedPct(v) { + if (v == null || Number.isNaN(v)) return "-"; + const sign = v >= 0 ? "+" : "-"; + return `${sign}${Math.abs(Number(v)).toFixed(1)}%`; +} + /** * Notify a simulated or real trade event (BUY or SELL/settlement). * @@ -50,9 +64,13 @@ export function notify(text) { * @param {number} [p.cumPnl] - cumulative PNL after this trade (SELL only) * @param {string} [p.reason] - exit reason (SELL only) * @param {number} [p.invested] - amount invested (BUY only) + * @param {number} [p.totalTrades] - total trades so far (SELL only) + * @param {number} [p.wins] - total wins so far (SELL only) + * @param {number} [p.losses] - total losses so far (SELL only) */ export function notifyTrade({ bot, isLive, action, side, market, entryPrice, - exitPrice, roi, pnl, cumPnl, reason, invested }) { + exitPrice, roi, pnl, cumPnl, reason, invested, + totalTrades, wins, losses }) { const mode = isLive ? "LIVE" : "SIM"; const slug = String(market ?? "").slice(-16); // keep last 16 chars to stay compact const sideArrow = side === "UP" ? "↑ UP" : "↓ DOWN"; @@ -78,16 +96,26 @@ export function notifyTrade({ bot, isLive, action, side, market, entryPrice, const ep = entryPrice != null ? `${(entryPrice * 100).toFixed(1)}¢` : "-"; const xp = exitPrice != null ? `${(exitPrice * 100).toFixed(1)}¢` : "-"; - const roiStr = roi != null ? `${roi >= 0 ? "+" : ""}${Number(roi).toFixed(1)}%` : "-"; - const pnlStr = pnl != null ? `${pnl >= 0 ? "+" : ""}$${Math.abs(pnl).toFixed(2)}` : "-"; - const cumStr = cumPnl != null ? `${cumPnl >= 0 ? "+" : ""}$${Number(cumPnl).toFixed(2)}` : null; - text = [ + const roiStr = fmtSignedPct(roi); + const pnlStr = fmtSignedUsd(pnl); + const cumStr = cumPnl != null ? fmtSignedUsd(cumPnl) : null; + + const lines = [ `${icon} [${bot}·${mode}] SELL — ${reason ?? "MANUAL"}`, `${sideArrow}: ${ep} → ${xp}`, - `ROI: ${roiStr} | PNL: ${pnlStr}`, - cumStr ? `Acum: ${cumStr}` : null, - ].filter(Boolean).join("\n"); + `ROI: ${roiStr} | PNL: ${pnlStr}`, + ]; + + if (totalTrades != null) { + const w = wins ?? 0; + const l = losses ?? 0; + const wr = totalTrades > 0 ? `${((w / totalTrades) * 100).toFixed(0)}%` : "-"; + lines.push(`Trades: ${totalTrades} | W:${w} L:${l} | WR: ${wr}`); + } + if (cumStr) lines.push(`Acum: ${cumStr}`); + + text = lines.join("\n"); } _send(text); @@ -107,10 +135,9 @@ export function notifyStart(bot) { export function notifyDailySummary(bot, stats) { const { wins, losses, totalTrades, cumulativePnl } = stats; const wr = totalTrades > 0 ? `${((wins / totalTrades) * 100).toFixed(0)}%` : "-"; - const cum = `${cumulativePnl >= 0 ? "+" : ""}$${Number(cumulativePnl).toFixed(2)}`; _send([ `📋 [${bot}·SIM] Resumo do dia`, `Trades: ${totalTrades} | W:${wins} L:${losses} | WR: ${wr}`, - `PNL acumulado: ${cum}`, + `PNL acumulado: ${fmtSignedUsd(cumulativePnl)}`, ].join("\n")); } From 577d5f41b89466c1d116825bfbede1cdb438b6c2 Mon Sep 17 00:00:00 2001 From: Jean Carraro Date: Wed, 15 Apr 2026 09:54:17 -0300 Subject: [PATCH 20/49] Skip entry on markets where bot started late (late-start guard) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bot used to enter positions even when it joined a market mid-way, where the price-to-beat reference is unreliable. Now any market where the bot started >90s after open is fully skipped for entry (both sim and live). The display shows 'AGUARD. PRÓX. MERCADO [late start]' in yellow until the next market begins. Co-Authored-By: Claude Opus 4.6 --- src/dryRun.js | 6 +++--- src/index.js | 27 ++++++++++++++++++--------- src/index5m.js | 35 ++++++++++++++++++++++------------- src/trading/executor.js | 6 +++++- 4 files changed, 48 insertions(+), 26 deletions(-) diff --git a/src/dryRun.js b/src/dryRun.js index 38b155b6..2a1df27c 100644 --- a/src/dryRun.js +++ b/src/dryRun.js @@ -258,7 +258,7 @@ function createSimulator(csvPath, header, config, label = "bot") { * @param {number|null} timeLeftMin - minutes remaining in the market * @param {Array} dataValues - indicator CSV columns (everything before sim columns) */ - function tick({ slug, priceToBeat, btcPrice, rec, modelUp, modelDown, marketUp, marketDown, timeLeftMin, dataValues }) { + function tick({ slug, priceToBeat, btcPrice, rec, modelUp, modelDown, marketUp, marketDown, timeLeftMin, dataValues, sawMarketStart = true }) { // ── Market changed → settle open position and flush buffer ────────── if (slug !== currentSlug && currentSlug !== null) { _settlePosition(); @@ -333,8 +333,8 @@ function createSimulator(csvPath, header, config, label = "bot") { simCurrentPrice = currentMktPrice != null ? fmt(currentMktPrice, 4) : ""; simRoiPct = exitEval.roiPct != null ? fmt(exitEval.roiPct, 2) : ""; } - } else if (rec.action === "ENTER" && rec.side) { - // ── BUY — skip if still within post-flip cooldown ───────────────── + } else if (rec.action === "ENTER" && rec.side && sawMarketStart) { + // ── BUY — skip if late start or still within post-flip cooldown ──── const inCooldown = flipCooldownMs > 0 && lastFlipTime !== null && (Date.now() - lastFlipTime) < flipCooldownMs; diff --git a/src/index.js b/src/index.js index 6e64754e..bec37040 100644 --- a/src/index.js +++ b/src/index.js @@ -82,6 +82,10 @@ async function main() { const dryRun = createDryRunSimulator15m("./logs/dryrun_15m.csv", CONFIG.trading); process.on("exit", () => dryRun.flushNow()); + // Late-start guard: skip entering positions on markets the bot didn't see from open + const BOT_START_MS = Date.now(); + const LATE_START_GRACE_MS = 90_000; // 90s grace window + let prevSpotPrice = null; let prevCurrentPrice = null; let usdcBalance = null; @@ -166,6 +170,11 @@ async function main() { // ── Trading ─────────────────────────────────────────────────────────── const marketSlugNow = poly.ok ? String(poly.market?.slug ?? "") : ""; const conditionIdNow = poly.ok ? (poly.market?.conditionId ?? null) : null; + const marketStartMsNow = poly.ok && poly.market?.eventStartTime + ? new Date(poly.market.eventStartTime).getTime() + : null; + // Bot must have been running within LATE_START_GRACE_MS of market open to enter + const sawMarketStart = marketStartMsNow === null || BOT_START_MS <= marketStartMsNow + LATE_START_GRACE_MS; // On market change: redeem any settled tokens from the previous market if (marketSlugNow && marketSlugNow !== prevMarketSlug && prevConditionId && trading.tradingEnabled) { @@ -177,7 +186,7 @@ async function main() { resetIfMarketChanged(marketSlugNow); - await processActionQueue(keyboard.actionQueue, { trading, poly, rec, timeAware, marketSlugNow, botLabel: "15m" }); + await processActionQueue(keyboard.actionQueue, { trading, poly, rec, timeAware, marketSlugNow, botLabel: "15m", sawMarketStart }); if (trading.tradingEnabled && Date.now() - usdcLastFetchMs > 30_000) { usdcLastFetchMs = Date.now(); @@ -190,11 +199,8 @@ async function main() { const spotPrice = wsPrice ?? lastPrice; const currentPrice = chainlink?.price ?? null; const marketSlug = poly.ok ? String(poly.market?.slug ?? "") : ""; - const marketStartMs = poly.ok && poly.market?.eventStartTime - ? new Date(poly.market.eventStartTime).getTime() - : null; - const priceToBeat = priceLatch.update({ marketSlug, currentPrice, marketStartMs, market: poly.market ?? null }); + const priceToBeat = priceLatch.update({ marketSlug, currentPrice, marketStartMs: marketStartMsNow, market: poly.market ?? null }); const settled = tracker.update({ marketSlug, rec, marketUp, marketDown, currentPrice, priceToBeat }); if (settled) { @@ -248,10 +254,12 @@ async function main() { const timeColor = timeLeftMin >= 10 ? ANSI.green : timeLeftMin >= 5 ? ANSI.yellow : ANSI.red; const liquidity = poly.ok ? (Number(poly.market?.liquidityNum) || Number(poly.market?.liquidity) || null) : null; - const recColor = rec.action === "ENTER" ? ANSI.green : ANSI.gray; - const recLabel = rec.action === "ENTER" - ? `\u25BA ${rec.side === "UP" ? "BUY UP" : "BUY DOWN"} [${rec.phase}\u00B7${rec.strength}]` - : `NO TRADE [${rec.phase}]`; + const recColor = !sawMarketStart ? ANSI.yellow : rec.action === "ENTER" ? ANSI.green : ANSI.gray; + const recLabel = !sawMarketStart + ? `AGUARD. PRÓX. MERCADO [late start]` + : rec.action === "ENTER" + ? `\u25BA ${rec.side === "UP" ? "BUY UP" : "BUY DOWN"} [${rec.phase}\u00B7${rec.strength}]` + : `NO TRADE [${rec.phase}]`; const clLine = colorPriceLine({ label: "", price: currentPrice, prevPrice: prevCurrentPrice, decimals: 2, prefix: "$" }); const ptbDelta = currentPrice !== null && priceToBeat !== null ? currentPrice - priceToBeat : null; @@ -366,6 +374,7 @@ async function main() { marketUp, marketDown, timeLeftMin, + sawMarketStart, dataValues: [ new Date().toISOString(), marketSlugNow, diff --git a/src/index5m.js b/src/index5m.js index 941e98da..d7e29d24 100644 --- a/src/index5m.js +++ b/src/index5m.js @@ -86,6 +86,10 @@ async function main() { const dryRun = createDryRunSimulator5m("./logs/dryrun_5m.csv", CONFIG.trading); process.on("exit", () => dryRun.flushNow()); + // Late-start guard: skip entering positions on markets the bot didn't see from open + const BOT_START_MS = Date.now(); + const LATE_START_GRACE_MS = 90_000; // 90s grace window + let signalCooldown = { side: null, ts: 0, slug: null }; let prevSpotPrice = null; let prevCurrentPrice = null; @@ -170,6 +174,11 @@ async function main() { // ── Trading ─────────────────────────────────────────────────────────── const marketSlugNow = poly.ok ? String(poly.market?.slug ?? "") : ""; const conditionIdNow = poly.ok ? (poly.market?.conditionId ?? null) : null; + const marketStartMsNow = poly.ok && poly.market?.eventStartTime + ? new Date(poly.market.eventStartTime).getTime() + : null; + // Bot must have been running within LATE_START_GRACE_MS of market open to enter + const sawMarketStart = marketStartMsNow === null || BOT_START_MS <= marketStartMsNow + LATE_START_GRACE_MS; // On market change: redeem any settled tokens from the previous market if (marketSlugNow && marketSlugNow !== prevMarketSlug && prevConditionId && trading.tradingEnabled) { @@ -194,7 +203,7 @@ async function main() { } resetIfMarketChanged(marketSlugNow); - await processActionQueue(keyboard.actionQueue, { trading, poly, rec, timeAware, marketSlugNow, botLabel: "5m" }); + await processActionQueue(keyboard.actionQueue, { trading, poly, rec, timeAware, marketSlugNow, botLabel: "5m", sawMarketStart }); if (trading.tradingEnabled && Date.now() - usdcLastFetchMs > 30_000) { usdcLastFetchMs = Date.now(); @@ -207,12 +216,9 @@ async function main() { const spotPrice = wsPrice ?? lastPrice; const currentPrice = chainlink?.price ?? null; const marketSlug = poly.ok ? String(poly.market?.slug ?? "") : ""; - const marketStartMs = poly.ok && poly.market?.eventStartTime - ? new Date(poly.market.eventStartTime).getTime() - : null; const settlementMs5m = poly.ok && poly.market?.endDate ? new Date(poly.market.endDate).getTime() : null; - const priceToBeat = priceLatch.update({ marketSlug, currentPrice, marketStartMs, market: poly.market ?? null }); + const priceToBeat = priceLatch.update({ marketSlug, currentPrice, marketStartMs: marketStartMsNow, market: poly.market ?? null }); const settled = tracker.update({ marketSlug, rec, marketUp, marketDown, currentPrice, priceToBeat }); if (settled) { @@ -277,12 +283,14 @@ async function main() { const delta3Narr = narrativeFromSign(delta3m); const signal = rec.action === "ENTER" ? (rec.side === "UP" ? "BUY UP" : "BUY DOWN") : "NO TRADE"; - const recColor = rec.action === "ENTER" ? ANSI.green : ANSI.gray; - const recLabel = rec.action === "ENTER" - ? `\u25BA ${rec.side === "UP" ? "BUY UP" : "BUY DOWN"} [${rec.phase}\u00B7${rec.strength}]` - : `NO TRADE [${rec.phase}]`; - - const isNextMarket = marketStartMs !== null && marketStartMs > Date.now(); + const recColor = !sawMarketStart ? ANSI.yellow : rec.action === "ENTER" ? ANSI.green : ANSI.gray; + const recLabel = !sawMarketStart + ? `AGUARD. PRÓX. MERCADO [late start]` + : rec.action === "ENTER" + ? `\u25BA ${rec.side === "UP" ? "BUY UP" : "BUY DOWN"} [${rec.phase}\u00B7${rec.strength}]` + : `NO TRADE [${rec.phase}]`; + + const isNextMarket = marketStartMsNow !== null && marketStartMsNow > Date.now(); const timeColor = isNextMarket ? ANSI.yellow : timeLeftMin >= 3 ? ANSI.green : timeLeftMin >= 1.5 ? ANSI.yellow : ANSI.red; const liquidity = poly.ok ? (Number(poly.market?.liquidityNum) || Number(poly.market?.liquidity) || null) : null; @@ -296,8 +304,8 @@ async function main() { : `${ANSI.dim}[Q]${ANSI.reset} Sair`; const confirmHint = keyboard.getConfirmHint({ rec, timeAware, marketUp, marketDown, tradeAmount: trading.tradeAmount }); - const intervalLine = (marketStartMs !== null && settlementMs5m !== null) - ? kv("Intervalo:", `${isNextMarket ? ANSI.yellow : ""}${fmtEtHHMM(marketStartMs)} \u2192 ${fmtEtHHMM(settlementMs5m)} ET${isNextMarket ? ANSI.reset : ""}`) + const intervalLine = (marketStartMsNow !== null && settlementMs5m !== null) + ? kv("Intervalo:", `${isNextMarket ? ANSI.yellow : ""}${fmtEtHHMM(marketStartMsNow)} \u2192 ${fmtEtHHMM(settlementMs5m)} ET${isNextMarket ? ANSI.reset : ""}`) : null; const simStats = dryRun.getStats(); @@ -415,6 +423,7 @@ async function main() { marketUp, marketDown, timeLeftMin, + sawMarketStart, dataValues: [ new Date().toISOString(), marketSlugNow, diff --git a/src/trading/executor.js b/src/trading/executor.js index dab32d11..ba8e8b50 100644 --- a/src/trading/executor.js +++ b/src/trading/executor.js @@ -25,13 +25,17 @@ function logError(msg) { * @param {string} ctx.marketSlugNow * @param {Function} [ctx.onSold] - called with { side, entryPrice, exitPrice, pnl, roi } after a sell */ -export async function processActionQueue(actionQueue, { trading, poly, rec, timeAware, marketSlugNow, onSold, botLabel = "bot" }) { +export async function processActionQueue(actionQueue, { trading, poly, rec, timeAware, marketSlugNow, onSold, botLabel = "bot", sawMarketStart = true }) { while (actionQueue.length && trading.tradingEnabled && poly.ok) { const action = actionQueue.shift(); const marketUp = poly.prices.up; const marketDown = poly.prices.down; if (action.type === "buy") { + if (!sawMarketStart) { + setStatusMessage("Late start — aguardando próximo mercado para entrar", 5000); + continue; + } const pos = getPosition(); if (pos.active) { setStatusMessage("Já existe posição aberta"); From 83a4a7bb5c0a650b2e902c170d5b3487f83ab109 Mon Sep 17 00:00:00 2001 From: Jean Carraro Date: Wed, 15 Apr 2026 13:37:28 -0300 Subject: [PATCH 21/49] Use Polymarket outcomePrices for settlement; add PTB safety guard on exits MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Settlement (win/loss determination): - polymarket.js: add fetchMarketOutcome(slug) — reads outcomePrices from Gamma API after market closes; returns UP/DOWN/null - dryRun.js + tracker.js: on slug change, query API for definitive outcome instead of estimating from btcFinal > ptb. Falls back to ptb only if API returns null (market not yet resolved or network error). - index.js + index5m.js: await tracker.update() and dryRun.tick() since settlement path is now async PTB safety guard on exits: - position.js: add btcPrice/priceToBeat/ptbSafeMarginUsd to evaluateExit — suppresses STOP_LOSS, SIGNAL_FLIP, TIME_DECAY when BTC is >= $30 on the winning side of the price-to-beat (absorbs ~$9 ptb drift + buffer) - dryRun.js: same guard in evaluateSimExit - config.js: TRADE_PTB_SAFE_MARGIN_USD env var, default 30 - index.js + index5m.js: pass btcPrice/priceToBeat/ptbSafeMarginUsd to all evaluateExit calls Co-Authored-By: Claude Opus 4.6 --- src/config.js | 3 +++ src/data/polymarket.js | 29 ++++++++++++++++++++++++ src/dryRun.js | 49 +++++++++++++++++++++++++++-------------- src/index.js | 6 +++-- src/index5m.js | 6 +++-- src/trading/position.js | 22 +++++++++++------- src/trading/tracker.js | 36 ++++++++++++++++++------------ 7 files changed, 108 insertions(+), 43 deletions(-) diff --git a/src/config.js b/src/config.js index 3a453e0e..059c9104 100644 --- a/src/config.js +++ b/src/config.js @@ -37,6 +37,9 @@ export const CONFIG = { // Stop-loss guards: require higher conviction + minimum hold time before stopping out stopLossMinProb: Number(process.env.TRADE_SL_MIN_PROB || "0.65"), // min opposite prob to trigger SL stopLossMinDurationS: Number(process.env.TRADE_SL_MIN_DURATION_S || "120"), // seconds position must age before SL fires + // PTB safety guard: suppress SL/SIGNAL_FLIP exits when BTC is this many USD + // on the winning side of the price-to-beat. Absorbs ~$9 ptb drift + buffer. + ptbSafeMarginUsd: Number(process.env.TRADE_PTB_SAFE_MARGIN_USD || "30"), // Cooldown after a SIGNAL_FLIP before re-entering the same market flipCooldownS: Number(process.env.TRADE_FLIP_COOLDOWN_S || "60"), // Consecutive ticks model must confirm reversal before SIGNAL_FLIP fires diff --git a/src/data/polymarket.js b/src/data/polymarket.js index 1f9df2e1..d6853fdd 100644 --- a/src/data/polymarket.js +++ b/src/data/polymarket.js @@ -130,6 +130,35 @@ export async function fetchMarketBySlug(slug) { return market; } +/** + * Fetch the definitive outcome of a resolved market. + * Reads outcomePrices from the Gamma API for the given slug. + * + * Returns "UP" if the Up token resolved to $1, + * "DOWN" if the Down token resolved to $1, + * null if the market is not yet resolved or the fetch failed. + */ +export async function fetchMarketOutcome(slug) { + try { + const market = await fetchMarketBySlug(slug); + if (!market) return null; + const outcomes = Array.isArray(market.outcomes) + ? market.outcomes + : (typeof market.outcomes === "string" ? JSON.parse(market.outcomes) : []); + const outcomePrices = Array.isArray(market.outcomePrices) + ? market.outcomePrices + : (typeof market.outcomePrices === "string" ? JSON.parse(market.outcomePrices) : []); + const upIndex = outcomes.findIndex((o) => String(o).toLowerCase() === "up"); + if (upIndex < 0) return null; + const upPrice = Number(outcomePrices[upIndex]); + if (upPrice >= 0.99) return "UP"; + if (upPrice <= 0.01) return "DOWN"; + return null; // market not fully resolved yet + } catch { + return null; + } +} + export async function fetchMarketsBySeriesSlug({ seriesSlug, limit = 50 }) { const url = new URL("/markets", CONFIG.gammaBaseUrl); url.searchParams.set("seriesSlug", seriesSlug); diff --git a/src/dryRun.js b/src/dryRun.js index 2a1df27c..b30f60fd 100644 --- a/src/dryRun.js +++ b/src/dryRun.js @@ -22,6 +22,7 @@ import fs from "node:fs"; import path from "node:path"; import { ensureDir } from "./utils.js"; import { notifyTrade } from "./notify.js"; +import { fetchMarketOutcome } from "./data/polymarket.js"; // ── Headers ───────────────────────────────────────────────────────────────── @@ -79,7 +80,7 @@ function fmt(v, decimals = 4) { // ── Exit evaluation (inlined to avoid position.js module-level side effects) ─ -function evaluateSimExit({ pos, modelUp, modelDown, currentMarketPrice, timeLeftMin, config }) { +function evaluateSimExit({ pos, modelUp, modelDown, currentMarketPrice, timeLeftMin, config, btcPrice = null, priceToBeat = null }) { if (!pos.active || currentMarketPrice == null) { return { shouldSell: false, reason: null, roiPct: null }; } @@ -93,6 +94,14 @@ function evaluateSimExit({ pos, modelUp, modelDown, currentMarketPrice, timeLeft : null; const modelConfirmsReversal = oppositeProb != null && oppositeProb >= config.signalFlipMinProb; + // PTB safety guard: if BTC is safely on the winning side of the price-to-beat, + // suppress SL/SIGNAL_FLIP/TIME_DECAY — the position is likely to settle as a win. + const ptbSafeMarginUsd = config.ptbSafeMarginUsd ?? 30; + const ptbMargin = (btcPrice != null && priceToBeat != null) + ? (pos.side === "UP" ? btcPrice - priceToBeat : priceToBeat - btcPrice) + : null; + const ptbSafe = ptbMargin !== null && ptbMargin >= ptbSafeMarginUsd; + // Stop-loss guards: higher prob threshold + minimum hold time const slMinProb = config.stopLossMinProb ?? config.signalFlipMinProb; const slConfirmed = oppositeProb != null && oppositeProb >= slMinProb; @@ -104,19 +113,19 @@ function evaluateSimExit({ pos, modelUp, modelDown, currentMarketPrice, timeLeft return { shouldSell: true, reason: "TAKE_PROFIT", roiPct }; } - // Stop loss — requires stricter prob + minimum hold time - if (roiPct <= -config.stopLossPct && slConfirmed && slAgedEnough) { + // Stop loss — suppressed if PTB safe (BTC still on winning side with margin) + if (!ptbSafe && roiPct <= -config.stopLossPct && slConfirmed && slAgedEnough) { return { shouldSell: true, reason: "STOP_LOSS", roiPct }; } - // Signal flipped with enough conviction - if (!config.disableSignalFlip && modelConfirmsReversal) { + // Signal flipped — suppressed if PTB safe + if (!ptbSafe && !config.disableSignalFlip && modelConfirmsReversal) { return { shouldSell: true, reason: "SIGNAL_FLIP", roiPct }; } - // Time decay — only for expensive entries (≥ 50¢) + // Time decay — suppressed if PTB safe; only for expensive entries (≥ 50¢) const entryWasCheap = pos.entryPrice < 0.50; - if (timeLeftMin != null && timeLeftMin < 1.5 && roiPct < -5 && !entryWasCheap) { + if (!ptbSafe && timeLeftMin != null && timeLeftMin < 1.5 && roiPct < -5 && !entryWasCheap) { return { shouldSell: true, reason: "TIME_DECAY", roiPct }; } @@ -197,18 +206,22 @@ function createSimulator(csvPath, header, config, label = "bot") { }); } - function _settlePosition() { + async function _settlePosition() { if (!pos.active) return; - const ptb = lastPriceToBeat; - const btcFinal = lastBtcPrice; - if (ptb == null || btcFinal == null) { - // Can't determine outcome — force close at last known market price (best effort) - _resetPos(); - return; + // Prefer definitive outcome from Polymarket API (outcomePrices) over ptb estimation. + // Falls back to ptb if the API doesn't return a resolved result. + let outcome = await fetchMarketOutcome(currentSlug).catch(() => null); + if (outcome === null) { + const ptb = lastPriceToBeat; + const btcFinal = lastBtcPrice; + if (ptb == null || btcFinal == null) { + _resetPos(); + return; + } + outcome = btcFinal > ptb ? "UP" : "DOWN"; } - const outcome = btcFinal > ptb ? "UP" : "DOWN"; const won = pos.side === outcome; const resolutionPrice = won ? 1.0 : 0.0; const exitValue = pos.shares * resolutionPrice; @@ -258,10 +271,10 @@ function createSimulator(csvPath, header, config, label = "bot") { * @param {number|null} timeLeftMin - minutes remaining in the market * @param {Array} dataValues - indicator CSV columns (everything before sim columns) */ - function tick({ slug, priceToBeat, btcPrice, rec, modelUp, modelDown, marketUp, marketDown, timeLeftMin, dataValues, sawMarketStart = true }) { + async function tick({ slug, priceToBeat, btcPrice, rec, modelUp, modelDown, marketUp, marketDown, timeLeftMin, dataValues, sawMarketStart = true }) { // ── Market changed → settle open position and flush buffer ────────── if (slug !== currentSlug && currentSlug !== null) { - _settlePosition(); + await _settlePosition(); _flush(); lastFlipTime = null; // reset cooldown on new market flipConfirmCount = 0; // reset flip confirmation on new market @@ -289,6 +302,8 @@ function createSimulator(csvPath, header, config, label = "bot") { currentMarketPrice: currentMktPrice, timeLeftMin, config, + btcPrice, + priceToBeat, }); // Apply consecutive-tick confirmation gate for SIGNAL_FLIP diff --git a/src/index.js b/src/index.js index bec37040..cefc58a9 100644 --- a/src/index.js +++ b/src/index.js @@ -202,7 +202,7 @@ async function main() { const priceToBeat = priceLatch.update({ marketSlug, currentPrice, marketStartMs: marketStartMsNow, market: poly.market ?? null }); - const settled = tracker.update({ marketSlug, rec, marketUp, marketDown, currentPrice, priceToBeat }); + const settled = await tracker.update({ marketSlug, rec, marketUp, marketDown, currentPrice, priceToBeat }); if (settled) { const { slug, side, won, pnl } = settled; appendCsvRow(CSV_PATH, CSV_HEADER, [ @@ -283,6 +283,7 @@ async function main() { stopLossMinDurationS: CONFIG.trading.stopLossMinDurationS, flipConfirmCount, flipConfirmTicks: CONFIG.trading.flipConfirmTicks, + btcPrice: currentPrice, priceToBeat, ptbSafeMarginUsd: CONFIG.trading.ptbSafeMarginUsd, }); flipConfirmCount = displayPos.active ? (displayExitEval.flipConfirmCount ?? 0) : 0; } else { @@ -298,6 +299,7 @@ async function main() { stopLossMinDurationS: CONFIG.trading.stopLossMinDurationS, flipConfirmCount: 0, flipConfirmTicks: 1, + btcPrice: currentPrice, priceToBeat, ptbSafeMarginUsd: CONFIG.trading.ptbSafeMarginUsd, }); } @@ -364,7 +366,7 @@ async function main() { // ── Dry-run paper-trading simulator ──────────────────────────────────── { const macdHistVal = macd?.hist ?? null; - dryRun.tick({ + await dryRun.tick({ slug: marketSlugNow, priceToBeat, btcPrice: currentPrice, diff --git a/src/index5m.js b/src/index5m.js index d7e29d24..d992c44d 100644 --- a/src/index5m.js +++ b/src/index5m.js @@ -220,7 +220,7 @@ async function main() { const priceToBeat = priceLatch.update({ marketSlug, currentPrice, marketStartMs: marketStartMsNow, market: poly.market ?? null }); - const settled = tracker.update({ marketSlug, rec, marketUp, marketDown, currentPrice, priceToBeat }); + const settled = await tracker.update({ marketSlug, rec, marketUp, marketDown, currentPrice, priceToBeat }); if (settled) { const { slug, side, won, pnl } = settled; appendCsvRow(CSV_PATH, CSV_HEADER, [ @@ -325,6 +325,7 @@ async function main() { stopLossMinDurationS: CONFIG.trading.stopLossMinDurationS, flipConfirmCount, flipConfirmTicks: CONFIG.trading.flipConfirmTicks, + btcPrice: currentPrice, priceToBeat, ptbSafeMarginUsd: CONFIG.trading.ptbSafeMarginUsd, }); flipConfirmCount = displayPos.active ? (displayExitEval.flipConfirmCount ?? 0) : 0; } else { @@ -340,6 +341,7 @@ async function main() { stopLossMinDurationS: CONFIG.trading.stopLossMinDurationS, flipConfirmCount: 0, flipConfirmTicks: 1, + btcPrice: currentPrice, priceToBeat, ptbSafeMarginUsd: CONFIG.trading.ptbSafeMarginUsd, }); } @@ -413,7 +415,7 @@ async function main() { // ── Dry-run paper-trading simulator ──────────────────────────────────── { - dryRun.tick({ + await dryRun.tick({ slug: marketSlugNow, priceToBeat, btcPrice: currentPrice, diff --git a/src/trading/position.js b/src/trading/position.js index 75bb0356..d571a1b7 100644 --- a/src/trading/position.js +++ b/src/trading/position.js @@ -108,7 +108,7 @@ export function resetIfMarketChanged(currentSlug) { // Avalia se a posição aberta deve ser encerrada. // Retorna { shouldSell, reason, urgency } onde urgency é "HIGH" | "MEDIUM" | null -export function evaluateExit({ position, modelUp, modelDown, currentMarketPrice, timeLeftMin, takeProfitPct, stopLossPct, signalFlipMinProb, stopLossMinProb = null, stopLossMinDurationS = 0, flipConfirmCount = 0, flipConfirmTicks = 1 }) { +export function evaluateExit({ position, modelUp, modelDown, currentMarketPrice, timeLeftMin, takeProfitPct, stopLossPct, signalFlipMinProb, stopLossMinProb = null, stopLossMinDurationS = 0, flipConfirmCount = 0, flipConfirmTicks = 1, btcPrice = null, priceToBeat = null, ptbSafeMarginUsd = 30 }) { if (!position.active || currentMarketPrice == null) { return { shouldSell: false, reason: null, urgency: null, flipConfirmCount: 0 }; } @@ -122,6 +122,13 @@ export function evaluateExit({ position, modelUp, modelDown, currentMarketPrice, : null; const modelConfirmsReversal = oppositeProb != null && oppositeProb >= signalFlipMinProb; + // PTB safety guard: if BTC is safely on the winning side of the price-to-beat, + // suppress SL/SIGNAL_FLIP/TIME_DECAY exits — the position is likely to settle as a win. + const ptbMargin = (btcPrice != null && priceToBeat != null) + ? (position.side === "UP" ? btcPrice - priceToBeat : priceToBeat - btcPrice) + : null; + const ptbSafe = ptbMargin !== null && ptbMargin >= ptbSafeMarginUsd; + // Effective minimum prob for stop-loss (may be stricter than signalFlipMinProb) const slMinProb = stopLossMinProb ?? signalFlipMinProb; const slConfirmed = oppositeProb != null && oppositeProb >= slMinProb; @@ -134,14 +141,14 @@ export function evaluateExit({ position, modelUp, modelDown, currentMarketPrice, return { shouldSell: true, reason: "TAKE_PROFIT", urgency, roiPct, flipConfirmCount: 0 }; } - // 2. Stop loss — requer prob mais alta e duração mínima (se configurado) - if (roiPct <= -stopLossPct && slConfirmed && slAgedEnough) { + // 2. Stop loss — suprimido se PTB seguro (BTC ainda do lado vencedor com margem) + if (!ptbSafe && roiPct <= -stopLossPct && slConfirmed && slAgedEnough) { const urgency = oppositeProb >= 0.65 ? "HIGH" : "MEDIUM"; return { shouldSell: true, reason: "STOP_LOSS", urgency, roiPct, flipConfirmCount: 0 }; } - // 3. Sinal invertido — requer N ticks consecutivos de confirmação - if (modelConfirmsReversal) { + // 3. Sinal invertido — suprimido se PTB seguro; requer N ticks consecutivos + if (!ptbSafe && modelConfirmsReversal) { const newCount = flipConfirmCount + 1; if (newCount >= flipConfirmTicks) { const urgency = oppositeProb >= 0.65 ? "HIGH" : "MEDIUM"; @@ -150,10 +157,9 @@ export function evaluateExit({ position, modelUp, modelDown, currentMarketPrice, return { shouldSell: false, reason: null, urgency: null, roiPct, flipConfirmCount: newCount }; } - // 4. Pouco tempo + perdendo — só aplica se a entrada foi cara (>= 50¢) - // Posições baratas já têm o risco precificado; vale segurar até a resolução + // 4. Pouco tempo + perdendo — suprimido se PTB seguro; só aplica se entrada cara (>= 50¢) const entryWasCheap = position.entryPrice < 0.50; - if (timeLeftMin != null && timeLeftMin < 1.5 && roiPct < -5 && !entryWasCheap) { + if (!ptbSafe && timeLeftMin != null && timeLeftMin < 1.5 && roiPct < -5 && !entryWasCheap) { return { shouldSell: true, reason: "TIME_DECAY", urgency: "MEDIUM", roiPct, flipConfirmCount: 0 }; } diff --git a/src/trading/tracker.js b/src/trading/tracker.js index 5b5de95a..a8e53d9f 100644 --- a/src/trading/tracker.js +++ b/src/trading/tracker.js @@ -10,9 +10,11 @@ * Usage: * const tracker = createTradeTracker(); * // inside the poll loop: - * const settled = tracker.update({ marketSlug, rec, marketUp, marketDown, currentPrice, priceToBeat }); + * const settled = await tracker.update({ marketSlug, rec, marketUp, marketDown, currentPrice, priceToBeat }); * if (settled) appendCsvRow(..., buildSettledRow(settled)); */ +import { fetchMarketOutcome } from "../data/polymarket.js"; + export function createTradeTracker() { let tradeState = { slug: null, @@ -34,24 +36,30 @@ export function createTradeTracker() { * @param {number|null} ctx.currentPrice - live Chainlink price * @param {number|null} ctx.priceToBeat * - * @returns {{ slug, side, won, pnl, ts } | null} settled outcome, or null + * @returns {Promise<{ slug, side, won, pnl, ts } | null>} settled outcome, or null */ - function update({ marketSlug, rec, marketUp, marketDown, currentPrice, priceToBeat }) { + async function update({ marketSlug, rec, marketUp, marketDown, currentPrice, priceToBeat }) { let settled = null; if (tradeState.slug !== null && tradeState.slug !== "" && marketSlug !== tradeState.slug) { // Market changed — evaluate the previous market's outcome - if (tradeState.hasSignal && tradeState.priceToBeat !== null && tradeState.lastChainlinkPrice !== null) { - const winner = tradeState.lastChainlinkPrice > tradeState.priceToBeat ? "UP" : "DOWN"; - const won = tradeState.side === winner; - const ep = tradeState.entryMarketPrice ?? 0.5; - const pnl = won ? (1 / ep) - 1 : -1; - if (won) runningStats.wins += 1; else runningStats.losses += 1; - runningStats.totalPnl += pnl; - const outcome = { slug: tradeState.slug, side: tradeState.side, won, pnl, ts: new Date().toISOString() }; - recentOutcomes.unshift(outcome); - if (recentOutcomes.length > 10) recentOutcomes.pop(); - settled = outcome; + if (tradeState.hasSignal) { + // Prefer definitive outcome from Polymarket API (outcomePrices); fall back to ptb + let winner = await fetchMarketOutcome(tradeState.slug).catch(() => null); + if (winner === null && tradeState.priceToBeat !== null && tradeState.lastChainlinkPrice !== null) { + winner = tradeState.lastChainlinkPrice > tradeState.priceToBeat ? "UP" : "DOWN"; + } + if (winner !== null) { + const won = tradeState.side === winner; + const ep = tradeState.entryMarketPrice ?? 0.5; + const pnl = won ? (1 / ep) - 1 : -1; + if (won) runningStats.wins += 1; else runningStats.losses += 1; + runningStats.totalPnl += pnl; + const outcome = { slug: tradeState.slug, side: tradeState.side, won, pnl, ts: new Date().toISOString() }; + recentOutcomes.unshift(outcome); + if (recentOutcomes.length > 10) recentOutcomes.pop(); + settled = outcome; + } } tradeState = { slug: marketSlug, From f821b16a098815cc57e43db6c77560e405dcac3c Mon Sep 17 00:00:00 2001 From: Jean Carraro Date: Wed, 15 Apr 2026 14:00:54 -0300 Subject: [PATCH 22/49] Add entry price filter and price_to_beat columns to dry-run CSV MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - New TRADE_ENTRY_MIN_PRICE / TRADE_ENTRY_MAX_PRICE env vars (default 0–1, disabled) block entries when the market price of the chosen side is outside the range. Applied in both the paper-trading simulator and live executor. - Add price_to_beat and btc_vs_ptb columns to every tick row in dryrun_15m.csv and dryrun_5m.csv so future analysis can filter by ptb gap without needing a separate data source. - Extend trades CSV with ptb_at_entry, btc_at_entry, btc_vs_ptb_at_entry, market_up_at_entry, market_down_at_entry for per-trade ptb analysis. Co-Authored-By: Claude Sonnet 4.6 --- src/config.js | 5 +++++ src/dryRun.js | 49 +++++++++++++++++++++++++++++++++++------ src/trading/executor.js | 7 ++++++ 3 files changed, 54 insertions(+), 7 deletions(-) diff --git a/src/config.js b/src/config.js index 059c9104..0e055f19 100644 --- a/src/config.js +++ b/src/config.js @@ -40,6 +40,11 @@ export const CONFIG = { // PTB safety guard: suppress SL/SIGNAL_FLIP exits when BTC is this many USD // on the winning side of the price-to-beat. Absorbs ~$9 ptb drift + buffer. ptbSafeMarginUsd: Number(process.env.TRADE_PTB_SAFE_MARGIN_USD || "30"), + // Entry price filter: only enter if the market price of the chosen side is + // within [entryMinMarketPrice, entryMaxMarketPrice]. Default 0–1 (disabled). + // Example: min=0.40, max=0.80 blocks entries when too far against/with market. + entryMinMarketPrice: Number(process.env.TRADE_ENTRY_MIN_PRICE || "0"), + entryMaxMarketPrice: Number(process.env.TRADE_ENTRY_MAX_PRICE || "1"), // Cooldown after a SIGNAL_FLIP before re-entering the same market flipCooldownS: Number(process.env.TRADE_FLIP_COOLDOWN_S || "60"), // Consecutive ticks model must confirm reversal before SIGNAL_FLIP fires diff --git a/src/dryRun.js b/src/dryRun.js index b30f60fd..ab604295 100644 --- a/src/dryRun.js +++ b/src/dryRun.js @@ -26,6 +26,10 @@ import { fetchMarketOutcome } from "./data/polymarket.js"; // ── Headers ───────────────────────────────────────────────────────────────── +// Price-to-beat columns: inserted between indicator cols and sim cols. +// Managed by the simulator from its own priceToBeat/btcPrice params. +const PTB_COLS = ["price_to_beat", "btc_vs_ptb"]; + const SIM_COLS = [ "sim_action", "sim_side", "sim_entry_price", "sim_current_price", "sim_roi_pct", "sim_exit_reason", "sim_pnl", "sim_cum_pnl", @@ -49,13 +53,15 @@ const INDICATOR_COLS_5M = [ "rsi", "ha_color", "ha_count", "vwap", "vwap_dist_pct", "vwap_slope", ]; -export const HEADER_15M = [...INDICATOR_COLS_15M, ...SIM_COLS, ...OUTCOME_COLS]; -export const HEADER_5M = [...INDICATOR_COLS_5M, ...SIM_COLS, ...OUTCOME_COLS]; +export const HEADER_15M = [...INDICATOR_COLS_15M, ...PTB_COLS, ...SIM_COLS, ...OUTCOME_COLS]; +export const HEADER_5M = [...INDICATOR_COLS_5M, ...PTB_COLS, ...SIM_COLS, ...OUTCOME_COLS]; const TRADE_JOURNAL_HEADER = [ "entry_time", "exit_time", "market_slug", "side", "entry_price", "exit_price", "shares", "invested", "exit_value", "pnl", "roi_pct", "exit_reason", "duration_s", + "ptb_at_entry", "btc_at_entry", "btc_vs_ptb_at_entry", + "market_up_at_entry", "market_down_at_entry", ]; // ── CSV helpers ───────────────────────────────────────────────────────────── @@ -143,7 +149,8 @@ function createSimulator(csvPath, header, config, label = "bot") { let buffer = []; // { dataValues[], simCols[] }[] // Virtual position - let pos = { active: false, side: null, entryPrice: 0, shares: 0, invested: 0, marketSlug: null, entryTime: null }; + let pos = { active: false, side: null, entryPrice: 0, shares: 0, invested: 0, marketSlug: null, entryTime: null, + ptbAtEntry: null, btcAtEntry: null, marketUpAtEntry: null, marketDownAtEntry: null }; let cumulativePnl = 0; // Trade stats @@ -161,7 +168,8 @@ function createSimulator(csvPath, header, config, label = "bot") { const flipConfirmTicks = config.flipConfirmTicks ?? 1; function _resetPos() { - pos = { active: false, side: null, entryPrice: 0, shares: 0, invested: 0, marketSlug: null, entryTime: null }; + pos = { active: false, side: null, entryPrice: 0, shares: 0, invested: 0, marketSlug: null, entryTime: null, + ptbAtEntry: null, btcAtEntry: null, marketUpAtEntry: null, marketDownAtEntry: null }; } function _ensureHeader(filePath, headerRow) { @@ -174,6 +182,9 @@ function createSimulator(csvPath, header, config, label = "bot") { function _logTrade({ exitPrice, exitValue, pnl, roiPct, reason, exitTime }) { _ensureHeader(tradesPath, TRADE_JOURNAL_HEADER); const durationS = pos.entryTime ? Math.round((exitTime - pos.entryTime) / 1000) : ""; + const btcVsPtbAtEntry = (pos.btcAtEntry != null && pos.ptbAtEntry != null) + ? pos.btcAtEntry - pos.ptbAtEntry + : null; const row = toCsvLine([ pos.entryTime ? new Date(pos.entryTime).toISOString() : "", new Date(exitTime).toISOString(), @@ -188,6 +199,11 @@ function createSimulator(csvPath, header, config, label = "bot") { fmt(roiPct, 2), reason, durationS, + fmt(pos.ptbAtEntry, 2), + fmt(pos.btcAtEntry, 2), + fmt(btcVsPtbAtEntry, 2), + fmt(pos.marketUpAtEntry, 4), + fmt(pos.marketDownAtEntry, 4), ]); fs.appendFileSync(tradesPath, row + "\n", "utf8"); @@ -244,9 +260,10 @@ function createSimulator(csvPath, header, config, label = "bot") { const canCompute = ptb !== null && btcFinal !== null; const outcome = canCompute ? (btcFinal > ptb ? "UP" : "DOWN") : null; - const lines = buffer.map(({ dataValues, simCols }) => { + const lines = buffer.map(({ dataValues, ptbCols, simCols }) => { return toCsvLine([ ...dataValues, + ...ptbCols, ...simCols, outcome ?? "", btcFinal !== null ? fmt(btcFinal, 2) : "", @@ -284,6 +301,10 @@ function createSimulator(csvPath, header, config, label = "bot") { if (priceToBeat !== null) lastPriceToBeat = priceToBeat; if (btcPrice !== null) lastBtcPrice = btcPrice; + // Capture ptb cols for this tick (used in buffer) + const tickPtb = lastPriceToBeat; + const tickBtcVsPtb = (lastBtcPrice != null && tickPtb != null) ? lastBtcPrice - tickPtb : null; + // ── Simulation logic ──────────────────────────────────────────────── let simAction = "WAIT"; let simSide = ""; @@ -354,7 +375,13 @@ function createSimulator(csvPath, header, config, label = "bot") { (Date.now() - lastFlipTime) < flipCooldownMs; const entryMktPrice = !inCooldown ? (rec.side === "UP" ? marketUp : marketDown) : null; - if (entryMktPrice != null && entryMktPrice > 0) { + + // Entry price filter: skip if market price of entry side is outside allowed range + const minEntry = config.entryMinMarketPrice ?? 0; + const maxEntry = config.entryMaxMarketPrice ?? 1; + const priceAllowed = entryMktPrice != null && entryMktPrice >= minEntry && entryMktPrice <= maxEntry; + + if (priceAllowed && entryMktPrice > 0) { const shares = config.tradeAmount / entryMktPrice; pos = { active: true, @@ -364,6 +391,10 @@ function createSimulator(csvPath, header, config, label = "bot") { invested: config.tradeAmount, marketSlug: slug, entryTime: Date.now(), + ptbAtEntry: tickPtb, + btcAtEntry: lastBtcPrice, + marketUpAtEntry: marketUp ?? null, + marketDownAtEntry: marketDown ?? null, }; flipConfirmCount = 0; @@ -393,7 +424,7 @@ function createSimulator(csvPath, header, config, label = "bot") { fmt(cumulativePnl, 4), ]; - buffer.push({ dataValues, simCols }); + buffer.push({ dataValues, ptbCols: [fmt(tickPtb, 2), fmt(tickBtcVsPtb, 2)], simCols }); } /** Flush current buffer immediately (call on process exit). */ @@ -438,6 +469,8 @@ export function createDryRunSimulator15m(csvPath, tradingConfig = {}) { stopLossMinDurationS: tradingConfig.stopLossMinDurationS ?? 120, flipCooldownS: tradingConfig.flipCooldownS ?? 60, flipConfirmTicks: tradingConfig.flipConfirmTicks ?? 2, + entryMinMarketPrice: tradingConfig.entryMinMarketPrice ?? 0, + entryMaxMarketPrice: tradingConfig.entryMaxMarketPrice ?? 1, }; return createSimulator(csvPath, HEADER_15M, config, "15m"); } @@ -458,6 +491,8 @@ export function createDryRunSimulator5m(csvPath, tradingConfig = {}) { flipCooldownS: tradingConfig.flipCooldownS ?? 90, flipConfirmTicks: tradingConfig.flipConfirmTicks ?? 5, disableSignalFlip: tradingConfig.disableSignalFlip ?? true, + entryMinMarketPrice: tradingConfig.entryMinMarketPrice ?? 0, + entryMaxMarketPrice: tradingConfig.entryMaxMarketPrice ?? 1, }; return createSimulator(csvPath, HEADER_5M, config, "5m"); } diff --git a/src/trading/executor.js b/src/trading/executor.js index ba8e8b50..ac676e2c 100644 --- a/src/trading/executor.js +++ b/src/trading/executor.js @@ -44,6 +44,13 @@ export async function processActionQueue(actionQueue, { trading, poly, rec, time const side = rec.action === "ENTER" ? rec.side : (timeAware.adjustedUp >= timeAware.adjustedDown ? "UP" : "DOWN"); + const entryMktPriceCheck = side === "UP" ? marketUp : marketDown; + const minEntry = trading.entryMinMarketPrice ?? 0; + const maxEntry = trading.entryMaxMarketPrice ?? 1; + if (entryMktPriceCheck != null && (entryMktPriceCheck < minEntry || entryMktPriceCheck > maxEntry)) { + setStatusMessage(`Entrada bloqueada — preço ${(entryMktPriceCheck * 100).toFixed(1)}¢ fora do range [${(minEntry * 100).toFixed(0)}¢–${(maxEntry * 100).toFixed(0)}¢]`, 5000); + continue; + } const tokenId = side === "UP" ? poly.tokens.upTokenId : poly.tokens.downTokenId; const book = side === "UP" ? poly.orderbook.up : poly.orderbook.down; const rawAsk = book?.bestAsk ?? (side === "UP" ? marketUp : marketDown); From bb04bf78078887a5479398582519c33fabd611a1 Mon Sep 17 00:00:00 2001 From: Jean Carraro Date: Thu, 16 Apr 2026 10:34:58 -0300 Subject: [PATCH 23/49] Disable stop-loss on 5m bot: hold-to-settlement is dominant strategy MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Analysis of 161 SL trades showed 78% of SLs correctly exited before a total loss, but the 22% that cut eventual winners cost far more than the savings: real SL PnL was −$75.65 vs hypothetical hold-to-settlement PnL of −$25.64 (+$50 left on the table). With an 85% settled win rate, holding to settlement is the dominant strategy on 5m markets. - config5m.js: add disableStopLoss: true - trading/position.js: add disableStopLoss param to evaluateExit() - dryRun.js: honor disableStopLoss in sim exit logic + 5m factory - index5m.js: pass disableStopLoss to both evaluateExit call sites - CLAUDE.md: document SL disabled rationale alongside existing SIGNAL_FLIP note Co-Authored-By: Claude Sonnet 4.6 --- CLAUDE.md | 6 ++++-- src/config5m.js | 4 ++++ src/dryRun.js | 5 +++-- src/index5m.js | 2 ++ src/trading/position.js | 6 +++--- 5 files changed, 16 insertions(+), 7 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index a4475534..fb91ab78 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -145,9 +145,11 @@ After selling, the simulator can re-enter on a new signal within the same market **Post-flip cooldown:** after a `SIGNAL_FLIP` exit the simulator will not open a new position for `flipCooldownS` seconds (60s on 15m, 90s on 5m). The cooldown resets when a new market starts. -**Stop-loss guards:** both simulators use `stopLossMinProb = 0.65` (stricter than the `signalFlipMinProb` gate) and `stopLossMinDurationS = 120` to avoid being stopped out in the first ~2 minutes of a volatile move. Earlier 5m dry-run analysis showed stops averaging only 79 seconds despite an 86% settled win rate — most would have recovered if held longer. A later 5-day run on 15m confirmed the same pattern (21 stops, 0 wins, −$10), so the 5m guards were ported to the 15m base config. +**Stop-loss (5m — disabled):** `disableStopLoss = true` in config5m.js. Analysis of 161 SL trades showed 78% correctly exited before a total loss, but the 22% that cut eventual winners cost far more than the savings: real SL PnL was −$75.65 vs hypothetical hold-to-settlement PnL of −$25.64 (+$50 left on the table). With an 85% settled win rate, holding to settlement is the dominant strategy on 5m. -**Signal-flip guards (5m):** the 5m simulator raises `signalFlipMinProb` to `0.62` (vs `0.58` on 15m) and `flipConfirmTicks` to `5`. A 5-day dry-run showed 158 SIGNAL_FLIP exits with only 3.8% winning — the lower threshold was catching transient blips across 0.58 that then reverted, cutting positions that would have settled as wins (settled-only win rate is 92.8%). Higher conviction + longer confirmation persistence filters those blips. +**Stop-loss guards (15m):** the 15m simulator uses `stopLossMinProb = 0.65` (stricter than the `signalFlipMinProb` gate) and `stopLossMinDurationS = 120` to avoid being stopped out in the first ~2 minutes of a volatile move. + +**Signal-flip (5m — disabled):** `disableSignalFlip = true`. A 5-day dry-run showed 158 SIGNAL_FLIP exits with only 3.8% winning — the lower threshold was catching transient blips across 0.58 that then reverted, cutting positions that would have settled as wins. **Output files:** diff --git a/src/config5m.js b/src/config5m.js index 52eea6c0..13ce929a 100644 --- a/src/config5m.js +++ b/src/config5m.js @@ -39,5 +39,9 @@ export const CONFIG = { flipCooldownS: Number(process.env.TRADE_FLIP_COOLDOWN_S || "90"), // Require 5 consecutive confirming ticks before exiting on signal flip flipConfirmTicks: Number(process.env.TRADE_FLIP_CONFIRM_TICKS || "5"), + // Disable stop-loss on 5m: data shows 78% of SLs exit before a loss, but the 22% + // that cut winners cost more than the savings. Holding to settlement is +$50 better + // across 161 SL trades. The 85% settled win rate makes hold-to-settlement dominant. + disableStopLoss: true, }, }; diff --git a/src/dryRun.js b/src/dryRun.js index ab604295..49593ee0 100644 --- a/src/dryRun.js +++ b/src/dryRun.js @@ -119,8 +119,8 @@ function evaluateSimExit({ pos, modelUp, modelDown, currentMarketPrice, timeLeft return { shouldSell: true, reason: "TAKE_PROFIT", roiPct }; } - // Stop loss — suppressed if PTB safe (BTC still on winning side with margin) - if (!ptbSafe && roiPct <= -config.stopLossPct && slConfirmed && slAgedEnough) { + // Stop loss — suppressed if PTB safe or disabled by config + if (!ptbSafe && !config.disableStopLoss && roiPct <= -config.stopLossPct && slConfirmed && slAgedEnough) { return { shouldSell: true, reason: "STOP_LOSS", roiPct }; } @@ -491,6 +491,7 @@ export function createDryRunSimulator5m(csvPath, tradingConfig = {}) { flipCooldownS: tradingConfig.flipCooldownS ?? 90, flipConfirmTicks: tradingConfig.flipConfirmTicks ?? 5, disableSignalFlip: tradingConfig.disableSignalFlip ?? true, + disableStopLoss: tradingConfig.disableStopLoss ?? false, entryMinMarketPrice: tradingConfig.entryMinMarketPrice ?? 0, entryMaxMarketPrice: tradingConfig.entryMaxMarketPrice ?? 1, }; diff --git a/src/index5m.js b/src/index5m.js index d992c44d..01f661b8 100644 --- a/src/index5m.js +++ b/src/index5m.js @@ -326,6 +326,7 @@ async function main() { flipConfirmCount, flipConfirmTicks: CONFIG.trading.flipConfirmTicks, btcPrice: currentPrice, priceToBeat, ptbSafeMarginUsd: CONFIG.trading.ptbSafeMarginUsd, + disableStopLoss: CONFIG.trading.disableStopLoss ?? false, }); flipConfirmCount = displayPos.active ? (displayExitEval.flipConfirmCount ?? 0) : 0; } else { @@ -342,6 +343,7 @@ async function main() { flipConfirmCount: 0, flipConfirmTicks: 1, btcPrice: currentPrice, priceToBeat, ptbSafeMarginUsd: CONFIG.trading.ptbSafeMarginUsd, + disableStopLoss: CONFIG.trading.disableStopLoss ?? false, }); } diff --git a/src/trading/position.js b/src/trading/position.js index d571a1b7..b4399635 100644 --- a/src/trading/position.js +++ b/src/trading/position.js @@ -108,7 +108,7 @@ export function resetIfMarketChanged(currentSlug) { // Avalia se a posição aberta deve ser encerrada. // Retorna { shouldSell, reason, urgency } onde urgency é "HIGH" | "MEDIUM" | null -export function evaluateExit({ position, modelUp, modelDown, currentMarketPrice, timeLeftMin, takeProfitPct, stopLossPct, signalFlipMinProb, stopLossMinProb = null, stopLossMinDurationS = 0, flipConfirmCount = 0, flipConfirmTicks = 1, btcPrice = null, priceToBeat = null, ptbSafeMarginUsd = 30 }) { +export function evaluateExit({ position, modelUp, modelDown, currentMarketPrice, timeLeftMin, takeProfitPct, stopLossPct, signalFlipMinProb, stopLossMinProb = null, stopLossMinDurationS = 0, flipConfirmCount = 0, flipConfirmTicks = 1, btcPrice = null, priceToBeat = null, ptbSafeMarginUsd = 30, disableStopLoss = false }) { if (!position.active || currentMarketPrice == null) { return { shouldSell: false, reason: null, urgency: null, flipConfirmCount: 0 }; } @@ -141,8 +141,8 @@ export function evaluateExit({ position, modelUp, modelDown, currentMarketPrice, return { shouldSell: true, reason: "TAKE_PROFIT", urgency, roiPct, flipConfirmCount: 0 }; } - // 2. Stop loss — suprimido se PTB seguro (BTC ainda do lado vencedor com margem) - if (!ptbSafe && roiPct <= -stopLossPct && slConfirmed && slAgedEnough) { + // 2. Stop loss — suprimido se PTB seguro ou desabilitado por config + if (!ptbSafe && !disableStopLoss && roiPct <= -stopLossPct && slConfirmed && slAgedEnough) { const urgency = oppositeProb >= 0.65 ? "HIGH" : "MEDIUM"; return { shouldSell: true, reason: "STOP_LOSS", urgency, roiPct, flipConfirmCount: 0 }; } From 78e60ecbccb98b79902f0be60c83f10be4c36ebc Mon Sep 17 00:00:00 2001 From: Jean Carraro Date: Thu, 16 Apr 2026 19:36:50 -0300 Subject: [PATCH 24/49] Add STRATEGY_LOG.md: catalog all strategy versions and dry-run performance Documents 9 archive snapshots with parameters, win rates, PnL, and the key decisions (disabling 5m SIGNAL_FLIP and STOP_LOSS) with their evidence. Co-Authored-By: Claude Sonnet 4.6 --- STRATEGY_LOG.md | 221 ++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 221 insertions(+) create mode 100644 STRATEGY_LOG.md diff --git a/STRATEGY_LOG.md b/STRATEGY_LOG.md new file mode 100644 index 00000000..3d402552 --- /dev/null +++ b/STRATEGY_LOG.md @@ -0,0 +1,221 @@ +# Strategy Log — Polymarket BTC Assistant + +Cada entrada documenta o estado da estratégia em um snapshot de logs, com parâmetros-chave e desempenho do paper-trading acumulado até aquele ponto. Os snapshots são os arquivos em `logs/archive/`. + +**Nota sobre os dados:** os snapshots de curto prazo (< 20 trades) têm alta variância estatística e não são representativos. + +--- + +## Versão atual + +**Ref:** `logs/` (live) +**Data:** 2026-04-16 (em andamento) + +### Mudança vs anterior +- 5m: stop-loss desabilitado (`disableStopLoss = true`). Análise de 161 trades SL mostrou 78% de saídas corretas, mas o 22% que cortou winners custou muito mais (SL real: −$75.65 vs hold-to-settlement: −$25.64). Taxa de win settled 5m é ~85%, logo hold até settlement é dominante. + +### Parâmetros — 15m +| Parâmetro | Valor | +|---|---| +| `takeProfitPct` | 20% | +| `stopLossPct` | 25% | +| `signalFlipMinProb` | 0.58 | +| `stopLossMinProb` | 0.65 | +| `stopLossMinDurationS` | 120s | +| `flipCooldownS` | 60s | +| `flipConfirmTicks` | 2 | +| `disableStopLoss` | false | +| `disableSignalFlip` | false | + +### Parâmetros — 5m +| Parâmetro | Valor | +|---|---| +| `takeProfitPct` | 20% | +| `stopLossPct` | 25% | +| `signalFlipMinProb` | 0.62 | +| `stopLossMinProb` | 0.65 | +| `stopLossMinDurationS` | 120s | +| `flipCooldownS` | 90s | +| `flipConfirmTicks` | 5 | +| `disableStopLoss` | **true** | +| `disableSignalFlip` | **true** | + +### Desempenho (paper-trading acumulado) +| Bot | Trades | Win | Loss | Win Rate | PnL | +|---|---|---|---|---|---| +| 15m | 34 | 17 | 17 | 50.0% | +$4.73 | +| 5m | 105 | 49 | 56 | 46.7% | +$3.33 | + +### Exit reasons — 5m +| Razão | Count | +|---|---| +| SETTLED_WIN | 49 | +| TIME_DECAY | 28 | +| SETTLED_LOSS | 28 | + +--- + +## Snapshot: `2026-04-16_pre-disable-5m-stoploss` + +**Mudança introduzida depois:** desabilitar stop-loss no 5m. + +### Parâmetros — 5m (diferenças) +| Parâmetro | Valor | +|---|---| +| `disableStopLoss` | false ← principal diferença | +| `disableSignalFlip` | true | +| `signalFlipMinProb` | 0.62 | +| `flipConfirmTicks` | 5 | + +### Desempenho +| Bot | Trades | Win | Loss | Win Rate | PnL | +|---|---|---|---|---|---| +| 15m | 44 | 22 | 22 | 50.0% | +$11.34 | +| 5m | 71 | 30 | 41 | 42.3% | +$4.52 | + +### Exit reasons — 5m +| Razão | Count | +|---|---| +| STOP_LOSS | 26 | +| SETTLED_WIN | 26 | +| TIME_DECAY | 10 | +| SETTLED_LOSS | 5 | +| TAKE_PROFIT | 4 | + +**Observação:** 26 stop-losses num total de 71 trades (37%) — muitas saídas prematuras. Análise revelou que hold-to-settlement era melhor na maioria dos casos. + +--- + +## Snapshot: `2026-04-15_pre-outcome-api-and-ptb-guard` + +**Mudança introduzida depois:** usar `outcomePrices` da API Polymarket para settlement; PTB safety guard (suprime SL/FLIP quando BTC está dentro de $30 do preço de referência). + +### Desempenho +| Bot | Trades | Win | Loss | Win Rate | PnL | +|---|---|---|---|---|---| +| 15m | 16 | 4 | 12 | 25.0% | −$4.44 | +| 5m | 52 | 23 | 29 | 44.2% | +$2.28 | + +--- + +## Snapshot: `2026-04-15_pre-late-start-guard` + +**Mudança introduzida depois:** ignorar mercados onde o bot começou tarde (>30s após abertura) para evitar entrar sem dado de preço de referência. + +### Desempenho +| Bot | Trades | Win | Loss | Win Rate | PnL | +|---|---|---|---|---|---| +| 15m | 2 | 2 | 0 | 100.0% | +$1.93 | +| 5m | 8 | 3 | 5 | 37.5% | −$0.44 | + +*(amostra muito pequena)* + +--- + +## Snapshot: `2026-04-15_pre-disable-5m-flip-and-sign-fixes` + +**Mudança introduzida depois:** desabilitar SIGNAL_FLIP no 5m (`disableSignalFlip = true`); correções de formatação de sinal; estatísticas de trades nas notificações. + +### Parâmetros relevantes — 5m (antes) +| Parâmetro | Valor | +|---|---| +| `disableSignalFlip` | false ← principal diferença | +| `signalFlipMinProb` | 0.62 | + +### Desempenho +| Bot | Trades | Win | Loss | Win Rate | PnL | +|---|---|---|---|---|---| +| 15m | 44 | 13 | 31 | 29.5% | −$2.89 | +| 5m | 119 | 40 | 79 | 33.6% | +$2.68 | + +**Observação:** 5m com SIGNAL_FLIP habilitado mas limiar em 0.62 ainda produzia muitas saídas ruins. + +--- + +## Snapshot: `2026-04-14_pre-telegram-notify` + +**Mudança introduzida depois:** notificações Telegram para eventos de trade. + +### Desempenho +| Bot | Trades | Win | Loss | Win Rate | PnL | +|---|---|---|---|---|---| +| 15m | 19 | 12 | 7 | 63.2% | +$4.92 | +| 5m | 163 | 53 | 110 | 32.5% | +$18.82 | + +**Observação:** melhor período documentado para o 15m (63% win rate). 5m com 163 trades é a maior amostra acumulada de um único período. + +--- + +## Snapshot: `2026-04-13_pre-tightened-exits` + +**Mudança introduzida depois:** apertar limiares de saída — elevar `signalFlipMinProb` 5m de 0.58→0.62, adicionar `flipConfirmTicks` 5m=5, aumentar `flipCooldownS` 5m de 60→90s. + +### Parâmetros — 5m (antes do aperto) +| Parâmetro | Valor | +|---|---| +| `signalFlipMinProb` | 0.58 | +| `flipConfirmTicks` | 2 | +| `flipCooldownS` | 60s | +| `disableSignalFlip` | false | + +### Desempenho +| Bot | Trades | Win | Loss | Win Rate | PnL | +|---|---|---|---|---|---| +| 15m | 94 | 32 | 62 | 34.0% | +$0.64 | +| 5m | 283 | 100 | 183 | 35.3% | +$28.73 | + +### Exit reasons — 5m +| Razão | Count | +|---|---| +| SIGNAL_FLIP | 160 | +| SETTLED_WIN | 77 | +| TIME_DECAY | 18 | +| TAKE_PROFIT | 16 | +| STOP_LOSS | 6 | +| SETTLED_LOSS | 6 | + +**Observação:** 160 SIGNAL_FLIPs com taxa de win 3.8% (de análise separada) — evidência clara de que o limiar de 0.58 era baixo demais para o 5m. PnL positivo apesar da estratégia ruim porque o volume era alto. + +--- + +## Snapshot: `2026-04-09_pre-cooldown-sl-fix` + +**Mudança introduzida depois:** cooldown pós-flip e guards de stop-loss (mínimo de idade de posição, prob mínima mais alta para SL). + +### Parâmetros — antes do fix +| Parâmetro | Valor | +|---|---| +| `stopLossMinProb` | igual a `signalFlipMinProb` (sem guard separado) | +| `stopLossMinDurationS` | 0 (sem guard de idade) | +| `flipCooldownS` | 0 (sem cooldown) | + +### Desempenho +| Bot | Trades | Win | Loss | Win Rate | PnL | +|---|---|---|---|---|---| +| 15m | 93 | 34 | 59 | 36.6% | +$2.75 | +| 5m | 395 | 155 | 240 | 39.2% | +$42.54 | + +**Observação:** maior volume de trades documentado (395 no 5m). PnL absoluto alto pelo volume, mas win rate baixa indica que muitos trades eram ruídos — o flip sem cooldown permitia re-entrar imediatamente após sair. + +--- + +## Resumo comparativo + +| Snapshot | 15m Trades | 15m Win% | 15m PnL | 5m Trades | 5m Win% | 5m PnL | +|---|---|---|---|---|---|---| +| `pre-cooldown-sl-fix` | 93 | 36.6% | +$2.75 | 395 | 39.2% | +$42.54 | +| `pre-tightened-exits` | 94 | 34.0% | +$0.64 | 283 | 35.3% | +$28.73 | +| `pre-telegram-notify` | 19 | 63.2% | +$4.92 | 163 | 32.5% | +$18.82 | +| `pre-disable-5m-flip` | 44 | 29.5% | −$2.89 | 119 | 33.6% | +$2.68 | +| `pre-outcome-api-ptb` | 16 | 25.0% | −$4.44 | 52 | 44.2% | +$2.28 | +| `pre-disable-5m-stoploss` | 44 | 50.0% | +$11.34 | 71 | 42.3% | +$4.52 | +| **atual** | 34 | 50.0% | +$4.73 | 105 | 46.7% | +$3.33 | + +### Principais decisões estratégicas e aprendizados + +| Data | Decisão | Evidência | +|---|---|---| +| ~2026-04-09 | Adicionar cooldown pós-flip + guards de SL (prob mínima + idade mínima) | Re-entradas imediatas geravam ruído e trades de baixa qualidade | +| ~2026-04-13 | Elevar `signalFlipMinProb` 5m: 0.58→0.62; `flipConfirmTicks` 5m: 2→5 | 158 SIGNAL_FLIPs com 3.8% win rate — limiar muito baixo capturava blips transitórios | +| ~2026-04-15 | Desabilitar SIGNAL_FLIP no 5m por completo | Mesmo com 0.62, a taxa de exit prematuro ainda era alta demais | +| ~2026-04-16 | Desabilitar STOP_LOSS no 5m | 161 SL trades: 78% corretos mas 22% cortou winners; hold-to-settlement domina com 85% win rate settled | From d44c75c25f51ab38a675c04da32335a8706f1273 Mon Sep 17 00:00:00 2001 From: Jean Carraro Date: Fri, 17 Apr 2026 23:12:40 -0300 Subject: [PATCH 25/49] Activate entry filter, disable 15m FLIP, add high-conviction sizing - Entry price filter now gated by defaults (15m 0.45-0.58, 5m 0.50-0.60), skipping low-confidence zones identified from dry-run analysis. - 15m SIGNAL_FLIP exits disabled (was bleeding -$9.21 across 26 trades). - 5m TIME_DECAY tightened to 2.5min/15% (was cutting near -80% ROI with little recovery opportunity). - New asymmetric position sizing: 15m multiplies tradeAmount by 2x when entry is 0.45-0.50 AND model confidence >= 0.70 (disabled on 5m). - Fix: client.js now spreads config.trading so executor/simulator see entryMinMarketPrice, highConvictionMultiplier, and related params. - Add scripts/report.js + npm run report for post-change PnL review. - STRATEGY_LOG.md documents all params + routine to run after changes. Co-Authored-By: Claude Opus 4.7 --- .env.example | 62 +++++++++++++++++- STRATEGY_LOG.md | 140 +++++++++++++++++++++++++++++----------- package.json | 3 +- scripts/report.js | 126 ++++++++++++++++++++++++++++++++++++ src/config.js | 25 +++++-- src/config5m.js | 17 ++++- src/dryRun.js | 40 ++++++++++-- src/index.js | 29 +++++---- src/index5m.js | 31 ++++----- src/trading/client.js | 9 ++- src/trading/executor.js | 21 ++++-- src/trading/position.js | 8 +-- src/trading/sizing.js | 26 ++++++++ 13 files changed, 447 insertions(+), 90 deletions(-) create mode 100644 scripts/report.js create mode 100644 src/trading/sizing.js diff --git a/.env.example b/.env.example index 15667e64..013e4f94 100644 --- a/.env.example +++ b/.env.example @@ -46,10 +46,70 @@ # Só dispara se o modelo também confirmar reversão. # TRADE_STOP_LOSS_PCT=25 -# Probabilidade mínima do lado oposto para considerar que o modelo inverteu (padrão: 0.58) +# Probabilidade mínima do lado oposto para considerar que o modelo inverteu (padrão: 0.58 / 5m: 0.62) # Usado como gatilho para TP, SL e SIGNAL_FLIPPED. # TRADE_SIGNAL_FLIP_PROB=0.58 +# Prob mínima específica para stop-loss (padrão: 0.65) — pode ser mais rígida que signalFlipMinProb +# TRADE_SL_MIN_PROB=0.65 + +# Duração mínima da posição antes de SL poder disparar, em segundos (padrão: 120) +# TRADE_SL_MIN_DURATION_S=120 + +# Cooldown pós-SIGNAL_FLIP antes de re-entrar no mesmo mercado, em segundos (padrão: 60 / 5m: 90) +# TRADE_FLIP_COOLDOWN_S=60 + +# Ticks consecutivos confirmando reversão antes de SIGNAL_FLIP disparar (padrão: 2 / 5m: 5) +# TRADE_FLIP_CONFIRM_TICKS=2 + +# Margem de segurança PTB: suprime SL/FLIP/TIME_DECAY se BTC está pelo menos X USD +# do lado vencedor do preço de referência (padrão: 30) +# TRADE_PTB_SAFE_MARGIN_USD=30 + +# Desabilitar saídas (padrão 15m: flip=true, sl=false | padrão 5m: flip=true, sl=true) +# TRADE_DISABLE_SIGNAL_FLIP=true +# TRADE_DISABLE_STOP_LOSS=false +# TRADE_DISABLE_SIGNAL_FLIP_5M=true +# TRADE_DISABLE_STOP_LOSS_5M=true + +# ───────────────────────────────────────────── +# Filtro de preço de entrada (entry price gating) +# ───────────────────────────────────────────── +# Só entra quando o preço de mercado do lado escolhido está em [MIN, MAX]. +# Defaults baseados em análise de dry-run: +# 15m: faixa 0.45–0.58 (exclui entradas <0.45 que perdem $3.77) +# 5m : faixa 0.50–0.60 (exclui zonas neutras 0.45–0.49 que perdem $2.40) +# Variáveis distintas por bot: o 5m tem seu próprio par *_5M. +# TRADE_ENTRY_MIN_PRICE=0.45 +# TRADE_ENTRY_MAX_PRICE=0.58 +# TRADE_ENTRY_MIN_PRICE_5M=0.50 +# TRADE_ENTRY_MAX_PRICE_5M=0.60 + +# ───────────────────────────────────────────── +# TIME_DECAY (saída por tempo restante + loss) +# ───────────────────────────────────────────── +# Dispara quando: tempo restante (min) < MIN_LEFT_MIN AND ROI < -MIN_LOSS_PCT +# Só se aplica a entradas ≥ $0.50 (as baratas seguram até settlement). +# 15m: 1.5 min / 5% (configuração clássica) +# 5m : 2.5 min / 15% (mais conservador: recorta só quem já perdeu muito) +# TRADE_TIME_DECAY_MIN_LEFT_MIN=1.5 +# TRADE_TIME_DECAY_MIN_LOSS_PCT=5 +# TRADE_TIME_DECAY_MIN_LEFT_MIN_5M=2.5 +# TRADE_TIME_DECAY_MIN_LOSS_PCT_5M=15 + +# ───────────────────────────────────────────── +# Sizing de alta convicção (high-conviction position sizing) +# ───────────────────────────────────────────── +# Multiplica o tradeAmount quando entryPrice ∈ [ENTRY_MIN, ENTRY_MAX] +# AND prob do lado escolhido ≥ MIN_PROB. MULT=1 desativa a feature. +# 15m: MULT=2 (ativo) — aproveita entradas baratas com alta convicção +# 5m : MULT=1 (desativado) — ruído mais alto, sem evidência para upsizing +# TRADE_HIGH_CONVICTION_MULT=2 +# TRADE_HIGH_CONVICTION_MIN_PROB=0.70 +# TRADE_HIGH_CONVICTION_ENTRY_MIN=0.45 +# TRADE_HIGH_CONVICTION_ENTRY_MAX=0.50 +# TRADE_HIGH_CONVICTION_MULT_5M=1 + # ───────────────────────────────────────────── # Chainlink / Polygon RPC # ───────────────────────────────────────────── diff --git a/STRATEGY_LOG.md b/STRATEGY_LOG.md index 3d402552..401ca8d0 100644 --- a/STRATEGY_LOG.md +++ b/STRATEGY_LOG.md @@ -6,52 +6,118 @@ Cada entrada documenta o estado da estratégia em um snapshot de logs, com parâ --- +## Comandos úteis + +Rodar os bots (cada um lê `--env-file=.env` automaticamente): +```bash +npm start # 15m bot +npm run start:5m # 5m bot +``` + +**Gerar relatório de performance acumulada** (sempre que trocar parâmetros, rodar para confirmar o baseline): +```bash +npm run report +``` +O comando lê `logs/dryrun_15m_trades.csv` e `logs/dryrun_5m_trades.csv` e imprime win rate, PnL, profit factor, breakdown por motivo de saída, por lado, maior streak de wins/losses, ROI máximo/mínimo e duração média. É o indicador mais rápido para saber se uma mudança de parâmetro moveu a estratégia na direção esperada. + +**Fluxo recomendado ao mudar parâmetro:** +1. Arquivar os logs atuais em `logs/archive/_pre-/` e registrar um snapshot aqui. +2. Editar o parâmetro (via `.env` ou default em `src/config*.js`). +3. Rodar o bot por ≥ 24h para acumular amostra mínima (>20 trades). +4. Rodar `npm run report` e comparar com o snapshot anterior nesta tabela. + +--- + ## Versão atual **Ref:** `logs/` (live) -**Data:** 2026-04-16 (em andamento) +**Data:** 2026-04-17 -### Mudança vs anterior -- 5m: stop-loss desabilitado (`disableStopLoss = true`). Análise de 161 trades SL mostrou 78% de saídas corretas, mas o 22% que cortou winners custou muito mais (SL real: −$75.65 vs hold-to-settlement: −$25.64). Taxa de win settled 5m é ~85%, logo hold até settlement é dominante. +### Mudanças vs anterior +Introduzidas após análise dos 51 trades 15m / 69 trades 5m registrados em 2026-04-17 (baseline arquivado em `logs/archive/2026-04-17_pre-entry-filter-and-15m-flip-disable/`): +1. **Ativado filtro de preço de entrada em ambos os bots** (`entryMinMarketPrice` / `entryMaxMarketPrice`). Estava disponível mas desativado (0–1). Defaults agora baseados em análise por faixa de PnL. +2. **Desabilitado SIGNAL_FLIP no 15m** (`disableSignalFlip = true`). 25 flips causaram −$9.00 (51% dos trades) — mesmo padrão que justificou desabilitar no 5m. +3. **Endurecido TIME_DECAY no 5m**: `timeLeftMin < 2.5` + loss `> 15%` (antes: <1.5 + >5%). TIME_DECAY custou −$13.25 em 17 trades, a maioria com recuperação negligenciável no final. +4. **Sizing de alta convicção no 15m**: `highConvictionMultiplier = 2` quando entrada ∈ [0.45, 0.50] e prob do lado escolhido ≥ 0.70. Aproveita a faixa 0.45–0.49 que historicamente traz +$1.28 em 26 trades. ### Parâmetros — 15m -| Parâmetro | Valor | -|---|---| -| `takeProfitPct` | 20% | -| `stopLossPct` | 25% | -| `signalFlipMinProb` | 0.58 | -| `stopLossMinProb` | 0.65 | -| `stopLossMinDurationS` | 120s | -| `flipCooldownS` | 60s | -| `flipConfirmTicks` | 2 | -| `disableStopLoss` | false | -| `disableSignalFlip` | false | +| Parâmetro | Valor | Origem | +|---|---|---| +| `tradeAmount` | $1 (base) | `POLYMARKET_TRADE_AMOUNT` | +| `takeProfitPct` | 20% | `TRADE_TAKE_PROFIT_PCT` | +| `stopLossPct` | 25% | `TRADE_STOP_LOSS_PCT` | +| `signalFlipMinProb` | 0.58 | `TRADE_SIGNAL_FLIP_PROB` | +| `stopLossMinProb` | 0.65 | `TRADE_SL_MIN_PROB` | +| `stopLossMinDurationS` | 120s | `TRADE_SL_MIN_DURATION_S` | +| `flipCooldownS` | 60s | `TRADE_FLIP_COOLDOWN_S` | +| `flipConfirmTicks` | 2 | `TRADE_FLIP_CONFIRM_TICKS` | +| `ptbSafeMarginUsd` | 30 | `TRADE_PTB_SAFE_MARGIN_USD` | +| `disableStopLoss` | false | `TRADE_DISABLE_STOP_LOSS` | +| `disableSignalFlip` | **true** ✱ | `TRADE_DISABLE_SIGNAL_FLIP` | +| `entryMinMarketPrice` | **0.45** ✱ | `TRADE_ENTRY_MIN_PRICE` | +| `entryMaxMarketPrice` | **0.58** ✱ | `TRADE_ENTRY_MAX_PRICE` | +| `timeDecayMinLeftMin` | 1.5 min | `TRADE_TIME_DECAY_MIN_LEFT_MIN` | +| `timeDecayMinLossPct` | 5% | `TRADE_TIME_DECAY_MIN_LOSS_PCT` | +| `highConvictionMultiplier` | **2×** ✱ | `TRADE_HIGH_CONVICTION_MULT` | +| `highConvictionMinProb` | 0.70 | `TRADE_HIGH_CONVICTION_MIN_PROB` | +| `highConvictionEntryMin` | 0.45 | `TRADE_HIGH_CONVICTION_ENTRY_MIN` | +| `highConvictionEntryMax` | 0.50 | `TRADE_HIGH_CONVICTION_ENTRY_MAX` | ### Parâmetros — 5m -| Parâmetro | Valor | -|---|---| -| `takeProfitPct` | 20% | -| `stopLossPct` | 25% | -| `signalFlipMinProb` | 0.62 | -| `stopLossMinProb` | 0.65 | -| `stopLossMinDurationS` | 120s | -| `flipCooldownS` | 90s | -| `flipConfirmTicks` | 5 | -| `disableStopLoss` | **true** | -| `disableSignalFlip` | **true** | - -### Desempenho (paper-trading acumulado) -| Bot | Trades | Win | Loss | Win Rate | PnL | -|---|---|---|---|---|---| -| 15m | 34 | 17 | 17 | 50.0% | +$4.73 | -| 5m | 105 | 49 | 56 | 46.7% | +$3.33 | +| Parâmetro | Valor | Origem | +|---|---|---| +| `tradeAmount` | $1 (base) | `POLYMARKET_TRADE_AMOUNT` | +| `takeProfitPct` | 20% | `TRADE_TAKE_PROFIT_PCT` | +| `stopLossPct` | 25% | `TRADE_STOP_LOSS_PCT` | +| `signalFlipMinProb` | 0.62 | `TRADE_SIGNAL_FLIP_PROB` | +| `stopLossMinProb` | 0.65 | `TRADE_SL_MIN_PROB` | +| `stopLossMinDurationS` | 120s | `TRADE_SL_MIN_DURATION_S` | +| `flipCooldownS` | 90s | `TRADE_FLIP_COOLDOWN_S` | +| `flipConfirmTicks` | 5 | `TRADE_FLIP_CONFIRM_TICKS` | +| `ptbSafeMarginUsd` | 30 | `TRADE_PTB_SAFE_MARGIN_USD` | +| `disableStopLoss` | **true** | `TRADE_DISABLE_STOP_LOSS_5M` | +| `disableSignalFlip` | **true** | `TRADE_DISABLE_SIGNAL_FLIP_5M` | +| `entryMinMarketPrice` | **0.50** ✱ | `TRADE_ENTRY_MIN_PRICE_5M` | +| `entryMaxMarketPrice` | **0.60** ✱ | `TRADE_ENTRY_MAX_PRICE_5M` | +| `timeDecayMinLeftMin` | **2.5 min** ✱ | `TRADE_TIME_DECAY_MIN_LEFT_MIN_5M` | +| `timeDecayMinLossPct` | **15%** ✱ | `TRADE_TIME_DECAY_MIN_LOSS_PCT_5M` | +| `highConvictionMultiplier` | 1 (off) | `TRADE_HIGH_CONVICTION_MULT_5M` | + +✱ = mudanças desta versão. Valores são defaults no código; `.env` sobrescreve se definido. + +### Desempenho (paper-trading acumulado — pré-mudança, via `npm run report`) +| Bot | Trades | Win | Loss | Win Rate | PnL | Profit Fac | Avg Win | Avg Loss | +|---|---|---|---|---|---|---|---|---| +| 15m | 51 | 15 | 36 | 29.4% | −$1.98 | 0.89 | +$1.06 | −$0.50 | +| 5m | 69 | 26 | 43 | 37.7% | −$12.01 | 0.69 | +$1.04 | −$0.91 | + +### Exit reasons — 15m (pré-mudança) +| Razão | Count | PnL | +|---|---|---| +| SIGNAL_FLIP | **26** | **−$9.21** | +| SETTLED_WIN | 14 | +$15.44 | +| SETTLED_LOSS | 8 | −$8.00 | +| TAKE_PROFIT | 1 | +$0.50 | +| TIME_DECAY | 1 | −$0.36 | +| STOP_LOSS | 1 | −$0.35 | + +### Exit reasons — 5m (pré-mudança) +| Razão | Count | PnL | +|---|---|---| +| SETTLED_LOSS | 25 | −$25.00 | +| SETTLED_WIN | 24 | +$26.33 | +| TIME_DECAY | **18** | **−$14.10** | +| TAKE_PROFIT | 2 | +$0.77 | -### Exit reasons — 5m -| Razão | Count | -|---|---| -| SETTLED_WIN | 49 | -| TIME_DECAY | 28 | -| SETTLED_LOSS | 28 | +### PnL por faixa de preço de entrada (pré-mudança) +| Faixa | 15m PnL (trades) | 5m PnL (trades) | +|---|---|---| +| 0.30–0.39 | −$1.13 (2) | −$0.22 (3) | +| 0.40–0.44 | −$2.64 (10) | −$0.91 (8) | +| 0.45–0.49 | +$1.28 (26) | −$2.40 (28) | +| 0.50–0.54 | +$1.62 (6) | −$5.46 (21) | +| 0.55–0.59 | +$0.61 (3) | +$1.07 (3) | +| 0.60+ | −$0.50 (2) | −$1.23 (3) | --- diff --git a/package.json b/package.json index 5139278f..5c51384d 100644 --- a/package.json +++ b/package.json @@ -5,7 +5,8 @@ "private": true, "scripts": { "start": "node --env-file=.env src/index.js", - "start:5m": "node --env-file=.env src/index5m.js" + "start:5m": "node --env-file=.env src/index5m.js", + "report": "node scripts/report.js" }, "dependencies": { "@polymarket/clob-client": "^5.8.1", diff --git a/scripts/report.js b/scripts/report.js new file mode 100644 index 00000000..03deb5f9 --- /dev/null +++ b/scripts/report.js @@ -0,0 +1,126 @@ +#!/usr/bin/env node +import { readFileSync, existsSync } from "fs"; +import { resolve } from "path"; + +const ROOT = resolve(import.meta.dirname, ".."); + +function parseCsv(path) { + if (!existsSync(path)) return []; + const lines = readFileSync(path, "utf8").trim().split("\n"); + if (lines.length < 2) return []; + const headers = lines[0].split(","); + return lines.slice(1).map((line) => { + const vals = line.split(","); + return Object.fromEntries(headers.map((h, i) => [h, vals[i]])); + }); +} + +function pct(n, d) { + return d === 0 ? "—" : ((n / d) * 100).toFixed(1) + "%"; +} + +function fmt(n) { + const sign = n >= 0 ? "+" : ""; + return sign + n.toFixed(4); +} + +function fmtUsd(n) { + const sign = n >= 0 ? "+" : ""; + return sign + "$" + n.toFixed(2); +} + +function analyze(trades, label) { + if (trades.length === 0) { + console.log(`\n── ${label} ──────────────────────────────`); + console.log(" No trades found."); + return; + } + + const total = trades.length; + const wins = trades.filter((t) => parseFloat(t.pnl) > 0).length; + const losses = total - wins; + const winRate = wins / total; + + const totalPnl = trades.reduce((s, t) => s + parseFloat(t.pnl), 0); + const avgPnl = totalPnl / total; + + const winTrades = trades.filter((t) => parseFloat(t.pnl) > 0); + const lossTrades = trades.filter((t) => parseFloat(t.pnl) <= 0); + const avgWin = winTrades.length + ? winTrades.reduce((s, t) => s + parseFloat(t.pnl), 0) / winTrades.length + : 0; + const avgLoss = lossTrades.length + ? lossTrades.reduce((s, t) => s + parseFloat(t.pnl), 0) / lossTrades.length + : 0; + const profitFactor = + avgLoss !== 0 ? Math.abs(avgWin * winTrades.length) / Math.abs(avgLoss * lossTrades.length) : Infinity; + + const byReason = {}; + for (const t of trades) { + const r = t.exit_reason || "UNKNOWN"; + if (!byReason[r]) byReason[r] = { count: 0, pnl: 0 }; + byReason[r].count++; + byReason[r].pnl += parseFloat(t.pnl); + } + + const bySide = {}; + for (const t of trades) { + const s = t.side || "UNKNOWN"; + if (!bySide[s]) bySide[s] = { count: 0, wins: 0, pnl: 0 }; + bySide[s].count++; + bySide[s].pnl += parseFloat(t.pnl); + if (parseFloat(t.pnl) > 0) bySide[s].wins++; + } + + const avgDuration = trades.reduce((s, t) => s + parseInt(t.duration_s || 0), 0) / total; + + const rois = trades.map((t) => parseFloat(t.roi_pct)); + const maxWin = Math.max(...rois); + const maxLoss = Math.min(...rois); + + // Streak + let maxWinStreak = 0, maxLossStreak = 0, curWin = 0, curLoss = 0; + for (const t of trades) { + if (parseFloat(t.pnl) > 0) { curWin++; curLoss = 0; maxWinStreak = Math.max(maxWinStreak, curWin); } + else { curLoss++; curWin = 0; maxLossStreak = Math.max(maxLossStreak, curLoss); } + } + + // Date range + const firstEntry = trades[0]?.entry_time?.replace("T", " ").slice(0, 16) + "Z"; + const lastExit = trades[trades.length - 1]?.exit_time?.replace("T", " ").slice(0, 16) + "Z"; + + console.log(`\n${"─".repeat(50)}`); + console.log(` ${label}`); + console.log(`${"─".repeat(50)}`); + console.log(` Period : ${firstEntry} → ${lastExit}`); + console.log(` Trades : ${total} (wins: ${wins}, losses: ${losses})`); + console.log(` Win rate : ${pct(wins, total)}`); + console.log(` Total PnL : ${fmtUsd(totalPnl)}`); + console.log(` Avg PnL : ${fmtUsd(avgPnl)}`); + console.log(` Avg win : ${fmtUsd(avgWin)} Avg loss: ${fmtUsd(avgLoss)}`); + console.log(` Profit fac : ${profitFactor === Infinity ? "∞" : profitFactor.toFixed(2)}`); + console.log(` Best ROI : +${maxWin.toFixed(1)}% Worst: ${maxLoss.toFixed(1)}%`); + console.log(` Avg dur : ${Math.round(avgDuration)}s (max win streak: ${maxWinStreak}, max loss streak: ${maxLossStreak})`); + + console.log(`\n By exit reason:`); + for (const [r, d] of Object.entries(byReason).sort((a, b) => b[1].count - a[1].count)) { + console.log(` ${r.padEnd(18)} ${String(d.count).padStart(3)} trades PnL ${fmtUsd(d.pnl)}`); + } + + console.log(`\n By side:`); + for (const [s, d] of Object.entries(bySide)) { + console.log(` ${s.padEnd(6)} ${d.count} trades win rate ${pct(d.wins, d.count).padStart(6)} PnL ${fmtUsd(d.pnl)}`); + } +} + +const trades15m = parseCsv(resolve(ROOT, "logs/dryrun_15m_trades.csv")); +const trades5m = parseCsv(resolve(ROOT, "logs/dryrun_5m_trades.csv")); + +console.log("\n╔══════════════════════════════════════════════════╗"); +console.log("║ Polymarket BTC — Dry-Run Performance Report ║"); +console.log("╚══════════════════════════════════════════════════╝"); + +analyze(trades15m, "15-minute bot"); +analyze(trades5m, "5-minute bot"); + +console.log(`\n${"─".repeat(50)}\n`); diff --git a/src/config.js b/src/config.js index 0e055f19..8531b55a 100644 --- a/src/config.js +++ b/src/config.js @@ -41,14 +41,31 @@ export const CONFIG = { // on the winning side of the price-to-beat. Absorbs ~$9 ptb drift + buffer. ptbSafeMarginUsd: Number(process.env.TRADE_PTB_SAFE_MARGIN_USD || "30"), // Entry price filter: only enter if the market price of the chosen side is - // within [entryMinMarketPrice, entryMaxMarketPrice]. Default 0–1 (disabled). - // Example: min=0.40, max=0.80 blocks entries when too far against/with market. - entryMinMarketPrice: Number(process.env.TRADE_ENTRY_MIN_PRICE || "0"), - entryMaxMarketPrice: Number(process.env.TRADE_ENTRY_MAX_PRICE || "1"), + // within [entryMinMarketPrice, entryMaxMarketPrice]. + // Defaults come from dry-run analysis: entries below 0.45 and above 0.58 + // are net-losers on 15m. See STRATEGY_LOG.md. + entryMinMarketPrice: Number(process.env.TRADE_ENTRY_MIN_PRICE || "0.45"), + entryMaxMarketPrice: Number(process.env.TRADE_ENTRY_MAX_PRICE || "0.58"), // Cooldown after a SIGNAL_FLIP before re-entering the same market flipCooldownS: Number(process.env.TRADE_FLIP_COOLDOWN_S || "60"), // Consecutive ticks model must confirm reversal before SIGNAL_FLIP fires flipConfirmTicks: Number(process.env.TRADE_FLIP_CONFIRM_TICKS || "2"), + // Disable exits: + // - SIGNAL_FLIP: 15m data shows 25 flips with avg -$0.36 PnL; hold-to-settlement + // performs better. Enable via TRADE_DISABLE_SIGNAL_FLIP=false to re-activate. + disableSignalFlip: (process.env.TRADE_DISABLE_SIGNAL_FLIP ?? "true").toLowerCase() === "true", + disableStopLoss: (process.env.TRADE_DISABLE_STOP_LOSS ?? "false").toLowerCase() === "true", + // TIME_DECAY exit: fires when time-left (min) < X AND losing more than Y%. + // Only applies to expensive entries (entryPrice >= 0.50). + timeDecayMinLeftMin: Number(process.env.TRADE_TIME_DECAY_MIN_LEFT_MIN || "1.5"), + timeDecayMinLossPct: Number(process.env.TRADE_TIME_DECAY_MIN_LOSS_PCT || "5"), + // High-conviction position sizing. When entry price ∈ [entryMin, entryMax] + // AND chosen-side model prob ≥ minProb, trade amount is multiplied. + // Multiplier=1 disables the feature. + highConvictionMultiplier: Number(process.env.TRADE_HIGH_CONVICTION_MULT || "2"), + highConvictionMinProb: Number(process.env.TRADE_HIGH_CONVICTION_MIN_PROB || "0.70"), + highConvictionEntryMin: Number(process.env.TRADE_HIGH_CONVICTION_ENTRY_MIN || "0.45"), + highConvictionEntryMax: Number(process.env.TRADE_HIGH_CONVICTION_ENTRY_MAX || "0.50"), // When true: paper-trading only — no real orders even if private key is set dryRunOnly: (process.env.DRY_RUN || "").toLowerCase() === "true", // When true: enables real order execution. Default false = simulated/paper mode. diff --git a/src/config5m.js b/src/config5m.js index 13ce929a..b1126e74 100644 --- a/src/config5m.js +++ b/src/config5m.js @@ -42,6 +42,21 @@ export const CONFIG = { // Disable stop-loss on 5m: data shows 78% of SLs exit before a loss, but the 22% // that cut winners cost more than the savings. Holding to settlement is +$50 better // across 161 SL trades. The 85% settled win rate makes hold-to-settlement dominant. - disableStopLoss: true, + disableStopLoss: (process.env.TRADE_DISABLE_STOP_LOSS_5M ?? "true").toLowerCase() === "true", + // Disable signal-flip on 5m: 158 SIGNAL_FLIPs showed 3.8% win rate. + // Lower threshold was catching transient blips across 0.58 that then reverted. + disableSignalFlip: (process.env.TRADE_DISABLE_SIGNAL_FLIP_5M ?? "true").toLowerCase() === "true", + // Entry price filter tuned for 5m dry-run: entries < 0.50 lose $3.53, entries + // at 0.50–0.54 lose $5.46 historically. Only 0.55–0.60 zone shows positive PnL. + // Uses its own env vars so it can differ from the 15m config. + entryMinMarketPrice: Number(process.env.TRADE_ENTRY_MIN_PRICE_5M || "0.50"), + entryMaxMarketPrice: Number(process.env.TRADE_ENTRY_MAX_PRICE_5M || "0.60"), + // Tighter TIME_DECAY on 5m: require ≥15% loss before cutting (vs 5% on 15m), + // and fire earlier (2.5 min left vs 1.5 min). Cuts clearly-lost positions + // sooner but avoids trimming the small recoveries seen near settlement. + timeDecayMinLeftMin: Number(process.env.TRADE_TIME_DECAY_MIN_LEFT_MIN_5M || "2.5"), + timeDecayMinLossPct: Number(process.env.TRADE_TIME_DECAY_MIN_LOSS_PCT_5M || "15"), + // High-conviction sizing disabled by default on 5m — higher noise per trade. + highConvictionMultiplier: Number(process.env.TRADE_HIGH_CONVICTION_MULT_5M || "1"), }, }; diff --git a/src/dryRun.js b/src/dryRun.js index 49593ee0..eccd7a09 100644 --- a/src/dryRun.js +++ b/src/dryRun.js @@ -23,6 +23,7 @@ import path from "node:path"; import { ensureDir } from "./utils.js"; import { notifyTrade } from "./notify.js"; import { fetchMarketOutcome } from "./data/polymarket.js"; +import { computeTradeAmount } from "./trading/sizing.js"; // ── Headers ───────────────────────────────────────────────────────────────── @@ -129,9 +130,13 @@ function evaluateSimExit({ pos, modelUp, modelDown, currentMarketPrice, timeLeft return { shouldSell: true, reason: "SIGNAL_FLIP", roiPct }; } - // Time decay — suppressed if PTB safe; only for expensive entries (≥ 50¢) + // Time decay — suppressed if PTB safe; only for expensive entries (≥ 50¢). + // Thresholds come from config so the 5m bot can widen the loss requirement + // and fire earlier than 15m (see config5m.js). const entryWasCheap = pos.entryPrice < 0.50; - if (!ptbSafe && timeLeftMin != null && timeLeftMin < 1.5 && roiPct < -5 && !entryWasCheap) { + const tdMinLeft = config.timeDecayMinLeftMin ?? 1.5; + const tdMinLoss = config.timeDecayMinLossPct ?? 5; + if (!ptbSafe && timeLeftMin != null && timeLeftMin < tdMinLeft && roiPct < -tdMinLoss && !entryWasCheap) { return { shouldSell: true, reason: "TIME_DECAY", roiPct }; } @@ -382,13 +387,20 @@ function createSimulator(csvPath, header, config, label = "bot") { const priceAllowed = entryMktPrice != null && entryMktPrice >= minEntry && entryMktPrice <= maxEntry; if (priceAllowed && entryMktPrice > 0) { - const shares = config.tradeAmount / entryMktPrice; + const invested = computeTradeAmount({ + baseAmount: config.tradeAmount, + side: rec.side, + entryPrice: entryMktPrice, + modelUp, modelDown, + config, + }); + const shares = invested / entryMktPrice; pos = { active: true, side: rec.side, entryPrice: entryMktPrice, shares, - invested: config.tradeAmount, + invested, marketSlug: slug, entryTime: Date.now(), ptbAtEntry: tickPtb, @@ -408,7 +420,7 @@ function createSimulator(csvPath, header, config, label = "bot") { notifyTrade({ bot: label, isLive: false, action: "BUY", side: rec.side, market: slug, - entryPrice: entryMktPrice, invested: config.tradeAmount, + entryPrice: entryMktPrice, invested, }); } } @@ -469,8 +481,17 @@ export function createDryRunSimulator15m(csvPath, tradingConfig = {}) { stopLossMinDurationS: tradingConfig.stopLossMinDurationS ?? 120, flipCooldownS: tradingConfig.flipCooldownS ?? 60, flipConfirmTicks: tradingConfig.flipConfirmTicks ?? 2, + disableSignalFlip: tradingConfig.disableSignalFlip ?? false, + disableStopLoss: tradingConfig.disableStopLoss ?? false, entryMinMarketPrice: tradingConfig.entryMinMarketPrice ?? 0, entryMaxMarketPrice: tradingConfig.entryMaxMarketPrice ?? 1, + timeDecayMinLeftMin: tradingConfig.timeDecayMinLeftMin ?? 1.5, + timeDecayMinLossPct: tradingConfig.timeDecayMinLossPct ?? 5, + ptbSafeMarginUsd: tradingConfig.ptbSafeMarginUsd ?? 30, + highConvictionMultiplier: tradingConfig.highConvictionMultiplier ?? 1, + highConvictionMinProb: tradingConfig.highConvictionMinProb ?? 0.70, + highConvictionEntryMin: tradingConfig.highConvictionEntryMin ?? 0.45, + highConvictionEntryMax: tradingConfig.highConvictionEntryMax ?? 0.50, }; return createSimulator(csvPath, HEADER_15M, config, "15m"); } @@ -491,9 +512,16 @@ export function createDryRunSimulator5m(csvPath, tradingConfig = {}) { flipCooldownS: tradingConfig.flipCooldownS ?? 90, flipConfirmTicks: tradingConfig.flipConfirmTicks ?? 5, disableSignalFlip: tradingConfig.disableSignalFlip ?? true, - disableStopLoss: tradingConfig.disableStopLoss ?? false, + disableStopLoss: tradingConfig.disableStopLoss ?? true, entryMinMarketPrice: tradingConfig.entryMinMarketPrice ?? 0, entryMaxMarketPrice: tradingConfig.entryMaxMarketPrice ?? 1, + timeDecayMinLeftMin: tradingConfig.timeDecayMinLeftMin ?? 2.5, + timeDecayMinLossPct: tradingConfig.timeDecayMinLossPct ?? 15, + ptbSafeMarginUsd: tradingConfig.ptbSafeMarginUsd ?? 30, + highConvictionMultiplier: tradingConfig.highConvictionMultiplier ?? 1, + highConvictionMinProb: tradingConfig.highConvictionMinProb ?? 0.70, + highConvictionEntryMin: tradingConfig.highConvictionEntryMin ?? 0.45, + highConvictionEntryMax: tradingConfig.highConvictionEntryMax ?? 0.50, }; return createSimulator(csvPath, HEADER_5M, config, "5m"); } diff --git a/src/index.js b/src/index.js index cefc58a9..80e4a757 100644 --- a/src/index.js +++ b/src/index.js @@ -270,36 +270,39 @@ async function main() { // Position and exit eval: real when live, simulated otherwise let displayPos, displayCurrentMktPrice, displayExitEval; + const baseExitEvalArgs = { + takeProfitPct: CONFIG.trading.takeProfitPct, + stopLossPct: CONFIG.trading.stopLossPct, + signalFlipMinProb: CONFIG.trading.signalFlipMinProb, + stopLossMinProb: CONFIG.trading.stopLossMinProb, + stopLossMinDurationS: CONFIG.trading.stopLossMinDurationS, + flipConfirmTicks: CONFIG.trading.flipConfirmTicks, + btcPrice: currentPrice, priceToBeat, + ptbSafeMarginUsd: CONFIG.trading.ptbSafeMarginUsd, + disableStopLoss: CONFIG.trading.disableStopLoss ?? false, + disableSignalFlip: CONFIG.trading.disableSignalFlip ?? false, + timeDecayMinLeftMin: CONFIG.trading.timeDecayMinLeftMin ?? 1.5, + timeDecayMinLossPct: CONFIG.trading.timeDecayMinLossPct ?? 5, + }; if (liveTrading) { displayPos = getPosition(); displayCurrentMktPrice = displayPos.active ? (displayPos.side === "UP" ? marketUp : marketDown) : null; displayExitEval = evaluateExit({ + ...baseExitEvalArgs, position: displayPos, modelUp: pLong, modelDown: pShort, currentMarketPrice: displayCurrentMktPrice, timeLeftMin, - takeProfitPct: CONFIG.trading.takeProfitPct, - stopLossPct: CONFIG.trading.stopLossPct, - signalFlipMinProb: CONFIG.trading.signalFlipMinProb, - stopLossMinProb: CONFIG.trading.stopLossMinProb, - stopLossMinDurationS: CONFIG.trading.stopLossMinDurationS, flipConfirmCount, - flipConfirmTicks: CONFIG.trading.flipConfirmTicks, - btcPrice: currentPrice, priceToBeat, ptbSafeMarginUsd: CONFIG.trading.ptbSafeMarginUsd, }); flipConfirmCount = displayPos.active ? (displayExitEval.flipConfirmCount ?? 0) : 0; } else { displayPos = simStats.position; displayCurrentMktPrice = displayPos.active ? (displayPos.side === "UP" ? marketUp : marketDown) : null; displayExitEval = evaluateExit({ + ...baseExitEvalArgs, position: displayPos, modelUp: pLong, modelDown: pShort, currentMarketPrice: displayCurrentMktPrice, timeLeftMin, - takeProfitPct: CONFIG.trading.takeProfitPct, - stopLossPct: CONFIG.trading.stopLossPct, - signalFlipMinProb: CONFIG.trading.signalFlipMinProb, - stopLossMinProb: CONFIG.trading.stopLossMinProb, - stopLossMinDurationS: CONFIG.trading.stopLossMinDurationS, flipConfirmCount: 0, flipConfirmTicks: 1, - btcPrice: currentPrice, priceToBeat, ptbSafeMarginUsd: CONFIG.trading.ptbSafeMarginUsd, }); } diff --git a/src/index5m.js b/src/index5m.js index 01f661b8..9ce90625 100644 --- a/src/index5m.js +++ b/src/index5m.js @@ -312,38 +312,39 @@ async function main() { // Position and exit eval: real when live, simulated otherwise let displayPos, displayCurrentMktPrice, displayExitEval; + const baseExitEvalArgs = { + takeProfitPct: CONFIG.trading.takeProfitPct, + stopLossPct: CONFIG.trading.stopLossPct, + signalFlipMinProb: CONFIG.trading.signalFlipMinProb, + stopLossMinProb: CONFIG.trading.stopLossMinProb, + stopLossMinDurationS: CONFIG.trading.stopLossMinDurationS, + flipConfirmTicks: CONFIG.trading.flipConfirmTicks, + btcPrice: currentPrice, priceToBeat, + ptbSafeMarginUsd: CONFIG.trading.ptbSafeMarginUsd, + disableStopLoss: CONFIG.trading.disableStopLoss ?? false, + disableSignalFlip: CONFIG.trading.disableSignalFlip ?? false, + timeDecayMinLeftMin: CONFIG.trading.timeDecayMinLeftMin ?? 2.5, + timeDecayMinLossPct: CONFIG.trading.timeDecayMinLossPct ?? 15, + }; if (liveTrading) { displayPos = getPosition(); displayCurrentMktPrice = displayPos.active ? (displayPos.side === "UP" ? marketUp : marketDown) : null; displayExitEval = evaluateExit({ + ...baseExitEvalArgs, position: displayPos, modelUp: timeAware.adjustedUp, modelDown: timeAware.adjustedDown, currentMarketPrice: displayCurrentMktPrice, timeLeftMin, - takeProfitPct: CONFIG.trading.takeProfitPct, - stopLossPct: CONFIG.trading.stopLossPct, - signalFlipMinProb: CONFIG.trading.signalFlipMinProb, - stopLossMinProb: CONFIG.trading.stopLossMinProb, - stopLossMinDurationS: CONFIG.trading.stopLossMinDurationS, flipConfirmCount, - flipConfirmTicks: CONFIG.trading.flipConfirmTicks, - btcPrice: currentPrice, priceToBeat, ptbSafeMarginUsd: CONFIG.trading.ptbSafeMarginUsd, - disableStopLoss: CONFIG.trading.disableStopLoss ?? false, }); flipConfirmCount = displayPos.active ? (displayExitEval.flipConfirmCount ?? 0) : 0; } else { displayPos = simStats.position; displayCurrentMktPrice = displayPos.active ? (displayPos.side === "UP" ? marketUp : marketDown) : null; displayExitEval = evaluateExit({ + ...baseExitEvalArgs, position: displayPos, modelUp: timeAware.adjustedUp, modelDown: timeAware.adjustedDown, currentMarketPrice: displayCurrentMktPrice, timeLeftMin, - takeProfitPct: CONFIG.trading.takeProfitPct, - stopLossPct: CONFIG.trading.stopLossPct, - signalFlipMinProb: CONFIG.trading.signalFlipMinProb, - stopLossMinProb: CONFIG.trading.stopLossMinProb, - stopLossMinDurationS: CONFIG.trading.stopLossMinDurationS, flipConfirmCount: 0, flipConfirmTicks: 1, - btcPrice: currentPrice, priceToBeat, ptbSafeMarginUsd: CONFIG.trading.ptbSafeMarginUsd, - disableStopLoss: CONFIG.trading.disableStopLoss ?? false, }); } diff --git a/src/trading/client.js b/src/trading/client.js index 7ae0124f..3100c2f8 100644 --- a/src/trading/client.js +++ b/src/trading/client.js @@ -18,12 +18,12 @@ export async function initTradingClient(config) { const { privateKey, funder, signatureType, tradeAmount } = config.trading; if (!privateKey) { - _cached = { client: null, tradingEnabled: false, tradeAmount: 0, wallet: null }; + _cached = { ...config.trading, client: null, tradingEnabled: false, tradeAmount: 0, wallet: null }; return _cached; } if (config.trading.dryRunOnly) { - _cached = { client: null, tradingEnabled: false, tradeAmount: 0, wallet: null }; + _cached = { ...config.trading, client: null, tradingEnabled: false, tradeAmount: 0, wallet: null }; return _cached; } @@ -100,7 +100,10 @@ export async function initTradingClient(config) { // balanceAddress: onde está o USDC — o funder (proxy) ou o EOA const balanceAddress = funderAddr ?? _wallet.address; - _cached = { client, tradingEnabled: true, tradeAmount, balanceAddress, wallet: _wallet }; + // Spread all trading config so downstream consumers (executor, evaluators) can + // read entryMinMarketPrice, highConvictionMultiplier, timeDecay*, etc. directly + // from the trading object instead of re-reading CONFIG.trading. + _cached = { ...config.trading, client, tradingEnabled: true, tradeAmount, balanceAddress, wallet: _wallet }; return _cached; } diff --git a/src/trading/executor.js b/src/trading/executor.js index ac676e2c..50ae6ee9 100644 --- a/src/trading/executor.js +++ b/src/trading/executor.js @@ -3,6 +3,7 @@ import { clamp } from "../utils.js"; import { setStatusMessage } from "../display.js"; import { buyMarketOrder, sellMarketOrder } from "./orders.js"; import { getPosition, recordBuy, recordSell, fetchPositionBalance } from "./position.js"; +import { computeTradeAmount } from "./sizing.js"; import { notifyTrade } from "../notify.js"; function logError(msg) { @@ -57,16 +58,26 @@ export async function processActionQueue(actionQueue, { trading, poly, rec, time const priceNum = rawAsk != null ? clamp(rawAsk + 0.02, 0, 0.97) : 0.5; const entryRef = rawAsk ?? priceNum; + const invested = computeTradeAmount({ + baseAmount: trading.tradeAmount, + side, + entryPrice: entryRef, + modelUp: timeAware?.adjustedUp, + modelDown: timeAware?.adjustedDown, + config: trading, + }); + setStatusMessage(`Comprando ${side}...`); - const result = await buyMarketOrder({ client: trading.client, tokenId, amount: trading.tradeAmount, price: priceNum }); + const result = await buyMarketOrder({ client: trading.client, tokenId, amount: invested, price: priceNum }); if (result.ok) { const balance = await fetchPositionBalance(trading.client, tokenId); - const shares = balance > 0 ? balance : trading.tradeAmount / entryRef; - recordBuy({ side, tokenId, shares, entryPrice: entryRef, invested: trading.tradeAmount, marketSlug: marketSlugNow, orderId: result.order?.orderID }); + const shares = balance > 0 ? balance : invested / entryRef; + recordBuy({ side, tokenId, shares, entryPrice: entryRef, invested, marketSlug: marketSlugNow, orderId: result.order?.orderID }); const orderId = result.order?.orderID ?? result.order?.id ?? "-"; const balanceStr = balance > 0 ? `shares: ${balance.toFixed(2)}` : "saldo 0 (ordem não preenchida?)"; - setStatusMessage(`COMPROU ${side} @ ${(entryRef * 100).toFixed(1)}¢ | $${trading.tradeAmount} | ${balanceStr} | ID: ${String(orderId).slice(0, 12)}`, 8000); - notifyTrade({ bot: botLabel, isLive: true, action: "BUY", side, market: marketSlugNow, entryPrice: entryRef, invested: trading.tradeAmount }); + const sizingTag = invested !== trading.tradeAmount ? ` [HIGH-CONV x${(invested / trading.tradeAmount).toFixed(1)}]` : ""; + setStatusMessage(`COMPROU ${side} @ ${(entryRef * 100).toFixed(1)}¢ | $${invested}${sizingTag} | ${balanceStr} | ID: ${String(orderId).slice(0, 12)}`, 8000); + notifyTrade({ bot: botLabel, isLive: true, action: "BUY", side, market: marketSlugNow, entryPrice: entryRef, invested }); } else { const errMsg = `Erro na compra: ${result.error}`; setStatusMessage(errMsg, 15000); diff --git a/src/trading/position.js b/src/trading/position.js index b4399635..2b4c9e29 100644 --- a/src/trading/position.js +++ b/src/trading/position.js @@ -108,7 +108,7 @@ export function resetIfMarketChanged(currentSlug) { // Avalia se a posição aberta deve ser encerrada. // Retorna { shouldSell, reason, urgency } onde urgency é "HIGH" | "MEDIUM" | null -export function evaluateExit({ position, modelUp, modelDown, currentMarketPrice, timeLeftMin, takeProfitPct, stopLossPct, signalFlipMinProb, stopLossMinProb = null, stopLossMinDurationS = 0, flipConfirmCount = 0, flipConfirmTicks = 1, btcPrice = null, priceToBeat = null, ptbSafeMarginUsd = 30, disableStopLoss = false }) { +export function evaluateExit({ position, modelUp, modelDown, currentMarketPrice, timeLeftMin, takeProfitPct, stopLossPct, signalFlipMinProb, stopLossMinProb = null, stopLossMinDurationS = 0, flipConfirmCount = 0, flipConfirmTicks = 1, btcPrice = null, priceToBeat = null, ptbSafeMarginUsd = 30, disableStopLoss = false, disableSignalFlip = false, timeDecayMinLeftMin = 1.5, timeDecayMinLossPct = 5 }) { if (!position.active || currentMarketPrice == null) { return { shouldSell: false, reason: null, urgency: null, flipConfirmCount: 0 }; } @@ -147,8 +147,8 @@ export function evaluateExit({ position, modelUp, modelDown, currentMarketPrice, return { shouldSell: true, reason: "STOP_LOSS", urgency, roiPct, flipConfirmCount: 0 }; } - // 3. Sinal invertido — suprimido se PTB seguro; requer N ticks consecutivos - if (!ptbSafe && modelConfirmsReversal) { + // 3. Sinal invertido — suprimido se PTB seguro ou desabilitado; requer N ticks consecutivos + if (!ptbSafe && !disableSignalFlip && modelConfirmsReversal) { const newCount = flipConfirmCount + 1; if (newCount >= flipConfirmTicks) { const urgency = oppositeProb >= 0.65 ? "HIGH" : "MEDIUM"; @@ -159,7 +159,7 @@ export function evaluateExit({ position, modelUp, modelDown, currentMarketPrice, // 4. Pouco tempo + perdendo — suprimido se PTB seguro; só aplica se entrada cara (>= 50¢) const entryWasCheap = position.entryPrice < 0.50; - if (!ptbSafe && timeLeftMin != null && timeLeftMin < 1.5 && roiPct < -5 && !entryWasCheap) { + if (!ptbSafe && timeLeftMin != null && timeLeftMin < timeDecayMinLeftMin && roiPct < -timeDecayMinLossPct && !entryWasCheap) { return { shouldSell: true, reason: "TIME_DECAY", urgency: "MEDIUM", roiPct, flipConfirmCount: 0 }; } diff --git a/src/trading/sizing.js b/src/trading/sizing.js new file mode 100644 index 00000000..ea74a167 --- /dev/null +++ b/src/trading/sizing.js @@ -0,0 +1,26 @@ +/** + * High-conviction position sizing helper. + * + * When the chosen side's model probability is above `minProb` AND the entry + * price falls inside [entryMin, entryMax], the trade amount is multiplied. + * The target zone is the "cheap-but-confident" bucket: low enough price to + * give a ≥2× payoff on win, high enough that the model conviction is strong. + * + * Returns the base amount unchanged when the feature is disabled + * (multiplier ≤ 1) or when any required input is missing. + */ +export function computeTradeAmount({ baseAmount, side, entryPrice, modelUp, modelDown, config }) { + const multiplier = config?.highConvictionMultiplier ?? 1; + if (multiplier <= 1 || !Number.isFinite(entryPrice)) return baseAmount; + + const minProb = config?.highConvictionMinProb ?? 0.70; + const entryMin = config?.highConvictionEntryMin ?? 0.45; + const entryMax = config?.highConvictionEntryMax ?? 0.50; + + const sideProb = side === "UP" ? modelUp : modelDown; + if (sideProb == null) return baseAmount; + + const inEntryRange = entryPrice >= entryMin && entryPrice <= entryMax; + const highConv = sideProb >= minProb; + return (inEntryRange && highConv) ? baseAmount * multiplier : baseAmount; +} From 367b60a6114c1b44dfcf2bfa5193cba647c12c24 Mon Sep 17 00:00:00 2001 From: Jean Carraro Date: Thu, 23 Apr 2026 16:00:22 -0300 Subject: [PATCH 26/49] Consolidate dry-run results into STRATEGY_LOG (39 trades 15m, 188 trades 5m) Co-Authored-By: Claude Sonnet 4.6 --- STRATEGY_LOG.md | 20 +++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) diff --git a/STRATEGY_LOG.md b/STRATEGY_LOG.md index 401ca8d0..af23e334 100644 --- a/STRATEGY_LOG.md +++ b/STRATEGY_LOG.md @@ -119,6 +119,22 @@ Introduzidas após análise dos 51 trades 15m / 69 trades 5m registrados em 2026 | 0.55–0.59 | +$0.61 (3) | +$1.07 (3) | | 0.60+ | −$0.50 (2) | −$1.23 (3) | +### Desempenho local acumulado (máquina WSL — até 2026-04-23) +*Nota: este run local rodou com params da versão anterior (disableSignalFlip=false 15m, sem entry filter). 39 trades 15m / 188 trades 5m registrados antes do pull desta versão.* + +| Bot | Trades | Win | Loss | Win Rate | PnL | +|---|---|---|---|---|---| +| 15m | 39 | 20 | 19 | 51.3% | +$4.96 | +| 5m | 188 | 92 | 96 | 48.9% | +$15.27 | + +### Exit reasons — 5m (local acumulado) +| Razão | Count | +|---|---| +| SETTLED_WIN | 87 | +| SETTLED_LOSS | 50 | +| TIME_DECAY | 46 | +| TAKE_PROFIT | 5 | + --- ## Snapshot: `2026-04-16_pre-disable-5m-stoploss` @@ -275,7 +291,9 @@ Introduzidas após análise dos 51 trades 15m / 69 trades 5m registrados em 2026 | `pre-disable-5m-flip` | 44 | 29.5% | −$2.89 | 119 | 33.6% | +$2.68 | | `pre-outcome-api-ptb` | 16 | 25.0% | −$4.44 | 52 | 44.2% | +$2.28 | | `pre-disable-5m-stoploss` | 44 | 50.0% | +$11.34 | 71 | 42.3% | +$4.52 | -| **atual** | 34 | 50.0% | +$4.73 | 105 | 46.7% | +$3.33 | +| `pre-entry-filter-and-15m-flip-disable` (local) | 39 | 51.3% | +$4.96 | 188 | 48.9% | +$15.27 | +| `pre-entry-filter-and-15m-flip-disable` (remoto) | 51 | 29.4% | −$1.98 | 69 | 37.7% | −$12.01 | +| **atual** | — | — | — | — | — | — | ### Principais decisões estratégicas e aprendizados From d36b3f467e70ee045f48c27356d17c6c836cfd1a Mon Sep 17 00:00:00 2001 From: Jean Carraro Date: Thu, 23 Apr 2026 16:08:10 -0300 Subject: [PATCH 27/49] Document local run params and explain remote vs local performance gap Local had TRADE_ENTRY_MIN_PRICE=0.40/MAX=0.85 in .env while remote ran with code defaults (0-1, no filter). The ~$18 gap likely explained by the local filter avoiding extreme-price trades that triggered bad SIGNAL_FLIPs. Co-Authored-By: Claude Sonnet 4.6 --- STRATEGY_LOG.md | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/STRATEGY_LOG.md b/STRATEGY_LOG.md index af23e334..411e0e1e 100644 --- a/STRATEGY_LOG.md +++ b/STRATEGY_LOG.md @@ -120,7 +120,20 @@ Introduzidas após análise dos 51 trades 15m / 69 trades 5m registrados em 2026 | 0.60+ | −$0.50 (2) | −$1.23 (3) | ### Desempenho local acumulado (máquina WSL — até 2026-04-23) -*Nota: este run local rodou com params da versão anterior (disableSignalFlip=false 15m, sem entry filter). 39 trades 15m / 188 trades 5m registrados antes do pull desta versão.* + +**Parâmetros efetivos neste run** (código com defaults 0–1, mas `.env` sobrescrevia): +| Parâmetro | Valor no .env local | +|---|---| +| `TRADE_ENTRY_MIN_PRICE` | **0.40** | +| `TRADE_ENTRY_MAX_PRICE` | **0.85** | +| `disableSignalFlip` (15m) | false (default do código antigo) | +| `disableStopLoss` (5m) | true (hardcoded no config5m) | +| demais params | defaults do código antigo | + +**Por que a diferença de +$18 vs o remoto (−$14)?** +O remoto rodou sem entry filter (0–1), entrando em todos os preços, inclusive faixas extremas (<0.40 e >0.85) onde o mercado já precificou muita certeza. O local filtrava para 0.40–0.85, evitando essas entradas ruins. O remote 15m acumulou 26 SIGNAL_FLIPs (−$9.21) — muitos vindos de entradas em preços extremos onde o sinal era noise. + +**Hipótese para análise futura:** a faixa 0.40–0.85 parece capturar boa parte do alpha sem excesso de trades em zonas de baixa informação. Vale comparar com a nova faixa 0.45–0.58 (mais restritiva) para ver se o ganho de qualidade compensa a perda de volume. | Bot | Trades | Win | Loss | Win Rate | PnL | |---|---|---|---|---|---| From df66f55fd67f0033c88f96549faf1501257c8c19 Mon Sep 17 00:00:00 2001 From: Jean Carraro Date: Sat, 25 Apr 2026 12:20:22 -0300 Subject: [PATCH 28/49] Add Coolify deploy: log server, log rotation, named volumes - Add src/logServer.js: HTTP server on port 3456 with /report, /logs listing and CSV download - Add entrypoint.sh: rotates tick logs on container start (gzip + 14-day retention), preserves trades CSV - Update Dockerfile: include scripts/ and entrypoint.sh - Update docker-compose.yml: named volume, tty, optional env_file, reports service Co-Authored-By: Claude Sonnet 4.6 --- Dockerfile | 7 ++-- docker-compose.yml | 30 +++++++++++++-- entrypoint.sh | 17 +++++++++ src/logServer.js | 95 ++++++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 142 insertions(+), 7 deletions(-) create mode 100644 entrypoint.sh create mode 100644 src/logServer.js diff --git a/Dockerfile b/Dockerfile index 807c7ec6..74d73402 100644 --- a/Dockerfile +++ b/Dockerfile @@ -8,8 +8,9 @@ RUN npm ci --omit=dev # Copy source COPY src/ ./src/ +COPY scripts/ ./scripts/ +COPY entrypoint.sh /entrypoint.sh +RUN chmod +x /entrypoint.sh && mkdir -p logs -# Logs dir (will be overridden by volume mount, but good to have) -RUN mkdir -p logs - +ENTRYPOINT ["/entrypoint.sh"] CMD ["node", "src/index.js"] diff --git a/docker-compose.yml b/docker-compose.yml index 1d8ea2ad..286b5250 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -3,11 +3,15 @@ services: build: . container_name: polymarket-bot-15m restart: unless-stopped - env_file: .env + tty: true + stdin_open: true + env_file: + - path: .env + required: false command: node --max-old-space-size=384 src/index.js mem_limit: 512m volumes: - - ./logs:/app/logs + - polymarket_logs:/app/logs logging: driver: json-file options: @@ -18,13 +22,31 @@ services: build: . container_name: polymarket-bot-5m restart: unless-stopped - env_file: .env + tty: true + stdin_open: true + env_file: + - path: .env + required: false command: node --max-old-space-size=384 src/index5m.js mem_limit: 512m volumes: - - ./logs:/app/logs + - polymarket_logs:/app/logs logging: driver: json-file options: max-size: "10m" max-file: "3" + + reports: + build: . + container_name: polymarket-reports + restart: unless-stopped + command: node src/logServer.js + ports: + - "3456:3456" + volumes: + - polymarket_logs:/app/logs:ro + mem_limit: 128m + +volumes: + polymarket_logs: diff --git a/entrypoint.sh b/entrypoint.sh new file mode 100644 index 00000000..8e8c3e48 --- /dev/null +++ b/entrypoint.sh @@ -0,0 +1,17 @@ +#!/bin/sh +# Rotates large tick logs on every container start. +# Trade history (*_trades.csv) is preserved across deploys. +TIMESTAMP=$(date -u +%Y%m%d_%H%M%S) +mkdir -p /app/logs/archive + +for f in signals.csv signals_5m.csv dryrun_15m.csv dryrun_5m.csv; do + if [ -f "/app/logs/$f" ]; then + gzip -c "/app/logs/$f" > "/app/logs/archive/${TIMESTAMP}_${f}.gz" + rm "/app/logs/$f" + fi +done + +# delete archives older than 14 days +find /app/logs/archive -name "*.gz" -mtime +14 -delete + +exec "$@" diff --git a/src/logServer.js b/src/logServer.js new file mode 100644 index 00000000..64e2bd86 --- /dev/null +++ b/src/logServer.js @@ -0,0 +1,95 @@ +import http from 'http'; +import { readFile, readdir } from 'fs/promises'; +import { existsSync } from 'fs'; +import { execFile } from 'child_process'; +import { promisify } from 'util'; +import path from 'path'; + +const execFileAsync = promisify(execFile); +const LOGS_DIR = './logs'; +const PORT = process.env.LOG_SERVER_PORT ?? 3456; + +const ALLOWED_EXTENSIONS = new Set(['.csv', '.log', '.json']); + +const HTML = (body) => ` + + + + + Polymarket BTC — Reports + + + + + ${body} + +`; + +async function handleReport(res) { + try { + const { stdout } = await execFileAsync('node', ['scripts/report.js']); + res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' }); + res.end(HTML(`

Performance Report

${stdout}
`)); + } catch (err) { + res.writeHead(500, { 'Content-Type': 'text/plain' }); + res.end(err.message); + } +} + +async function handleLogsList(res) { + try { + const files = (await readdir(LOGS_DIR)) + .filter(f => ALLOWED_EXTENSIONS.has(path.extname(f))) + .sort(); + const links = files.map(f => `
  • ${f}
  • `).join('\n'); + res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' }); + res.end(HTML(`

    Log Files

      ${links}
    `)); + } catch (err) { + res.writeHead(500); res.end(err.message); + } +} + +async function handleLogFile(filename, res) { + const safe = path.basename(filename); + if (!ALLOWED_EXTENSIONS.has(path.extname(safe))) { + res.writeHead(403); res.end('Forbidden'); return; + } + const filepath = path.join(LOGS_DIR, safe); + if (!existsSync(filepath)) { + res.writeHead(404); res.end('Not found'); return; + } + const data = await readFile(filepath); + res.writeHead(200, { + 'Content-Type': 'text/plain; charset=utf-8', + 'Content-Disposition': `attachment; filename="${safe}"`, + }); + res.end(data); +} + +const server = http.createServer(async (req, res) => { + const url = new URL(req.url, 'http://localhost'); + try { + if (url.pathname === '/' || url.pathname === '/report') { + await handleReport(res); + } else if (url.pathname === '/logs') { + await handleLogsList(res); + } else if (url.pathname.startsWith('/logs/')) { + await handleLogFile(decodeURIComponent(url.pathname.slice(6)), res); + } else { + res.writeHead(404); res.end('Not found'); + } + } catch (err) { + res.writeHead(500); res.end(err.message); + } +}); + +server.listen(PORT, () => console.log(`Log server on http://0.0.0.0:${PORT}`)); From e716580ba0399756d335cb903a814390349ddaf6 Mon Sep 17 00:00:00 2001 From: Jean Carraro Date: Sat, 25 Apr 2026 22:07:53 -0300 Subject: [PATCH 29/49] feat: adiciona dashboard com vite --- Dockerfile | 12 +- STRATEGY_LOG.md | 88 +- dashboard/.gitignore | 24 + dashboard/README.md | 73 + dashboard/components.json | 20 + dashboard/eslint.config.js | 22 + dashboard/index.html | 13 + dashboard/package-lock.json | 5993 +++++++++++++++++ dashboard/package.json | 42 + dashboard/public/favicon.svg | 1 + dashboard/public/icons.svg | 24 + dashboard/src/components/ui/badge.tsx | 48 + dashboard/src/components/ui/button.tsx | 64 + dashboard/src/components/ui/card.tsx | 92 + dashboard/src/components/ui/chart.tsx | 372 + dashboard/src/components/ui/input.tsx | 21 + .../src/components/ui/navigation-menu.tsx | 168 + dashboard/src/components/ui/scroll-area.tsx | 58 + dashboard/src/components/ui/separator.tsx | 26 + dashboard/src/components/ui/sheet.tsx | 143 + dashboard/src/components/ui/sidebar.tsx | 726 ++ dashboard/src/components/ui/skeleton.tsx | 13 + dashboard/src/components/ui/table.tsx | 114 + dashboard/src/components/ui/tabs.tsx | 91 + dashboard/src/components/ui/tooltip.tsx | 55 + dashboard/src/hooks/use-mobile.ts | 19 + dashboard/src/index.css | 105 + dashboard/src/lib/api.ts | 133 + dashboard/src/lib/utils.ts | 6 + dashboard/src/main.tsx | 23 + dashboard/src/routeTree.gen.ts | 95 + dashboard/src/routes/__root.tsx | 45 + dashboard/src/routes/index.tsx | 185 + dashboard/src/routes/signals.tsx | 197 + dashboard/src/routes/trades.tsx | 115 + dashboard/tsconfig.app.json | 30 + dashboard/tsconfig.json | 7 + dashboard/tsconfig.node.json | 24 + dashboard/vite.config.ts | 23 + docker-compose.yml | 15 +- package.json | 5 +- src/logServer.js | 328 +- 42 files changed, 9564 insertions(+), 94 deletions(-) create mode 100644 dashboard/.gitignore create mode 100644 dashboard/README.md create mode 100644 dashboard/components.json create mode 100644 dashboard/eslint.config.js create mode 100644 dashboard/index.html create mode 100644 dashboard/package-lock.json create mode 100644 dashboard/package.json create mode 100644 dashboard/public/favicon.svg create mode 100644 dashboard/public/icons.svg create mode 100644 dashboard/src/components/ui/badge.tsx create mode 100644 dashboard/src/components/ui/button.tsx create mode 100644 dashboard/src/components/ui/card.tsx create mode 100644 dashboard/src/components/ui/chart.tsx create mode 100644 dashboard/src/components/ui/input.tsx create mode 100644 dashboard/src/components/ui/navigation-menu.tsx create mode 100644 dashboard/src/components/ui/scroll-area.tsx create mode 100644 dashboard/src/components/ui/separator.tsx create mode 100644 dashboard/src/components/ui/sheet.tsx create mode 100644 dashboard/src/components/ui/sidebar.tsx create mode 100644 dashboard/src/components/ui/skeleton.tsx create mode 100644 dashboard/src/components/ui/table.tsx create mode 100644 dashboard/src/components/ui/tabs.tsx create mode 100644 dashboard/src/components/ui/tooltip.tsx create mode 100644 dashboard/src/hooks/use-mobile.ts create mode 100644 dashboard/src/index.css create mode 100644 dashboard/src/lib/api.ts create mode 100644 dashboard/src/lib/utils.ts create mode 100644 dashboard/src/main.tsx create mode 100644 dashboard/src/routeTree.gen.ts create mode 100644 dashboard/src/routes/__root.tsx create mode 100644 dashboard/src/routes/index.tsx create mode 100644 dashboard/src/routes/signals.tsx create mode 100644 dashboard/src/routes/trades.tsx create mode 100644 dashboard/tsconfig.app.json create mode 100644 dashboard/tsconfig.json create mode 100644 dashboard/tsconfig.node.json create mode 100644 dashboard/vite.config.ts diff --git a/Dockerfile b/Dockerfile index 74d73402..fc08d6cb 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,15 +1,21 @@ -FROM node:22-alpine +FROM node:22-alpine AS dashboard-builder +WORKDIR /build +COPY dashboard/package*.json ./ +RUN npm ci +COPY dashboard/ ./ +RUN npm run build +FROM node:22-alpine WORKDIR /app -# Install dependencies first (cached layer) COPY package*.json ./ RUN npm ci --omit=dev -# Copy source COPY src/ ./src/ COPY scripts/ ./scripts/ COPY entrypoint.sh /entrypoint.sh +COPY --from=dashboard-builder /build/dist ./dashboard/dist + RUN chmod +x /entrypoint.sh && mkdir -p logs ENTRYPOINT ["/entrypoint.sh"] diff --git a/STRATEGY_LOG.md b/STRATEGY_LOG.md index 411e0e1e..23945850 100644 --- a/STRATEGY_LOG.md +++ b/STRATEGY_LOG.md @@ -294,19 +294,81 @@ O remoto rodou sem entry filter (0–1), entrando em todos os preços, inclusive --- -## Resumo comparativo - -| Snapshot | 15m Trades | 15m Win% | 15m PnL | 5m Trades | 5m Win% | 5m PnL | -|---|---|---|---|---|---|---| -| `pre-cooldown-sl-fix` | 93 | 36.6% | +$2.75 | 395 | 39.2% | +$42.54 | -| `pre-tightened-exits` | 94 | 34.0% | +$0.64 | 283 | 35.3% | +$28.73 | -| `pre-telegram-notify` | 19 | 63.2% | +$4.92 | 163 | 32.5% | +$18.82 | -| `pre-disable-5m-flip` | 44 | 29.5% | −$2.89 | 119 | 33.6% | +$2.68 | -| `pre-outcome-api-ptb` | 16 | 25.0% | −$4.44 | 52 | 44.2% | +$2.28 | -| `pre-disable-5m-stoploss` | 44 | 50.0% | +$11.34 | 71 | 42.3% | +$4.52 | -| `pre-entry-filter-and-15m-flip-disable` (local) | 39 | 51.3% | +$4.96 | 188 | 48.9% | +$15.27 | -| `pre-entry-filter-and-15m-flip-disable` (remoto) | 51 | 29.4% | −$1.98 | 69 | 37.7% | −$12.01 | -| **atual** | — | — | — | — | — | — | +## Tabela consolidada — hash × parâmetros × resultados + +Cada linha = uma versão de código (commit-pai do commit que introduziu a próxima mudança). Os parâmetros refletem os **defaults do código naquele commit** + overrides explícitos no `.env` daquele run. Use para estudar qual combinação de (código + params) produziu cada resultado. + +**Legenda:** `flip@X` = `signalFlipMinProb=X`; `CT=N` = `flipConfirmTicks=N`; `CD=Ns` = `flipCooldownS=N`; ⚠ = amostra insuficiente (< 20 trades). + +### Parâmetros 15m por versão + +| # | Hash | Data | flip | disableFlip | disableSL | SL guards | entry filter | highConvMult | TD | +|---|---|---|---|---|---|---|---|---|---| +| 1 | `a8e2101` | 07-abr | 0.58 | false | false | 0.58 / 0s | — | — | 1.5m / 5% | +| 2 | `45170e2` | 13-abr | 0.58 | false | false | 0.65 / 120s | — | — | 1.5m / 5% | +| 3 | `fe27514` | 14-abr | 0.58 | false | false | 0.65 / 120s | — | — | 1.5m / 5% | +| 4 | `2c0cbc7` | 15-abr | 0.58 | false | false | 0.65 / 120s | — | — | 1.5m / 5% | +| 5 | `fa9fc9b` | 15-abr | 0.58 | false | false | 0.65 / 120s | — | — | 1.5m / 5% | +| 6 | `577d5f4` | 15-abr | 0.58 | false | false | 0.65 / 120s | — | — | 1.5m / 5% | +| 7 | `83a4a7b` | 15-abr | 0.58 | false | false | 0.65 / 120s | — (+PTB $30) | — | 1.5m / 5% | +| 8a | `f821b16` (remoto) | 16-abr | 0.58 | false | false | 0.65 / 120s | 0–1 (off) | — | 1.5m / 5% | +| 8b | `f821b16` + `.env` local | 17-abr | 0.58 | false | false | 0.65 / 120s | **0.40–0.85** | — | 1.5m / 5% | +| **9** | `d36b3f4` (HEAD) | 24-abr | 0.58 | **true** | false | 0.65 / 120s | **0.45–0.58** | **2×** @ 0.45–0.50 / prob≥0.70 | 1.5m / 5% | + +### Parâmetros 5m por versão + +| # | Hash | Data | flip | CT | CD | disableFlip | disableSL | entry filter | TD | +|---|---|---|---|---|---|---|---|---|---| +| 1 | `a8e2101` | 07-abr | 0.58 | 1 | 0s | false | false | — | 1.5m / 5% | +| 2 | `45170e2` | 13-abr | 0.58 | 3 | 90s | false | false | — | 1.5m / 5% | +| 3 | `fe27514` | 14-abr | **0.62** | **5** | 90s | false | false | — | 1.5m / 5% | +| 4 | `2c0cbc7` | 15-abr | 0.62 | 5 | 90s | false | false | — | 1.5m / 5% | +| 5 | `fa9fc9b` | 15-abr | 0.62 | 5 | 90s | **true** | false | — | 1.5m / 5% | +| 6 | `577d5f4` | 15-abr | 0.62 | 5 | 90s | true | false | — | 1.5m / 5% | +| 7 | `83a4a7b` | 15-abr | 0.62 | 5 | 90s | true | false | — (+PTB $30) | 1.5m / 5% | +| 8a | `f821b16` (remoto) | 16-abr | 0.62 | 5 | 90s | true | **true** | 0–1 (off) | 1.5m / 5% | +| 8b | `f821b16` + `.env` local | 17-abr | 0.62 | 5 | 90s | true | true | **0.40–0.85** | 1.5m / 5% | +| **9** | `d36b3f4` (HEAD) | 24-abr | 0.62 | 5 | 90s | true | true | **0.50–0.60** | **2.5m / 15%** | + +### Resultados (paper-trading acumulado) + +| # | Hash | Principal delta vs anterior | 15m (t / WR / PnL) | 5m (t / WR / PnL) | +|---|---|---|---|---| +| 1 | `a8e2101` | **Baseline** — sem cooldown, SL sem guards | 93 / 36.6% / **+$2.75** | 395 / 39.2% / **+$42.54** | +| 2 | `45170e2` | +SL guards (prob 0.65, duração 120s); +cooldown 60s/90s | 94 / 34.0% / +$0.64 | 283 / 35.3% / **+$28.73** | +| 3 | `fe27514` | Flip 5m endurecido: 0.58→0.62, CT 3→5 | 19 / 63.2% / +$4.92 ⚠ | 163 / 32.5% / **+$18.82** | +| 4 | `2c0cbc7` | Sem mudança de estratégia (só Telegram) | 44 / 29.5% / −$2.89 | 119 / 33.6% / +$2.68 | +| 5 | `fa9fc9b` | **disableSignalFlip=true** (5m) | 2 / 100% / +$1.93 ⚠ | 8 / 37.5% / −$0.44 ⚠ | +| 6 | `577d5f4` | +late-start guard | 16 / 25.0% / −$4.44 ⚠ | 52 / 44.2% / +$2.28 | +| 7 | `83a4a7b` | +outcome API settlement, +PTB safe margin $30 | 44 / 50.0% / **+$11.34** | 71 / 42.3% / +$4.52 | +| 8a | `f821b16` (remoto) | **disableStopLoss=true** (5m); sem entry filter | 51 / 29.4% / −$1.98 | 69 / 37.7% / −$12.01 | +| 8b | `f821b16` + `.env` local | idem + `.env` override: entry 0.40–0.85 | 39 / 51.3% / **+$4.96** | 188 / 48.9% / **+$15.27** | +| **9** | `d36b3f4` (HEAD) | entry filter 15m 0.45–0.58 / 5m 0.50–0.60; disableSignalFlip=true (15m); highConvMult=2× (15m); TD 5m 2.5m/15% | **em curso** | **em curso** | + +### Observações para fine-tuning + +- **Maior 5m PnL absoluto** (v1, +$42.54) veio com params **mais frouxos** (flip@0.58, sem cooldown, sem guards) e **maior volume** (395 trades). Endurecer exits reduziu PnL proporcionalmente mais que o ruído que cortaram. +- **15m: melhor PnL documentado** foi v7 (+$11.34, 50% WR) — com flip 15m **ainda ON** e PTB guard recém-adicionado. O v9 está desabilitando flip 15m baseado em análise de v8 (51 trades remotos), mas v7 mostra que flip 15m pode funcionar com PTB guard ativo. Candidato a re-testar. +- **Entry filter 0.40–0.85 (v8b local)** gerou os melhores resultados recentes em ambos os bots. A nova faixa v9 (15m 0.45–0.58 / 5m 0.50–0.60) é bem mais restritiva — vale comparar se o ganho de qualidade supera a perda de volume. +- **Amostra mínima:** v3 (19 trades 15m), v5 (2 + 8), v6 (16 trades 15m) são ⚠ amostras insuficientes — conclusões a partir delas são ruído. +- **Rodar ≥ 24h e ≥ 20 trades por bot** antes de comparar novos params com estas linhas. + +--- + +## Resumo comparativo (versão curta) + +| # | Hash | 15m Trades | 15m Win% | 15m PnL | 5m Trades | 5m Win% | 5m PnL | +|---|---|---|---|---|---|---|---| +| 1 | `a8e2101` | 93 | 36.6% | +$2.75 | 395 | 39.2% | +$42.54 | +| 2 | `45170e2` | 94 | 34.0% | +$0.64 | 283 | 35.3% | +$28.73 | +| 3 | `fe27514` | 19 | 63.2% | +$4.92 | 163 | 32.5% | +$18.82 | +| 4 | `2c0cbc7` | 44 | 29.5% | −$2.89 | 119 | 33.6% | +$2.68 | +| 5 | `fa9fc9b` | 2 | 100% | +$1.93 | 8 | 37.5% | −$0.44 | +| 6 | `577d5f4` | 16 | 25.0% | −$4.44 | 52 | 44.2% | +$2.28 | +| 7 | `83a4a7b` | 44 | 50.0% | +$11.34 | 71 | 42.3% | +$4.52 | +| 8a | `f821b16` (remoto) | 51 | 29.4% | −$1.98 | 69 | 37.7% | −$12.01 | +| 8b | `f821b16` + `.env` local | 39 | 51.3% | +$4.96 | 188 | 48.9% | +$15.27 | +| **9** | `d36b3f4` (HEAD) | — | — | — | — | — | — | ### Principais decisões estratégicas e aprendizados diff --git a/dashboard/.gitignore b/dashboard/.gitignore new file mode 100644 index 00000000..a547bf36 --- /dev/null +++ b/dashboard/.gitignore @@ -0,0 +1,24 @@ +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +pnpm-debug.log* +lerna-debug.log* + +node_modules +dist +dist-ssr +*.local + +# Editor directories and files +.vscode/* +!.vscode/extensions.json +.idea +.DS_Store +*.suo +*.ntvs* +*.njsproj +*.sln +*.sw? diff --git a/dashboard/README.md b/dashboard/README.md new file mode 100644 index 00000000..7dbf7ebf --- /dev/null +++ b/dashboard/README.md @@ -0,0 +1,73 @@ +# React + TypeScript + Vite + +This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules. + +Currently, two official plugins are available: + +- [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react) uses [Oxc](https://oxc.rs) +- [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react-swc) uses [SWC](https://swc.rs/) + +## React Compiler + +The React Compiler is not enabled on this template because of its impact on dev & build performances. To add it, see [this documentation](https://react.dev/learn/react-compiler/installation). + +## Expanding the ESLint configuration + +If you are developing a production application, we recommend updating the configuration to enable type-aware lint rules: + +```js +export default defineConfig([ + globalIgnores(['dist']), + { + files: ['**/*.{ts,tsx}'], + extends: [ + // Other configs... + + // Remove tseslint.configs.recommended and replace with this + tseslint.configs.recommendedTypeChecked, + // Alternatively, use this for stricter rules + tseslint.configs.strictTypeChecked, + // Optionally, add this for stylistic rules + tseslint.configs.stylisticTypeChecked, + + // Other configs... + ], + languageOptions: { + parserOptions: { + project: ['./tsconfig.node.json', './tsconfig.app.json'], + tsconfigRootDir: import.meta.dirname, + }, + // other options... + }, + }, +]) +``` + +You can also install [eslint-plugin-react-x](https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-x) and [eslint-plugin-react-dom](https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-dom) for React-specific lint rules: + +```js +// eslint.config.js +import reactX from 'eslint-plugin-react-x' +import reactDom from 'eslint-plugin-react-dom' + +export default defineConfig([ + globalIgnores(['dist']), + { + files: ['**/*.{ts,tsx}'], + extends: [ + // Other configs... + // Enable lint rules for React + reactX.configs['recommended-typescript'], + // Enable lint rules for React DOM + reactDom.configs.recommended, + ], + languageOptions: { + parserOptions: { + project: ['./tsconfig.node.json', './tsconfig.app.json'], + tsconfigRootDir: import.meta.dirname, + }, + // other options... + }, + }, +]) +``` diff --git a/dashboard/components.json b/dashboard/components.json new file mode 100644 index 00000000..4024f4d7 --- /dev/null +++ b/dashboard/components.json @@ -0,0 +1,20 @@ +{ + "$schema": "https://ui.shadcn.com/schema.json", + "style": "new-york", + "rsc": false, + "tsx": true, + "tailwind": { + "config": "", + "css": "src/index.css", + "baseColor": "zinc", + "cssVariables": true + }, + "aliases": { + "components": "@/components", + "utils": "@/lib/utils", + "ui": "@/components/ui", + "lib": "@/lib", + "hooks": "@/hooks" + }, + "iconLibrary": "lucide" +} diff --git a/dashboard/eslint.config.js b/dashboard/eslint.config.js new file mode 100644 index 00000000..ef614d25 --- /dev/null +++ b/dashboard/eslint.config.js @@ -0,0 +1,22 @@ +import js from '@eslint/js' +import globals from 'globals' +import reactHooks from 'eslint-plugin-react-hooks' +import reactRefresh from 'eslint-plugin-react-refresh' +import tseslint from 'typescript-eslint' +import { defineConfig, globalIgnores } from 'eslint/config' + +export default defineConfig([ + globalIgnores(['dist']), + { + files: ['**/*.{ts,tsx}'], + extends: [ + js.configs.recommended, + tseslint.configs.recommended, + reactHooks.configs.flat.recommended, + reactRefresh.configs.vite, + ], + languageOptions: { + globals: globals.browser, + }, + }, +]) diff --git a/dashboard/index.html b/dashboard/index.html new file mode 100644 index 00000000..984e31dc --- /dev/null +++ b/dashboard/index.html @@ -0,0 +1,13 @@ + + + + + + + dashboard + + +
    + + + diff --git a/dashboard/package-lock.json b/dashboard/package-lock.json new file mode 100644 index 00000000..dfbfa06d --- /dev/null +++ b/dashboard/package-lock.json @@ -0,0 +1,5993 @@ +{ + "name": "dashboard", + "version": "0.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "dashboard", + "version": "0.0.0", + "dependencies": { + "@tanstack/react-query": "^5.100.5", + "@tanstack/react-router": "^1.168.24", + "@tanstack/router-devtools": "^1.166.13", + "class-variance-authority": "^0.7.1", + "clsx": "^2.1.1", + "lucide-react": "^1.11.0", + "radix-ui": "^1.4.3", + "react": "^19.2.5", + "react-dom": "^19.2.5", + "recharts": "^3.8.0", + "tailwind-merge": "^3.5.0" + }, + "devDependencies": { + "@eslint/js": "^10.0.1", + "@tailwindcss/vite": "^4.2.4", + "@tanstack/router-plugin": "^1.167.27", + "@types/node": "^24.12.2", + "@types/react": "^19.2.14", + "@types/react-dom": "^19.2.3", + "@vitejs/plugin-react": "^6.0.1", + "eslint": "^10.2.1", + "eslint-plugin-react-hooks": "^7.1.1", + "eslint-plugin-react-refresh": "^0.5.2", + "globals": "^17.5.0", + "tailwindcss": "^4.2.4", + "typescript": "~6.0.2", + "typescript-eslint": "^8.58.2", + "vite": "^8.0.10" + } + }, + "node_modules/@babel/code-frame": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.0.tgz", + "integrity": "sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-validator-identifier": "^7.28.5", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/compat-data": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.29.0.tgz", + "integrity": "sha512-T1NCJqT/j9+cn8fvkt7jtwbLBfLC/1y1c7NtCeXFRgzGTsafi68MRv8yzkYSapBnFA6L3U2VSc02ciDzoAJhJg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/core": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.0.tgz", + "integrity": "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.0", + "@babel/generator": "^7.29.0", + "@babel/helper-compilation-targets": "^7.28.6", + "@babel/helper-module-transforms": "^7.28.6", + "@babel/helpers": "^7.28.6", + "@babel/parser": "^7.29.0", + "@babel/template": "^7.28.6", + "@babel/traverse": "^7.29.0", + "@babel/types": "^7.29.0", + "@jridgewell/remapping": "^2.3.5", + "convert-source-map": "^2.0.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.2.3", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/babel" + } + }, + "node_modules/@babel/generator": { + "version": "7.29.1", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.1.tgz", + "integrity": "sha512-qsaF+9Qcm2Qv8SRIMMscAvG4O3lJ0F1GuMo5HR/Bp02LopNgnZBC/EkbevHFeGs4ls/oPz9v+Bsmzbkbe+0dUw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.29.0", + "@babel/types": "^7.29.0", + "@jridgewell/gen-mapping": "^0.3.12", + "@jridgewell/trace-mapping": "^0.3.28", + "jsesc": "^3.0.2" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.28.6.tgz", + "integrity": "sha512-JYtls3hqi15fcx5GaSNL7SCTJ2MNmjrkHXg4FSpOA/grxK8KwyZ5bubHsCq8FXCkua6xhuaaBit+3b7+VZRfcA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/compat-data": "^7.28.6", + "@babel/helper-validator-option": "^7.27.1", + "browserslist": "^4.24.0", + "lru-cache": "^5.1.1", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-globals": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.28.0.tgz", + "integrity": "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-imports": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.28.6.tgz", + "integrity": "sha512-l5XkZK7r7wa9LucGw9LwZyyCUscb4x37JWTPz7swwFE/0FMQAGpiWUZn8u9DzkSBWEcK25jmvubfpw2dnAMdbw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.28.6", + "@babel/types": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-transforms": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.28.6.tgz", + "integrity": "sha512-67oXFAYr2cDLDVGLXTEABjdBJZ6drElUSI7WKp70NrpyISso3plG9SAGEF6y7zbha/wOzUByWWTJvEDVNIUGcA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-module-imports": "^7.28.6", + "@babel/helper-validator-identifier": "^7.28.5", + "@babel/traverse": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-plugin-utils": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.28.6.tgz", + "integrity": "sha512-S9gzZ/bz83GRysI7gAD4wPT/AI3uCnY+9xn+Mx/KPs2JwHJIz1W8PZkg2cqyt3RNOBM8ejcXhV6y8Og7ly/Dug==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", + "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz", + "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-option": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.27.1.tgz", + "integrity": "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helpers": { + "version": "7.29.2", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.29.2.tgz", + "integrity": "sha512-HoGuUs4sCZNezVEKdVcwqmZN8GoHirLUcLaYVNBK2J0DadGtdcqgr3BCbvH8+XUo4NGjNl3VOtSjEKNzqfFgKw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/template": "^7.28.6", + "@babel/types": "^7.29.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.29.2", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.2.tgz", + "integrity": "sha512-4GgRzy/+fsBa72/RZVJmGKPmZu9Byn8o4MoLpmNe1m8ZfYnz5emHLQz3U4gLud6Zwl0RZIcgiLD7Uq7ySFuDLA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.29.0" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/plugin-syntax-jsx": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.28.6.tgz", + "integrity": "sha512-wgEmr06G6sIpqr8YDwA2dSRTE3bJ+V0IfpzfSY3Lfgd7YWOaAdlykvJi13ZKBt8cZHfgH1IXN+CL656W3uUa4w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-typescript": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-typescript/-/plugin-syntax-typescript-7.28.6.tgz", + "integrity": "sha512-+nDNmQye7nlnuuHDboPbGm00Vqg3oO8niRRL27/4LYHUsHYh0zJ1xWOz0uRwNFmM1Avzk8wZbc6rdiYhomzv/A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/template": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.28.6.tgz", + "integrity": "sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.28.6", + "@babel/parser": "^7.28.6", + "@babel/types": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.29.0.tgz", + "integrity": "sha512-4HPiQr0X7+waHfyXPZpWPfWL/J7dcN1mx9gL6WdQVMbPnF3+ZhSMs8tCxN7oHddJE9fhNE7+lxdnlyemKfJRuA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.0", + "@babel/generator": "^7.29.0", + "@babel/helper-globals": "^7.28.0", + "@babel/parser": "^7.29.0", + "@babel/template": "^7.28.6", + "@babel/types": "^7.29.0", + "debug": "^4.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/types": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.0.tgz", + "integrity": "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.27.1", + "@babel/helper-validator-identifier": "^7.28.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@emnapi/core": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.10.0.tgz", + "integrity": "sha512-yq6OkJ4p82CAfPl0u9mQebQHKPJkY7WrIuk205cTYnYe+k2Z8YBh11FrbRG/H6ihirqcacOgl2BIO8oyMQLeXw==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/wasi-threads": "1.2.1", + "tslib": "^2.4.0" + } + }, + "node_modules/@emnapi/runtime": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.10.0.tgz", + "integrity": "sha512-ewvYlk86xUoGI0zQRNq/mC+16R1QeDlKQy21Ki3oSYXNgLb45GV1P6A0M+/s6nyCuNDqe5VpaY84BzXGwVbwFA==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@emnapi/wasi-threads": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.1.tgz", + "integrity": "sha512-uTII7OYF+/Mes/MrcIOYp5yOtSMLBWSIoLPpcgwipoiKbli6k322tcoFsxoIIxPDqW01SQGAgko4EzZi2BNv2w==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@eslint-community/eslint-utils": { + "version": "4.9.1", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.1.tgz", + "integrity": "sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "eslint-visitor-keys": "^3.4.3" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" + } + }, + "node_modules/@eslint-community/eslint-utils/node_modules/eslint-visitor-keys": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint-community/regexpp": { + "version": "4.12.2", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.2.tgz", + "integrity": "sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.0.0 || ^14.0.0 || >=16.0.0" + } + }, + "node_modules/@eslint/config-array": { + "version": "0.23.5", + "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.23.5.tgz", + "integrity": "sha512-Y3kKLvC1dvTOT+oGlqNQ1XLqK6D1HU2YXPc52NmAlJZbMMWDzGYXMiPRJ8TYD39muD/OTjlZmNJ4ib7dvSrMBA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/object-schema": "^3.0.5", + "debug": "^4.3.1", + "minimatch": "^10.2.4" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + } + }, + "node_modules/@eslint/config-helpers": { + "version": "0.5.5", + "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.5.5.tgz", + "integrity": "sha512-eIJYKTCECbP/nsKaaruF6LW967mtbQbsw4JTtSVkUQc9MneSkbrgPJAbKl9nWr0ZeowV8BfsarBmPpBzGelA2w==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^1.2.1" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + } + }, + "node_modules/@eslint/core": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@eslint/core/-/core-1.2.1.tgz", + "integrity": "sha512-MwcE1P+AZ4C6DWlpin/OmOA54mmIZ/+xZuJiQd4SyB29oAJjN30UW9wkKNptW2ctp4cEsvhlLY/CsQ1uoHDloQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@types/json-schema": "^7.0.15" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + } + }, + "node_modules/@eslint/js": { + "version": "10.0.1", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-10.0.1.tgz", + "integrity": "sha512-zeR9k5pd4gxjZ0abRoIaxdc7I3nDktoXZk2qOv9gCNWx3mVwEn32VRhyLaRsDiJjTs0xq/T8mfPtyuXu7GWBcA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://eslint.org/donate" + }, + "peerDependencies": { + "eslint": "^10.0.0" + }, + "peerDependenciesMeta": { + "eslint": { + "optional": true + } + } + }, + "node_modules/@eslint/object-schema": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-3.0.5.tgz", + "integrity": "sha512-vqTaUEgxzm+YDSdElad6PiRoX4t8VGDjCtt05zn4nU810UIx/uNEV7/lZJ6KwFThKZOzOxzXy48da+No7HZaMw==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + } + }, + "node_modules/@eslint/plugin-kit": { + "version": "0.7.1", + "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.7.1.tgz", + "integrity": "sha512-rZAP3aVgB9ds9KOeUSL+zZ21hPmo8dh6fnIFwRQj5EAZl9gzR7wxYbYXYysAM8CTqGmUGyp2S4kUdV17MnGuWQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^1.2.1", + "levn": "^0.4.1" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + } + }, + "node_modules/@floating-ui/core": { + "version": "1.7.5", + "resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.7.5.tgz", + "integrity": "sha512-1Ih4WTWyw0+lKyFMcBHGbb5U5FtuHJuujoyyr5zTaWS5EYMeT6Jb2AuDeftsCsEuchO+mM2ij5+q9crhydzLhQ==", + "license": "MIT", + "dependencies": { + "@floating-ui/utils": "^0.2.11" + } + }, + "node_modules/@floating-ui/dom": { + "version": "1.7.6", + "resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.7.6.tgz", + "integrity": "sha512-9gZSAI5XM36880PPMm//9dfiEngYoC6Am2izES1FF406YFsjvyBMmeJ2g4SAju3xWwtuynNRFL2s9hgxpLI5SQ==", + "license": "MIT", + "dependencies": { + "@floating-ui/core": "^1.7.5", + "@floating-ui/utils": "^0.2.11" + } + }, + "node_modules/@floating-ui/react-dom": { + "version": "2.1.8", + "resolved": "https://registry.npmjs.org/@floating-ui/react-dom/-/react-dom-2.1.8.tgz", + "integrity": "sha512-cC52bHwM/n/CxS87FH0yWdngEZrjdtLW/qVruo68qg+prK7ZQ4YGdut2GyDVpoGeAYe/h899rVeOVm6Oi40k2A==", + "license": "MIT", + "dependencies": { + "@floating-ui/dom": "^1.7.6" + }, + "peerDependencies": { + "react": ">=16.8.0", + "react-dom": ">=16.8.0" + } + }, + "node_modules/@floating-ui/utils": { + "version": "0.2.11", + "resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.11.tgz", + "integrity": "sha512-RiB/yIh78pcIxl6lLMG0CgBXAZ2Y0eVHqMPYugu+9U0AeT6YBeiJpf7lbdJNIugFP5SIjwNRgo4DhR1Qxi26Gg==", + "license": "MIT" + }, + "node_modules/@humanfs/core": { + "version": "0.19.2", + "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.2.tgz", + "integrity": "sha512-UhXNm+CFMWcbChXywFwkmhqjs3PRCmcSa/hfBgLIb7oQ5HNb1wS0icWsGtSAUNgefHeI+eBrA8I1fxmbHsGdvA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@humanfs/types": "^0.15.0" + }, + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanfs/node": { + "version": "0.16.8", + "resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.8.tgz", + "integrity": "sha512-gE1eQNZ3R++kTzFUpdGlpmy8kDZD/MLyHqDwqjkVQI0JMdI1D51sy1H958PNXYkM2rAac7e5/CnIKZrHtPh3BQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@humanfs/core": "^0.19.2", + "@humanfs/types": "^0.15.0", + "@humanwhocodes/retry": "^0.4.0" + }, + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanfs/types": { + "version": "0.15.0", + "resolved": "https://registry.npmjs.org/@humanfs/types/-/types-0.15.0.tgz", + "integrity": "sha512-ZZ1w0aoQkwuUuC7Yf+7sdeaNfqQiiLcSRbfI08oAxqLtpXQr9AIVX7Ay7HLDuiLYAaFPu8oBYNq/QIi9URHJ3Q==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanwhocodes/module-importer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", + "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.22" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@humanwhocodes/retry": { + "version": "0.4.3", + "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.3.tgz", + "integrity": "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.13", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/remapping": { + "version": "2.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz", + "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "dev": true, + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@napi-rs/wasm-runtime": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.4.tgz", + "integrity": "sha512-3NQNNgA1YSlJb/kMH1ildASP9HW7/7kYnRI2szWJaofaS1hWmbGI4H+d3+22aGzXXN9IJ+n+GiFVcGipJP18ow==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@tybys/wasm-util": "^0.10.1" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Brooooooklyn" + }, + "peerDependencies": { + "@emnapi/core": "^1.7.1", + "@emnapi/runtime": "^1.7.1" + } + }, + "node_modules/@oxc-project/types": { + "version": "0.127.0", + "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.127.0.tgz", + "integrity": "sha512-aIYXQBo4lCbO4z0R3FHeucQHpF46l2LbMdxRvqvuRuW2OxdnSkcng5B8+K12spgLDj93rtN3+J2Vac/TIO+ciQ==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/Boshen" + } + }, + "node_modules/@radix-ui/number": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/number/-/number-1.1.1.tgz", + "integrity": "sha512-MkKCwxlXTgz6CFoJx3pCwn07GKp36+aZyu/u2Ln2VrA5DcdyCZkASEDBTd8x5whTQQL5CiYf4prXKLcgQdv29g==", + "license": "MIT" + }, + "node_modules/@radix-ui/primitive": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@radix-ui/primitive/-/primitive-1.1.3.tgz", + "integrity": "sha512-JTF99U/6XIjCBo0wqkU5sK10glYe27MRRsfwoiq5zzOEZLHU3A3KCMa5X/azekYRCJ0HlwI0crAXS/5dEHTzDg==", + "license": "MIT" + }, + "node_modules/@radix-ui/react-accessible-icon": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/@radix-ui/react-accessible-icon/-/react-accessible-icon-1.1.7.tgz", + "integrity": "sha512-XM+E4WXl0OqUJFovy6GjmxxFyx9opfCAIUku4dlKRd5YEPqt4kALOkQOp0Of6reHuUkJuiPBEc5k0o4z4lTC8A==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-visually-hidden": "1.2.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-accordion": { + "version": "1.2.12", + "resolved": "https://registry.npmjs.org/@radix-ui/react-accordion/-/react-accordion-1.2.12.tgz", + "integrity": "sha512-T4nygeh9YE9dLRPhAHSeOZi7HBXo+0kYIPJXayZfvWOWA0+n3dESrZbjfDPUABkUNym6Hd+f2IR113To8D2GPA==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-collapsible": "1.1.12", + "@radix-ui/react-collection": "1.1.7", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-direction": "1.1.1", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-controllable-state": "1.2.2" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-alert-dialog": { + "version": "1.1.15", + "resolved": "https://registry.npmjs.org/@radix-ui/react-alert-dialog/-/react-alert-dialog-1.1.15.tgz", + "integrity": "sha512-oTVLkEw5GpdRe29BqJ0LSDFWI3qu0vR1M0mUkOQWDIUnY/QIkLpgDMWuKxP94c2NAC2LGcgVhG1ImF3jkZ5wXw==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-dialog": "1.1.15", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-slot": "1.2.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-arrow": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/@radix-ui/react-arrow/-/react-arrow-1.1.7.tgz", + "integrity": "sha512-F+M1tLhO+mlQaOWspE8Wstg+z6PwxwRd8oQ8IXceWz92kfAmalTRf0EjrouQeo7QssEPfCn05B4Ihs1K9WQ/7w==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-primitive": "2.1.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-aspect-ratio": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/@radix-ui/react-aspect-ratio/-/react-aspect-ratio-1.1.7.tgz", + "integrity": "sha512-Yq6lvO9HQyPwev1onK1daHCHqXVLzPhSVjmsNjCa2Zcxy2f7uJD2itDtxknv6FzAKCwD1qQkeVDmX/cev13n/g==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-primitive": "2.1.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-avatar": { + "version": "1.1.10", + "resolved": "https://registry.npmjs.org/@radix-ui/react-avatar/-/react-avatar-1.1.10.tgz", + "integrity": "sha512-V8piFfWapM5OmNCXTzVQY+E1rDa53zY+MQ4Y7356v4fFz6vqCyUtIz2rUD44ZEdwg78/jKmMJHj07+C/Z/rcog==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-callback-ref": "1.1.1", + "@radix-ui/react-use-is-hydrated": "0.1.0", + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-checkbox": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-checkbox/-/react-checkbox-1.3.3.tgz", + "integrity": "sha512-wBbpv+NQftHDdG86Qc0pIyXk5IR3tM8Vd0nWLKDcX8nNn4nXFOFwsKuqw2okA/1D/mpaAkmuyndrPJTYDNZtFw==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-presence": "1.1.5", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-controllable-state": "1.2.2", + "@radix-ui/react-use-previous": "1.1.1", + "@radix-ui/react-use-size": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-collapsible": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/@radix-ui/react-collapsible/-/react-collapsible-1.1.12.tgz", + "integrity": "sha512-Uu+mSh4agx2ib1uIGPP4/CKNULyajb3p92LsVXmH2EHVMTfZWpll88XJ0j4W0z3f8NK1eYl1+Mf/szHPmcHzyA==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-presence": "1.1.5", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-controllable-state": "1.2.2", + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-collection": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/@radix-ui/react-collection/-/react-collection-1.1.7.tgz", + "integrity": "sha512-Fh9rGN0MoI4ZFUNyfFVNU4y9LUz93u9/0K+yLgA2bwRojxM8JU1DyvvMBabnZPBgMWREAJvU2jjVzq+LrFUglw==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-slot": "1.2.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-compose-refs": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.2.tgz", + "integrity": "sha512-z4eqJvfiNnFMHIIvXP3CY57y2WJs5g2v3X0zm9mEJkrkNv4rDxu+sg9Jh8EkXyeqBkB7SOcboo9dMVqhyrACIg==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-context": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-context/-/react-context-1.1.2.tgz", + "integrity": "sha512-jCi/QKUM2r1Ju5a3J64TH2A5SpKAgh0LpknyqdQ4m6DCV0xJ2HG1xARRwNGPQfi1SLdLWZ1OJz6F4OMBBNiGJA==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-context-menu": { + "version": "2.2.16", + "resolved": "https://registry.npmjs.org/@radix-ui/react-context-menu/-/react-context-menu-2.2.16.tgz", + "integrity": "sha512-O8morBEW+HsVG28gYDZPTrT9UUovQUlJue5YO836tiTJhuIWBm/zQHc7j388sHWtdH/xUZurK9olD2+pcqx5ww==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-menu": "2.1.16", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-callback-ref": "1.1.1", + "@radix-ui/react-use-controllable-state": "1.2.2" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-dialog": { + "version": "1.1.15", + "resolved": "https://registry.npmjs.org/@radix-ui/react-dialog/-/react-dialog-1.1.15.tgz", + "integrity": "sha512-TCglVRtzlffRNxRMEyR36DGBLJpeusFcgMVD9PZEzAKnUs1lKCgX5u9BmC2Yg+LL9MgZDugFFs1Vl+Jp4t/PGw==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-dismissable-layer": "1.1.11", + "@radix-ui/react-focus-guards": "1.1.3", + "@radix-ui/react-focus-scope": "1.1.7", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-portal": "1.1.9", + "@radix-ui/react-presence": "1.1.5", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-slot": "1.2.3", + "@radix-ui/react-use-controllable-state": "1.2.2", + "aria-hidden": "^1.2.4", + "react-remove-scroll": "^2.6.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-direction": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-direction/-/react-direction-1.1.1.tgz", + "integrity": "sha512-1UEWRX6jnOA2y4H5WczZ44gOOjTEmlqv1uNW4GAJEO5+bauCBhv8snY65Iw5/VOS/ghKN9gr2KjnLKxrsvoMVw==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-dismissable-layer": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/@radix-ui/react-dismissable-layer/-/react-dismissable-layer-1.1.11.tgz", + "integrity": "sha512-Nqcp+t5cTB8BinFkZgXiMJniQH0PsUt2k51FUhbdfeKvc4ACcG2uQniY/8+h1Yv6Kza4Q7lD7PQV0z0oicE0Mg==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-callback-ref": "1.1.1", + "@radix-ui/react-use-escape-keydown": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-dropdown-menu": { + "version": "2.1.16", + "resolved": "https://registry.npmjs.org/@radix-ui/react-dropdown-menu/-/react-dropdown-menu-2.1.16.tgz", + "integrity": "sha512-1PLGQEynI/3OX/ftV54COn+3Sud/Mn8vALg2rWnBLnRaGtJDduNW/22XjlGgPdpcIbiQxjKtb7BkcjP00nqfJw==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-menu": "2.1.16", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-controllable-state": "1.2.2" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-focus-guards": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-focus-guards/-/react-focus-guards-1.1.3.tgz", + "integrity": "sha512-0rFg/Rj2Q62NCm62jZw0QX7a3sz6QCQU0LpZdNrJX8byRGaGVTqbrW9jAoIAHyMQqsNpeZ81YgSizOt5WXq0Pw==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-focus-scope": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/@radix-ui/react-focus-scope/-/react-focus-scope-1.1.7.tgz", + "integrity": "sha512-t2ODlkXBQyn7jkl6TNaw/MtVEVvIGelJDCG41Okq/KwUsJBwQ4XVZsHAVUkK4mBv3ewiAS3PGuUWuY2BoK4ZUw==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-callback-ref": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-form": { + "version": "0.1.8", + "resolved": "https://registry.npmjs.org/@radix-ui/react-form/-/react-form-0.1.8.tgz", + "integrity": "sha512-QM70k4Zwjttifr5a4sZFts9fn8FzHYvQ5PiB19O2HsYibaHSVt9fH9rzB0XZo/YcM+b7t/p7lYCT/F5eOeF5yQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-label": "2.1.7", + "@radix-ui/react-primitive": "2.1.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-hover-card": { + "version": "1.1.15", + "resolved": "https://registry.npmjs.org/@radix-ui/react-hover-card/-/react-hover-card-1.1.15.tgz", + "integrity": "sha512-qgTkjNT1CfKMoP0rcasmlH2r1DAiYicWsDsufxl940sT2wHNEWWv6FMWIQXWhVdmC1d/HYfbhQx60KYyAtKxjg==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-dismissable-layer": "1.1.11", + "@radix-ui/react-popper": "1.2.8", + "@radix-ui/react-portal": "1.1.9", + "@radix-ui/react-presence": "1.1.5", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-controllable-state": "1.2.2" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-id": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-id/-/react-id-1.1.1.tgz", + "integrity": "sha512-kGkGegYIdQsOb4XjsfM97rXsiHaBwco+hFI66oO4s9LU+PLAC5oJ7khdOVFxkhsmlbpUqDAvXw11CluXP+jkHg==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-label": { + "version": "2.1.7", + "resolved": "https://registry.npmjs.org/@radix-ui/react-label/-/react-label-2.1.7.tgz", + "integrity": "sha512-YT1GqPSL8kJn20djelMX7/cTRp/Y9w5IZHvfxQTVHrOqa2yMl7i/UfMqKRU5V7mEyKTrUVgJXhNQPVCG8PBLoQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-primitive": "2.1.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-menu": { + "version": "2.1.16", + "resolved": "https://registry.npmjs.org/@radix-ui/react-menu/-/react-menu-2.1.16.tgz", + "integrity": "sha512-72F2T+PLlphrqLcAotYPp0uJMr5SjP5SL01wfEspJbru5Zs5vQaSHb4VB3ZMJPimgHHCHG7gMOeOB9H3Hdmtxg==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-collection": "1.1.7", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-direction": "1.1.1", + "@radix-ui/react-dismissable-layer": "1.1.11", + "@radix-ui/react-focus-guards": "1.1.3", + "@radix-ui/react-focus-scope": "1.1.7", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-popper": "1.2.8", + "@radix-ui/react-portal": "1.1.9", + "@radix-ui/react-presence": "1.1.5", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-roving-focus": "1.1.11", + "@radix-ui/react-slot": "1.2.3", + "@radix-ui/react-use-callback-ref": "1.1.1", + "aria-hidden": "^1.2.4", + "react-remove-scroll": "^2.6.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-menubar": { + "version": "1.1.16", + "resolved": "https://registry.npmjs.org/@radix-ui/react-menubar/-/react-menubar-1.1.16.tgz", + "integrity": "sha512-EB1FktTz5xRRi2Er974AUQZWg2yVBb1yjip38/lgwtCVRd3a+maUoGHN/xs9Yv8SY8QwbSEb+YrxGadVWbEutA==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-collection": "1.1.7", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-direction": "1.1.1", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-menu": "2.1.16", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-roving-focus": "1.1.11", + "@radix-ui/react-use-controllable-state": "1.2.2" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-navigation-menu": { + "version": "1.2.14", + "resolved": "https://registry.npmjs.org/@radix-ui/react-navigation-menu/-/react-navigation-menu-1.2.14.tgz", + "integrity": "sha512-YB9mTFQvCOAQMHU+C/jVl96WmuWeltyUEpRJJky51huhds5W2FQr1J8D/16sQlf0ozxkPK8uF3niQMdUwZPv5w==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-collection": "1.1.7", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-direction": "1.1.1", + "@radix-ui/react-dismissable-layer": "1.1.11", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-presence": "1.1.5", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-callback-ref": "1.1.1", + "@radix-ui/react-use-controllable-state": "1.2.2", + "@radix-ui/react-use-layout-effect": "1.1.1", + "@radix-ui/react-use-previous": "1.1.1", + "@radix-ui/react-visually-hidden": "1.2.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-one-time-password-field": { + "version": "0.1.8", + "resolved": "https://registry.npmjs.org/@radix-ui/react-one-time-password-field/-/react-one-time-password-field-0.1.8.tgz", + "integrity": "sha512-ycS4rbwURavDPVjCb5iS3aG4lURFDILi6sKI/WITUMZ13gMmn/xGjpLoqBAalhJaDk8I3UbCM5GzKHrnzwHbvg==", + "license": "MIT", + "dependencies": { + "@radix-ui/number": "1.1.1", + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-collection": "1.1.7", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-direction": "1.1.1", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-roving-focus": "1.1.11", + "@radix-ui/react-use-controllable-state": "1.2.2", + "@radix-ui/react-use-effect-event": "0.0.2", + "@radix-ui/react-use-is-hydrated": "0.1.0", + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-password-toggle-field": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-password-toggle-field/-/react-password-toggle-field-0.1.3.tgz", + "integrity": "sha512-/UuCrDBWravcaMix4TdT+qlNdVwOM1Nck9kWx/vafXsdfj1ChfhOdfi3cy9SGBpWgTXwYCuboT/oYpJy3clqfw==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-controllable-state": "1.2.2", + "@radix-ui/react-use-effect-event": "0.0.2", + "@radix-ui/react-use-is-hydrated": "0.1.0" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-popover": { + "version": "1.1.15", + "resolved": "https://registry.npmjs.org/@radix-ui/react-popover/-/react-popover-1.1.15.tgz", + "integrity": "sha512-kr0X2+6Yy/vJzLYJUPCZEc8SfQcf+1COFoAqauJm74umQhta9M7lNJHP7QQS3vkvcGLQUbWpMzwrXYwrYztHKA==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-dismissable-layer": "1.1.11", + "@radix-ui/react-focus-guards": "1.1.3", + "@radix-ui/react-focus-scope": "1.1.7", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-popper": "1.2.8", + "@radix-ui/react-portal": "1.1.9", + "@radix-ui/react-presence": "1.1.5", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-slot": "1.2.3", + "@radix-ui/react-use-controllable-state": "1.2.2", + "aria-hidden": "^1.2.4", + "react-remove-scroll": "^2.6.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-popper": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@radix-ui/react-popper/-/react-popper-1.2.8.tgz", + "integrity": "sha512-0NJQ4LFFUuWkE7Oxf0htBKS6zLkkjBH+hM1uk7Ng705ReR8m/uelduy1DBo0PyBXPKVnBA6YBlU94MBGXrSBCw==", + "license": "MIT", + "dependencies": { + "@floating-ui/react-dom": "^2.0.0", + "@radix-ui/react-arrow": "1.1.7", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-callback-ref": "1.1.1", + "@radix-ui/react-use-layout-effect": "1.1.1", + "@radix-ui/react-use-rect": "1.1.1", + "@radix-ui/react-use-size": "1.1.1", + "@radix-ui/rect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-portal": { + "version": "1.1.9", + "resolved": "https://registry.npmjs.org/@radix-ui/react-portal/-/react-portal-1.1.9.tgz", + "integrity": "sha512-bpIxvq03if6UNwXZ+HTK71JLh4APvnXntDc6XOX8UVq4XQOVl7lwok0AvIl+b8zgCw3fSaVTZMpAPPagXbKmHQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-presence": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@radix-ui/react-presence/-/react-presence-1.1.5.tgz", + "integrity": "sha512-/jfEwNDdQVBCNvjkGit4h6pMOzq8bHkopq458dPt2lMjx+eBQUohZNG9A7DtO/O5ukSbxuaNGXMjHicgwy6rQQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-primitive": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.3.tgz", + "integrity": "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-slot": "1.2.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-progress": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/@radix-ui/react-progress/-/react-progress-1.1.7.tgz", + "integrity": "sha512-vPdg/tF6YC/ynuBIJlk1mm7Le0VgW6ub6J2UWnTQ7/D23KXcPI1qy+0vBkgKgd38RCMJavBXpB83HPNFMTb0Fg==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-primitive": "2.1.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-radio-group": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/@radix-ui/react-radio-group/-/react-radio-group-1.3.8.tgz", + "integrity": "sha512-VBKYIYImA5zsxACdisNQ3BjCBfmbGH3kQlnFVqlWU4tXwjy7cGX8ta80BcrO+WJXIn5iBylEH3K6ZTlee//lgQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-direction": "1.1.1", + "@radix-ui/react-presence": "1.1.5", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-roving-focus": "1.1.11", + "@radix-ui/react-use-controllable-state": "1.2.2", + "@radix-ui/react-use-previous": "1.1.1", + "@radix-ui/react-use-size": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-roving-focus": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/@radix-ui/react-roving-focus/-/react-roving-focus-1.1.11.tgz", + "integrity": "sha512-7A6S9jSgm/S+7MdtNDSb+IU859vQqJ/QAtcYQcfFC6W8RS4IxIZDldLR0xqCFZ6DCyrQLjLPsxtTNch5jVA4lA==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-collection": "1.1.7", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-direction": "1.1.1", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-callback-ref": "1.1.1", + "@radix-ui/react-use-controllable-state": "1.2.2" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-scroll-area": { + "version": "1.2.10", + "resolved": "https://registry.npmjs.org/@radix-ui/react-scroll-area/-/react-scroll-area-1.2.10.tgz", + "integrity": "sha512-tAXIa1g3sM5CGpVT0uIbUx/U3Gs5N8T52IICuCtObaos1S8fzsrPXG5WObkQN3S6NVl6wKgPhAIiBGbWnvc97A==", + "license": "MIT", + "dependencies": { + "@radix-ui/number": "1.1.1", + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-direction": "1.1.1", + "@radix-ui/react-presence": "1.1.5", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-callback-ref": "1.1.1", + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-select": { + "version": "2.2.6", + "resolved": "https://registry.npmjs.org/@radix-ui/react-select/-/react-select-2.2.6.tgz", + "integrity": "sha512-I30RydO+bnn2PQztvo25tswPH+wFBjehVGtmagkU78yMdwTwVf12wnAOF+AeP8S2N8xD+5UPbGhkUfPyvT+mwQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/number": "1.1.1", + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-collection": "1.1.7", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-direction": "1.1.1", + "@radix-ui/react-dismissable-layer": "1.1.11", + "@radix-ui/react-focus-guards": "1.1.3", + "@radix-ui/react-focus-scope": "1.1.7", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-popper": "1.2.8", + "@radix-ui/react-portal": "1.1.9", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-slot": "1.2.3", + "@radix-ui/react-use-callback-ref": "1.1.1", + "@radix-ui/react-use-controllable-state": "1.2.2", + "@radix-ui/react-use-layout-effect": "1.1.1", + "@radix-ui/react-use-previous": "1.1.1", + "@radix-ui/react-visually-hidden": "1.2.3", + "aria-hidden": "^1.2.4", + "react-remove-scroll": "^2.6.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-separator": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/@radix-ui/react-separator/-/react-separator-1.1.7.tgz", + "integrity": "sha512-0HEb8R9E8A+jZjvmFCy/J4xhbXy3TV+9XSnGJ3KvTtjlIUy/YQ/p6UYZvi7YbeoeXdyU9+Y3scizK6hkY37baA==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-primitive": "2.1.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-slider": { + "version": "1.3.6", + "resolved": "https://registry.npmjs.org/@radix-ui/react-slider/-/react-slider-1.3.6.tgz", + "integrity": "sha512-JPYb1GuM1bxfjMRlNLE+BcmBC8onfCi60Blk7OBqi2MLTFdS+8401U4uFjnwkOr49BLmXxLC6JHkvAsx5OJvHw==", + "license": "MIT", + "dependencies": { + "@radix-ui/number": "1.1.1", + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-collection": "1.1.7", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-direction": "1.1.1", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-controllable-state": "1.2.2", + "@radix-ui/react-use-layout-effect": "1.1.1", + "@radix-ui/react-use-previous": "1.1.1", + "@radix-ui/react-use-size": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-slot": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz", + "integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.2" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-switch": { + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/@radix-ui/react-switch/-/react-switch-1.2.6.tgz", + "integrity": "sha512-bByzr1+ep1zk4VubeEVViV592vu2lHE2BZY5OnzehZqOOgogN80+mNtCqPkhn2gklJqOpxWgPoYTSnhBCqpOXQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-controllable-state": "1.2.2", + "@radix-ui/react-use-previous": "1.1.1", + "@radix-ui/react-use-size": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-tabs": { + "version": "1.1.13", + "resolved": "https://registry.npmjs.org/@radix-ui/react-tabs/-/react-tabs-1.1.13.tgz", + "integrity": "sha512-7xdcatg7/U+7+Udyoj2zodtI9H/IIopqo+YOIcZOq1nJwXWBZ9p8xiu5llXlekDbZkca79a/fozEYQXIA4sW6A==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-direction": "1.1.1", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-presence": "1.1.5", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-roving-focus": "1.1.11", + "@radix-ui/react-use-controllable-state": "1.2.2" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-toast": { + "version": "1.2.15", + "resolved": "https://registry.npmjs.org/@radix-ui/react-toast/-/react-toast-1.2.15.tgz", + "integrity": "sha512-3OSz3TacUWy4WtOXV38DggwxoqJK4+eDkNMl5Z/MJZaoUPaP4/9lf81xXMe1I2ReTAptverZUpbPY4wWwWyL5g==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-collection": "1.1.7", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-dismissable-layer": "1.1.11", + "@radix-ui/react-portal": "1.1.9", + "@radix-ui/react-presence": "1.1.5", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-callback-ref": "1.1.1", + "@radix-ui/react-use-controllable-state": "1.2.2", + "@radix-ui/react-use-layout-effect": "1.1.1", + "@radix-ui/react-visually-hidden": "1.2.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-toggle": { + "version": "1.1.10", + "resolved": "https://registry.npmjs.org/@radix-ui/react-toggle/-/react-toggle-1.1.10.tgz", + "integrity": "sha512-lS1odchhFTeZv3xwHH31YPObmJn8gOg7Lq12inrr0+BH/l3Tsq32VfjqH1oh80ARM3mlkfMic15n0kg4sD1poQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-controllable-state": "1.2.2" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-toggle-group": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/@radix-ui/react-toggle-group/-/react-toggle-group-1.1.11.tgz", + "integrity": "sha512-5umnS0T8JQzQT6HbPyO7Hh9dgd82NmS36DQr+X/YJ9ctFNCiiQd6IJAYYZ33LUwm8M+taCz5t2ui29fHZc4Y6Q==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-direction": "1.1.1", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-roving-focus": "1.1.11", + "@radix-ui/react-toggle": "1.1.10", + "@radix-ui/react-use-controllable-state": "1.2.2" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-toolbar": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/@radix-ui/react-toolbar/-/react-toolbar-1.1.11.tgz", + "integrity": "sha512-4ol06/1bLoFu1nwUqzdD4Y5RZ9oDdKeiHIsntug54Hcr1pgaHiPqHFEaXI1IFP/EsOfROQZ8Mig9VTIRza6Tjg==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-direction": "1.1.1", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-roving-focus": "1.1.11", + "@radix-ui/react-separator": "1.1.7", + "@radix-ui/react-toggle-group": "1.1.11" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-tooltip": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@radix-ui/react-tooltip/-/react-tooltip-1.2.8.tgz", + "integrity": "sha512-tY7sVt1yL9ozIxvmbtN5qtmH2krXcBCfjEiCgKGLqunJHvgvZG2Pcl2oQ3kbcZARb1BGEHdkLzcYGO8ynVlieg==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-dismissable-layer": "1.1.11", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-popper": "1.2.8", + "@radix-ui/react-portal": "1.1.9", + "@radix-ui/react-presence": "1.1.5", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-slot": "1.2.3", + "@radix-ui/react-use-controllable-state": "1.2.2", + "@radix-ui/react-visually-hidden": "1.2.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-callback-ref": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-callback-ref/-/react-use-callback-ref-1.1.1.tgz", + "integrity": "sha512-FkBMwD+qbGQeMu1cOHnuGB6x4yzPjho8ap5WtbEJ26umhgqVXbhekKUQO+hZEL1vU92a3wHwdp0HAcqAUF5iDg==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-controllable-state": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-controllable-state/-/react-use-controllable-state-1.2.2.tgz", + "integrity": "sha512-BjasUjixPFdS+NKkypcyyN5Pmg83Olst0+c6vGov0diwTEo6mgdqVR6hxcEgFuh4QrAs7Rc+9KuGJ9TVCj0Zzg==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-use-effect-event": "0.0.2", + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-effect-event": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-effect-event/-/react-use-effect-event-0.0.2.tgz", + "integrity": "sha512-Qp8WbZOBe+blgpuUT+lw2xheLP8q0oatc9UpmiemEICxGvFLYmHm9QowVZGHtJlGbS6A6yJ3iViad/2cVjnOiA==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-escape-keydown": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-escape-keydown/-/react-use-escape-keydown-1.1.1.tgz", + "integrity": "sha512-Il0+boE7w/XebUHyBjroE+DbByORGR9KKmITzbR7MyQ4akpORYP/ZmbhAr0DG7RmmBqoOnZdy2QlvajJ2QA59g==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-use-callback-ref": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-is-hydrated": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-is-hydrated/-/react-use-is-hydrated-0.1.0.tgz", + "integrity": "sha512-U+UORVEq+cTnRIaostJv9AGdV3G6Y+zbVd+12e18jQ5A3c0xL03IhnHuiU4UV69wolOQp5GfR58NW/EgdQhwOA==", + "license": "MIT", + "dependencies": { + "use-sync-external-store": "^1.5.0" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-layout-effect": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-layout-effect/-/react-use-layout-effect-1.1.1.tgz", + "integrity": "sha512-RbJRS4UWQFkzHTTwVymMTUv8EqYhOp8dOOviLj2ugtTiXRaRQS7GLGxZTLL1jWhMeoSCf5zmcZkqTl9IiYfXcQ==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-previous": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-previous/-/react-use-previous-1.1.1.tgz", + "integrity": "sha512-2dHfToCj/pzca2Ck724OZ5L0EVrr3eHRNsG/b3xQJLA2hZpVCS99bLAX+hm1IHXDEnzU6by5z/5MIY794/a8NQ==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-rect": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-rect/-/react-use-rect-1.1.1.tgz", + "integrity": "sha512-QTYuDesS0VtuHNNvMh+CjlKJ4LJickCMUAqjlE3+j8w+RlRpwyX3apEQKGFzbZGdo7XNG1tXa+bQqIE7HIXT2w==", + "license": "MIT", + "dependencies": { + "@radix-ui/rect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-size": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-size/-/react-use-size-1.1.1.tgz", + "integrity": "sha512-ewrXRDTAqAXlkl6t/fkXWNAhFX9I+CkKlw6zjEwk86RSPKwZr3xpBRso655aqYafwtnbpHLj6toFzmd6xdVptQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-visually-hidden": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-visually-hidden/-/react-visually-hidden-1.2.3.tgz", + "integrity": "sha512-pzJq12tEaaIhqjbzpCuv/OypJY/BPavOofm+dbab+MHLajy277+1lLm6JFcGgF5eskJ6mquGirhXY2GD/8u8Ug==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-primitive": "2.1.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/rect": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/rect/-/rect-1.1.1.tgz", + "integrity": "sha512-HPwpGIzkl28mWyZqG52jiqDJ12waP11Pa1lGoiyUkIEuMLBP0oeK/C89esbXrxsky5we7dfd8U58nm0SgAWpVw==", + "license": "MIT" + }, + "node_modules/@reduxjs/toolkit": { + "version": "2.11.2", + "resolved": "https://registry.npmjs.org/@reduxjs/toolkit/-/toolkit-2.11.2.tgz", + "integrity": "sha512-Kd6kAHTA6/nUpp8mySPqj3en3dm0tdMIgbttnQ1xFMVpufoj+ADi8pXLBsd4xzTRHQa7t/Jv8W5UnCuW4kuWMQ==", + "license": "MIT", + "dependencies": { + "@standard-schema/spec": "^1.0.0", + "@standard-schema/utils": "^0.3.0", + "immer": "^11.0.0", + "redux": "^5.0.1", + "redux-thunk": "^3.1.0", + "reselect": "^5.1.0" + }, + "peerDependencies": { + "react": "^16.9.0 || ^17.0.0 || ^18 || ^19", + "react-redux": "^7.2.1 || ^8.1.3 || ^9.0.0" + }, + "peerDependenciesMeta": { + "react": { + "optional": true + }, + "react-redux": { + "optional": true + } + } + }, + "node_modules/@reduxjs/toolkit/node_modules/immer": { + "version": "11.1.4", + "resolved": "https://registry.npmjs.org/immer/-/immer-11.1.4.tgz", + "integrity": "sha512-XREFCPo6ksxVzP4E0ekD5aMdf8WMwmdNaz6vuvxgI40UaEiu6q3p8X52aU6GdyvLY3XXX/8R7JOTXStz/nBbRw==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/immer" + } + }, + "node_modules/@rolldown/binding-android-arm64": { + "version": "1.0.0-rc.17", + "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.0.0-rc.17.tgz", + "integrity": "sha512-s70pVGhw4zqGeFnXWvAzJDlvxhlRollagdCCKRgOsgUOH3N1l0LIxf83AtGzmb5SiVM4Hjl5HyarMRfdfj3DaQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-darwin-arm64": { + "version": "1.0.0-rc.17", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.0.0-rc.17.tgz", + "integrity": "sha512-4ksWc9n0mhlZpZ9PMZgTGjeOPRu8MB1Z3Tz0Mo02eWfWCHMW1zN82Qz/pL/rC+yQa+8ZnutMF0JjJe7PjwasYw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-darwin-x64": { + "version": "1.0.0-rc.17", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.0.0-rc.17.tgz", + "integrity": "sha512-SUSDOI6WwUVNcWxd02QEBjLdY1VPHvlEkw6T/8nYG322iYWCTxRb1vzk4E+mWWYehTp7ERibq54LSJGjmouOsw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-freebsd-x64": { + "version": "1.0.0-rc.17", + "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.0.0-rc.17.tgz", + "integrity": "sha512-hwnz3nw9dbJ05EDO/PvcjaaewqqDy7Y1rn1UO81l8iIK1GjenME75dl16ajbvSSMfv66WXSRCYKIqfgq2KCfxw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm-gnueabihf": { + "version": "1.0.0-rc.17", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.0.0-rc.17.tgz", + "integrity": "sha512-IS+W7epTcwANmFSQFrS1SivEXHtl1JtuQA9wlxrZTcNi6mx+FDOYrakGevvvTwgj2JvWiK8B29/qD9BELZPyXQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm64-gnu": { + "version": "1.0.0-rc.17", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.0.0-rc.17.tgz", + "integrity": "sha512-e6usGaHKW5BMNZOymS1UcEYGowQMWcgZ71Z17Sl/h2+ZziNJ1a9n3Zvcz6LdRyIW5572wBCTH/Z+bKuZouGk9Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm64-musl": { + "version": "1.0.0-rc.17", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.0.0-rc.17.tgz", + "integrity": "sha512-b/CgbwAJpmrRLp02RPfhbudf5tZnN9nsPWK82znefso832etkem8H7FSZwxrOI9djcdTP7U6YfNhbRnh7djErg==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-ppc64-gnu": { + "version": "1.0.0-rc.17", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.0.0-rc.17.tgz", + "integrity": "sha512-4EII1iNGRUN5WwGbF/kOh/EIkoDN9HsupgLQoXfY+D1oyJm7/F4t5PYU5n8SWZgG0FEwakyM8pGgwcBYruGTlA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-s390x-gnu": { + "version": "1.0.0-rc.17", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.0.0-rc.17.tgz", + "integrity": "sha512-AH8oq3XqQo4IibpVXvPeLDI5pzkpYn0WiZAfT05kFzoJ6tQNzwRdDYQ45M8I/gslbodRZwW8uxLhbSBbkv96rA==", + "cpu": [ + "s390x" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-x64-gnu": { + "version": "1.0.0-rc.17", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.0.0-rc.17.tgz", + "integrity": "sha512-cLnjV3xfo7KslbU41Z7z8BH/E1y5mzUYzAqih1d1MDaIGZRCMqTijqLv76/P7fyHuvUcfGsIpqCdddbxLLK9rA==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-x64-musl": { + "version": "1.0.0-rc.17", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.0.0-rc.17.tgz", + "integrity": "sha512-0phclDw1spsL7dUB37sIARuis2tAgomCJXAHZlpt8PXZ4Ba0dRP1e+66lsRqrfhISeN9bEGNjQs+T/Fbd7oYGw==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-openharmony-arm64": { + "version": "1.0.0-rc.17", + "resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.0.0-rc.17.tgz", + "integrity": "sha512-0ag/hEgXOwgw4t8QyQvUCxvEg+V0KBcA6YuOx9g0r02MprutRF5dyljgm3EmR02O292UX7UeS6HzWHAl6KgyhA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-wasm32-wasi": { + "version": "1.0.0-rc.17", + "resolved": "https://registry.npmjs.org/@rolldown/binding-wasm32-wasi/-/binding-wasm32-wasi-1.0.0-rc.17.tgz", + "integrity": "sha512-LEXei6vo0E5wTGwpkJ4KoT3OZJRnglwldt5ziLzOlc6qqb55z4tWNq2A+PFqCJuvWWdP53CVhG1Z9NtToDPJrA==", + "cpu": [ + "wasm32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/core": "1.10.0", + "@emnapi/runtime": "1.10.0", + "@napi-rs/wasm-runtime": "^1.1.4" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-win32-arm64-msvc": { + "version": "1.0.0-rc.17", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.0.0-rc.17.tgz", + "integrity": "sha512-gUmyzBl3SPMa6hrqFUth9sVfcLBlYsbMzBx5PlexMroZStgzGqlZ26pYG89rBb45Mnia+oil6YAIFeEWGWhoZA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-win32-x64-msvc": { + "version": "1.0.0-rc.17", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.0.0-rc.17.tgz", + "integrity": "sha512-3hkiolcUAvPB9FLb3UZdfjVVNWherN1f/skkGWJP/fgSQhYUZpSIRr0/I8ZK9TkF3F7kxvJAk0+IcKvPHk9qQg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/pluginutils": { + "version": "1.0.0-rc.7", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-rc.7.tgz", + "integrity": "sha512-qujRfC8sFVInYSPPMLQByRh7zhwkGFS4+tyMQ83srV1qrxL4g8E2tyxVVyxd0+8QeBM1mIk9KbWxkegRr76XzA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@standard-schema/spec": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz", + "integrity": "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==", + "license": "MIT" + }, + "node_modules/@standard-schema/utils": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/@standard-schema/utils/-/utils-0.3.0.tgz", + "integrity": "sha512-e7Mew686owMaPJVNNLs55PUvgz371nKgwsc4vxE49zsODpJEnxgxRo2y/OKrqueavXgZNMDVj3DdHFlaSAeU8g==", + "license": "MIT" + }, + "node_modules/@tailwindcss/node": { + "version": "4.2.4", + "resolved": "https://registry.npmjs.org/@tailwindcss/node/-/node-4.2.4.tgz", + "integrity": "sha512-Ai7+yQPxz3ddrDQzFfBKdHEVBg0w3Zl83jnjuwxnZOsnH9pGn93QHQtpU0p/8rYWxvbFZHneni6p1BSLK4DkGA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/remapping": "^2.3.5", + "enhanced-resolve": "^5.19.0", + "jiti": "^2.6.1", + "lightningcss": "1.32.0", + "magic-string": "^0.30.21", + "source-map-js": "^1.2.1", + "tailwindcss": "4.2.4" + } + }, + "node_modules/@tailwindcss/oxide": { + "version": "4.2.4", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide/-/oxide-4.2.4.tgz", + "integrity": "sha512-9El/iI069DKDSXwTvB9J4BwdO5JhRrOweGaK25taBAvBXyXqJAX+Jqdvs8r8gKpsI/1m0LeJLyQYTf/WLrBT1Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 20" + }, + "optionalDependencies": { + "@tailwindcss/oxide-android-arm64": "4.2.4", + "@tailwindcss/oxide-darwin-arm64": "4.2.4", + "@tailwindcss/oxide-darwin-x64": "4.2.4", + "@tailwindcss/oxide-freebsd-x64": "4.2.4", + "@tailwindcss/oxide-linux-arm-gnueabihf": "4.2.4", + "@tailwindcss/oxide-linux-arm64-gnu": "4.2.4", + "@tailwindcss/oxide-linux-arm64-musl": "4.2.4", + "@tailwindcss/oxide-linux-x64-gnu": "4.2.4", + "@tailwindcss/oxide-linux-x64-musl": "4.2.4", + "@tailwindcss/oxide-wasm32-wasi": "4.2.4", + "@tailwindcss/oxide-win32-arm64-msvc": "4.2.4", + "@tailwindcss/oxide-win32-x64-msvc": "4.2.4" + } + }, + "node_modules/@tailwindcss/oxide-android-arm64": { + "version": "4.2.4", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-android-arm64/-/oxide-android-arm64-4.2.4.tgz", + "integrity": "sha512-e7MOr1SAn9U8KlZzPi1ZXGZHeC5anY36qjNwmZv9pOJ8E4Q6jmD1vyEHkQFmNOIN7twGPEMXRHmitN4zCMN03g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-darwin-arm64": { + "version": "4.2.4", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-arm64/-/oxide-darwin-arm64-4.2.4.tgz", + "integrity": "sha512-tSC/Kbqpz/5/o/C2sG7QvOxAKqyd10bq+ypZNf+9Fi2TvbVbv1zNpcEptcsU7DPROaSbVgUXmrzKhurFvo5eDg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-darwin-x64": { + "version": "4.2.4", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-x64/-/oxide-darwin-x64-4.2.4.tgz", + "integrity": "sha512-yPyUXn3yO/ufR6+Kzv0t4fCg2qNr90jxXc5QqBpjlPNd0NqyDXcmQb/6weunH/MEDXW5dhyEi+agTDiqa3WsGg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-freebsd-x64": { + "version": "4.2.4", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-freebsd-x64/-/oxide-freebsd-x64-4.2.4.tgz", + "integrity": "sha512-BoMIB4vMQtZsXdGLVc2z+P9DbETkiopogfWZKbWwM8b/1Vinbs4YcUwo+kM/KeLkX3Ygrf4/PsRndKaYhS8Eiw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-linux-arm-gnueabihf": { + "version": "4.2.4", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm-gnueabihf/-/oxide-linux-arm-gnueabihf-4.2.4.tgz", + "integrity": "sha512-7pIHBLTHYRAlS7V22JNuTh33yLH4VElwKtB3bwchK/UaKUPpQ0lPQiOWcbm4V3WP2I6fNIJ23vABIvoy2izdwA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-linux-arm64-gnu": { + "version": "4.2.4", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-gnu/-/oxide-linux-arm64-gnu-4.2.4.tgz", + "integrity": "sha512-+E4wxJ0ZGOzSH325reXTWB48l42i93kQqMvDyz5gqfRzRZ7faNhnmvlV4EPGJU3QJM/3Ab5jhJ5pCRUsKn6OQw==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-linux-arm64-musl": { + "version": "4.2.4", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-musl/-/oxide-linux-arm64-musl-4.2.4.tgz", + "integrity": "sha512-bBADEGAbo4ASnppIziaQJelekCxdMaxisrk+fB7Thit72IBnALp9K6ffA2G4ruj90G9XRS2VQ6q2bCKbfFV82g==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-linux-x64-gnu": { + "version": "4.2.4", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-gnu/-/oxide-linux-x64-gnu-4.2.4.tgz", + "integrity": "sha512-7Mx25E4WTfnht0TVRTyC00j3i0M+EeFe7wguMDTlX4mRxafznw0CA8WJkFjWYH5BlgELd1kSjuU2JiPnNZbJDA==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-linux-x64-musl": { + "version": "4.2.4", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-musl/-/oxide-linux-x64-musl-4.2.4.tgz", + "integrity": "sha512-2wwJRF7nyhOR0hhHoChc04xngV3iS+akccHTGtz965FwF0up4b2lOdo6kI1EbDaEXKgvcrFBYcYQQ/rrnWFVfA==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-wasm32-wasi": { + "version": "4.2.4", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-wasm32-wasi/-/oxide-wasm32-wasi-4.2.4.tgz", + "integrity": "sha512-FQsqApeor8Fo6gUEklzmaa9994orJZZDBAlQpK2Mq+DslRKFJeD6AjHpBQ0kZFQohVr8o85PPh8eOy86VlSCmw==", + "bundleDependencies": [ + "@napi-rs/wasm-runtime", + "@emnapi/core", + "@emnapi/runtime", + "@tybys/wasm-util", + "@emnapi/wasi-threads", + "tslib" + ], + "cpu": [ + "wasm32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/core": "^1.8.1", + "@emnapi/runtime": "^1.8.1", + "@emnapi/wasi-threads": "^1.1.0", + "@napi-rs/wasm-runtime": "^1.1.1", + "@tybys/wasm-util": "^0.10.1", + "tslib": "^2.8.1" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@tailwindcss/oxide-win32-arm64-msvc": { + "version": "4.2.4", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-arm64-msvc/-/oxide-win32-arm64-msvc-4.2.4.tgz", + "integrity": "sha512-L9BXqxC4ToVgwMFqj3pmZRqyHEztulpUJzCxUtLjobMCzTPsGt1Fa9enKbOpY2iIyVtaHNeNvAK8ERP/64sqGQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-win32-x64-msvc": { + "version": "4.2.4", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-x64-msvc/-/oxide-win32-x64-msvc-4.2.4.tgz", + "integrity": "sha512-ESlKG0EpVJQwRjXDDa9rLvhEAh0mhP1sF7sap9dNZT0yyl9SAG6T7gdP09EH0vIv0UNTlo6jPWyujD6559fZvw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/vite": { + "version": "4.2.4", + "resolved": "https://registry.npmjs.org/@tailwindcss/vite/-/vite-4.2.4.tgz", + "integrity": "sha512-pCvohwOCspk3ZFn6eJzrrX3g4n2JY73H6MmYC87XfGPyTty4YsCjYTMArRZm/zOI8dIt3+EcrLHAFPe5A4bgtw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@tailwindcss/node": "4.2.4", + "@tailwindcss/oxide": "4.2.4", + "tailwindcss": "4.2.4" + }, + "peerDependencies": { + "vite": "^5.2.0 || ^6 || ^7 || ^8" + } + }, + "node_modules/@tanstack/history": { + "version": "1.161.6", + "resolved": "https://registry.npmjs.org/@tanstack/history/-/history-1.161.6.tgz", + "integrity": "sha512-NaOGLRrddszbQj9upGat6HG/4TKvXLvu+osAIgfxPYA+eIvYKv8GKDJOrY2D3/U9MRnKfMWD7bU4jeD4xmqyIg==", + "license": "MIT", + "engines": { + "node": ">=20.19" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + } + }, + "node_modules/@tanstack/query-core": { + "version": "5.100.5", + "resolved": "https://registry.npmjs.org/@tanstack/query-core/-/query-core-5.100.5.tgz", + "integrity": "sha512-t20KrhKkf0HXzqQkPbJ5erhFesup68BAbwFgYmTrS7bxMF7O5MdmL8jUkik4thsG7Hg00fblz30h6yF1d5TxGg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + } + }, + "node_modules/@tanstack/react-query": { + "version": "5.100.5", + "resolved": "https://registry.npmjs.org/@tanstack/react-query/-/react-query-5.100.5.tgz", + "integrity": "sha512-aNwj1mi2v2bQ9IxkyR1grLOUkv3BYWoykHy9KDyLNbjC3tsahbOHJibK+Wjtr1wRhG59/AvJhiJG5OlthaCgJA==", + "license": "MIT", + "dependencies": { + "@tanstack/query-core": "5.100.5" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + }, + "peerDependencies": { + "react": "^18 || ^19" + } + }, + "node_modules/@tanstack/react-router": { + "version": "1.168.24", + "resolved": "https://registry.npmjs.org/@tanstack/react-router/-/react-router-1.168.24.tgz", + "integrity": "sha512-CQWd9ywDZU6icG65SrjJMzWcNg/ehBKFxfxYssKSuELk0eM9SzJxJ3JBI760kdLXIsXewOX9PHpJDknxC/R5Ag==", + "license": "MIT", + "dependencies": { + "@tanstack/history": "1.161.6", + "@tanstack/react-store": "^0.9.3", + "@tanstack/router-core": "1.168.16", + "isbot": "^5.1.22" + }, + "engines": { + "node": ">=20.19" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + }, + "peerDependencies": { + "react": ">=18.0.0 || >=19.0.0", + "react-dom": ">=18.0.0 || >=19.0.0" + } + }, + "node_modules/@tanstack/react-router-devtools": { + "version": "1.166.13", + "resolved": "https://registry.npmjs.org/@tanstack/react-router-devtools/-/react-router-devtools-1.166.13.tgz", + "integrity": "sha512-6yKRFFJrEEOiGp5RAAuGCYsl81M4XAhJmLcu9PKj+HZle4A3dsP60lwHoqQYWHMK9nKKFkdXR+D8qxzxqtQbEA==", + "license": "MIT", + "dependencies": { + "@tanstack/router-devtools-core": "1.167.3" + }, + "engines": { + "node": ">=20.19" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + }, + "peerDependencies": { + "@tanstack/react-router": "^1.168.15", + "@tanstack/router-core": "^1.168.11", + "react": ">=18.0.0 || >=19.0.0", + "react-dom": ">=18.0.0 || >=19.0.0" + }, + "peerDependenciesMeta": { + "@tanstack/router-core": { + "optional": true + } + } + }, + "node_modules/@tanstack/react-store": { + "version": "0.9.3", + "resolved": "https://registry.npmjs.org/@tanstack/react-store/-/react-store-0.9.3.tgz", + "integrity": "sha512-y2iHd/N9OkoQbFJLUX1T9vbc2O9tjH0pQRgTcx1/Nz4IlwLvkgpuglXUx+mXt0g5ZDFrEeDnONPqkbfxXJKwRg==", + "license": "MIT", + "dependencies": { + "@tanstack/store": "0.9.3", + "use-sync-external-store": "^1.6.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", + "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, + "node_modules/@tanstack/router-core": { + "version": "1.168.16", + "resolved": "https://registry.npmjs.org/@tanstack/router-core/-/router-core-1.168.16.tgz", + "integrity": "sha512-2lkWNMzDWWxVqTf9Y54DH1ceZOrGrOGzx9CFVMq8gQSOxhr3x3+otjVei1Rlx4xmsCc4yCqccqjun5wDtE9MZA==", + "license": "MIT", + "dependencies": { + "@tanstack/history": "1.161.6", + "cookie-es": "^3.0.0", + "seroval": "^1.5.0", + "seroval-plugins": "^1.5.0" + }, + "bin": { + "intent": "bin/intent.js" + }, + "engines": { + "node": ">=20.19" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + } + }, + "node_modules/@tanstack/router-devtools": { + "version": "1.166.13", + "resolved": "https://registry.npmjs.org/@tanstack/router-devtools/-/router-devtools-1.166.13.tgz", + "integrity": "sha512-Qs8gkyI7m+eAxG3VcIOHuTSsUfA5ZxZcOa99ZyIIIJFxW6hy1k+m2s1J0ZYN1SNlip8P2ofd/MHiqmR1IWipMg==", + "license": "MIT", + "dependencies": { + "@tanstack/react-router-devtools": "1.166.13", + "clsx": "^2.1.1", + "goober": "^2.1.16" + }, + "engines": { + "node": ">=20.19" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + }, + "peerDependencies": { + "@tanstack/react-router": "^1.168.15", + "csstype": "^3.0.10", + "react": ">=18.0.0 || >=19.0.0", + "react-dom": ">=18.0.0 || >=19.0.0" + }, + "peerDependenciesMeta": { + "csstype": { + "optional": true + } + } + }, + "node_modules/@tanstack/router-devtools-core": { + "version": "1.167.3", + "resolved": "https://registry.npmjs.org/@tanstack/router-devtools-core/-/router-devtools-core-1.167.3.tgz", + "integrity": "sha512-fJ1VMhyQgnoashTrP763c2HRc9kofgF61L7Jb3F6eTHAmCKtGVx8BRtiFt37sr3U0P0jmaaiiSPGP6nT5JtVNg==", + "license": "MIT", + "dependencies": { + "clsx": "^2.1.1", + "goober": "^2.1.16" + }, + "engines": { + "node": ">=20.19" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + }, + "peerDependencies": { + "@tanstack/router-core": "^1.168.11", + "csstype": "^3.0.10" + }, + "peerDependenciesMeta": { + "csstype": { + "optional": true + } + } + }, + "node_modules/@tanstack/router-generator": { + "version": "1.166.35", + "resolved": "https://registry.npmjs.org/@tanstack/router-generator/-/router-generator-1.166.35.tgz", + "integrity": "sha512-d3tdUTf6QRjMW4V2P91mE8OR0haHgGktkLOs3zQmirEokQhu2qgoVfcSxLF6DfmMR6smvDjwdeMdy2sudImuFQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.28.5", + "@tanstack/router-core": "1.168.16", + "@tanstack/router-utils": "1.161.7", + "@tanstack/virtual-file-routes": "1.161.7", + "jiti": "^2.6.1", + "magic-string": "^0.30.21", + "prettier": "^3.5.0", + "zod": "^3.24.2" + }, + "engines": { + "node": ">=20.19" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + } + }, + "node_modules/@tanstack/router-generator/node_modules/zod": { + "version": "3.25.76", + "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz", + "integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + }, + "node_modules/@tanstack/router-plugin": { + "version": "1.167.27", + "resolved": "https://registry.npmjs.org/@tanstack/router-plugin/-/router-plugin-1.167.27.tgz", + "integrity": "sha512-3jl+9I6bgSqAhwIDU2ps4pjhaEy2kugXvZ/dCAE6zF+pYEuRkNr8GA7bpoboZqdAtbceyZ2sLgX6aB7VQvHGUg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.28.5", + "@babel/plugin-syntax-jsx": "^7.27.1", + "@babel/plugin-syntax-typescript": "^7.27.1", + "@babel/template": "^7.27.2", + "@babel/traverse": "^7.28.5", + "@babel/types": "^7.28.5", + "@tanstack/router-core": "1.168.16", + "@tanstack/router-generator": "1.166.35", + "@tanstack/router-utils": "1.161.7", + "@tanstack/virtual-file-routes": "1.161.7", + "chokidar": "^3.6.0", + "unplugin": "^3.0.0", + "zod": "^3.24.2" + }, + "bin": { + "intent": "bin/intent.js" + }, + "engines": { + "node": ">=20.19" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + }, + "peerDependencies": { + "@rsbuild/core": ">=1.0.2 || ^2.0.0", + "@tanstack/react-router": "^1.168.24", + "vite": ">=5.0.0 || >=6.0.0 || >=7.0.0 || >=8.0.0", + "vite-plugin-solid": "^2.11.10 || ^3.0.0-0", + "webpack": ">=5.92.0" + }, + "peerDependenciesMeta": { + "@rsbuild/core": { + "optional": true + }, + "@tanstack/react-router": { + "optional": true + }, + "vite": { + "optional": true + }, + "vite-plugin-solid": { + "optional": true + }, + "webpack": { + "optional": true + } + } + }, + "node_modules/@tanstack/router-plugin/node_modules/zod": { + "version": "3.25.76", + "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz", + "integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + }, + "node_modules/@tanstack/router-utils": { + "version": "1.161.7", + "resolved": "https://registry.npmjs.org/@tanstack/router-utils/-/router-utils-1.161.7.tgz", + "integrity": "sha512-VkY0u7ax/GD0qU6ZLLnfPC+UMxVzxRbvZp4yV4iUSXjgJZ/siAT5/QlLm9FEDJ9QDoC0VD9W7f00tKKreUI7Ng==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.28.5", + "@babel/generator": "^7.28.5", + "@babel/parser": "^7.28.5", + "@babel/types": "^7.28.5", + "ansis": "^4.1.0", + "babel-dead-code-elimination": "^1.0.12", + "diff": "^8.0.2", + "pathe": "^2.0.3", + "tinyglobby": "^0.2.15" + }, + "engines": { + "node": ">=20.19" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + } + }, + "node_modules/@tanstack/store": { + "version": "0.9.3", + "resolved": "https://registry.npmjs.org/@tanstack/store/-/store-0.9.3.tgz", + "integrity": "sha512-8reSzl/qGWGGVKhBoxXPMWzATSbZLZFWhwBAFO9NAyp0TxzfBP0mIrGb8CP8KrQTmvzXlR/vFPPUrHTLBGyFyw==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + } + }, + "node_modules/@tanstack/virtual-file-routes": { + "version": "1.161.7", + "resolved": "https://registry.npmjs.org/@tanstack/virtual-file-routes/-/virtual-file-routes-1.161.7.tgz", + "integrity": "sha512-olW33+Cn+bsCsZKPwEGhlkqS6w3M2slFv11JIobdnCFKMLG97oAI2kWKdx5/zsywTL8flpnoIgaZZPlQTFYhdQ==", + "dev": true, + "license": "MIT", + "bin": { + "intent": "bin/intent.js" + }, + "engines": { + "node": ">=20.19" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + } + }, + "node_modules/@tybys/wasm-util": { + "version": "0.10.1", + "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.1.tgz", + "integrity": "sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@types/d3-array": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/@types/d3-array/-/d3-array-3.2.2.tgz", + "integrity": "sha512-hOLWVbm7uRza0BYXpIIW5pxfrKe0W+D5lrFiAEYR+pb6w3N2SwSMaJbXdUfSEv+dT4MfHBLtn5js0LAWaO6otw==", + "license": "MIT" + }, + "node_modules/@types/d3-color": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/@types/d3-color/-/d3-color-3.1.3.tgz", + "integrity": "sha512-iO90scth9WAbmgv7ogoq57O9YpKmFBbmoEoCHDB2xMBY0+/KVrqAaCDyCE16dUspeOvIxFFRI+0sEtqDqy2b4A==", + "license": "MIT" + }, + "node_modules/@types/d3-ease": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@types/d3-ease/-/d3-ease-3.0.2.tgz", + "integrity": "sha512-NcV1JjO5oDzoK26oMzbILE6HW7uVXOHLQvHshBUW4UMdZGfiY6v5BeQwh9a9tCzv+CeefZQHJt5SRgK154RtiA==", + "license": "MIT" + }, + "node_modules/@types/d3-interpolate": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/d3-interpolate/-/d3-interpolate-3.0.4.tgz", + "integrity": "sha512-mgLPETlrpVV1YRJIglr4Ez47g7Yxjl1lj7YKsiMCb27VJH9W8NVM6Bb9d8kkpG/uAQS5AmbA48q2IAolKKo1MA==", + "license": "MIT", + "dependencies": { + "@types/d3-color": "*" + } + }, + "node_modules/@types/d3-path": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/@types/d3-path/-/d3-path-3.1.1.tgz", + "integrity": "sha512-VMZBYyQvbGmWyWVea0EHs/BwLgxc+MKi1zLDCONksozI4YJMcTt8ZEuIR4Sb1MMTE8MMW49v0IwI5+b7RmfWlg==", + "license": "MIT" + }, + "node_modules/@types/d3-scale": { + "version": "4.0.9", + "resolved": "https://registry.npmjs.org/@types/d3-scale/-/d3-scale-4.0.9.tgz", + "integrity": "sha512-dLmtwB8zkAeO/juAMfnV+sItKjlsw2lKdZVVy6LRr0cBmegxSABiLEpGVmSJJ8O08i4+sGR6qQtb6WtuwJdvVw==", + "license": "MIT", + "dependencies": { + "@types/d3-time": "*" + } + }, + "node_modules/@types/d3-shape": { + "version": "3.1.8", + "resolved": "https://registry.npmjs.org/@types/d3-shape/-/d3-shape-3.1.8.tgz", + "integrity": "sha512-lae0iWfcDeR7qt7rA88BNiqdvPS5pFVPpo5OfjElwNaT2yyekbM0C9vK+yqBqEmHr6lDkRnYNoTBYlAgJa7a4w==", + "license": "MIT", + "dependencies": { + "@types/d3-path": "*" + } + }, + "node_modules/@types/d3-time": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/d3-time/-/d3-time-3.0.4.tgz", + "integrity": "sha512-yuzZug1nkAAaBlBBikKZTgzCeA+k1uy4ZFwWANOfKw5z5LRhV0gNA7gNkKm7HoK+HRN0wX3EkxGk0fpbWhmB7g==", + "license": "MIT" + }, + "node_modules/@types/d3-timer": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@types/d3-timer/-/d3-timer-3.0.2.tgz", + "integrity": "sha512-Ps3T8E8dZDam6fUyNiMkekK3XUsaUEik+idO9/YjPtfj2qruF8tFBXS7XhtE4iIXBLxhmLjP3SXpLhVf21I9Lw==", + "license": "MIT" + }, + "node_modules/@types/esrecurse": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/@types/esrecurse/-/esrecurse-4.3.1.tgz", + "integrity": "sha512-xJBAbDifo5hpffDBuHl0Y8ywswbiAp/Wi7Y/GtAgSlZyIABppyurxVueOPE8LUQOxdlgi6Zqce7uoEpqNTeiUw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/estree": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", + "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/json-schema": { + "version": "7.0.15", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", + "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "24.12.2", + "resolved": "https://registry.npmjs.org/@types/node/-/node-24.12.2.tgz", + "integrity": "sha512-A1sre26ke7HDIuY/M23nd9gfB+nrmhtYyMINbjI1zHJxYteKR6qSMX56FsmjMcDb3SMcjJg5BiRRgOCC/yBD0g==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~7.16.0" + } + }, + "node_modules/@types/react": { + "version": "19.2.14", + "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.14.tgz", + "integrity": "sha512-ilcTH/UniCkMdtexkoCN0bI7pMcJDvmQFPvuPvmEaYA/NSfFTAgdUSLAoVjaRJm7+6PvcM+q1zYOwS4wTYMF9w==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "csstype": "^3.2.2" + } + }, + "node_modules/@types/react-dom": { + "version": "19.2.3", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.2.3.tgz", + "integrity": "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==", + "devOptional": true, + "license": "MIT", + "peerDependencies": { + "@types/react": "^19.2.0" + } + }, + "node_modules/@types/use-sync-external-store": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/@types/use-sync-external-store/-/use-sync-external-store-0.0.6.tgz", + "integrity": "sha512-zFDAD+tlpf2r4asuHEj0XH6pY6i0g5NeAHPn+15wk3BV6JA69eERFXC1gyGThDkVa1zCyKr5jox1+2LbV/AMLg==", + "license": "MIT" + }, + "node_modules/@typescript-eslint/eslint-plugin": { + "version": "8.59.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.59.0.tgz", + "integrity": "sha512-HyAZtpdkgZwpq8Sz3FSUvCR4c+ScbuWa9AksK2Jweub7w4M3yTz4O11AqVJzLYjy/B9ZWPyc81I+mOdJU/bDQw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/regexpp": "^4.12.2", + "@typescript-eslint/scope-manager": "8.59.0", + "@typescript-eslint/type-utils": "8.59.0", + "@typescript-eslint/utils": "8.59.0", + "@typescript-eslint/visitor-keys": "8.59.0", + "ignore": "^7.0.5", + "natural-compare": "^1.4.0", + "ts-api-utils": "^2.5.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "@typescript-eslint/parser": "^8.59.0", + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/eslint-plugin/node_modules/ignore": { + "version": "7.0.5", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.5.tgz", + "integrity": "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/@typescript-eslint/parser": { + "version": "8.59.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.59.0.tgz", + "integrity": "sha512-TI1XGwKbDpo9tRW8UDIXCOeLk55qe9ZFGs8MTKU6/M08HWTw52DD/IYhfQtOEhEdPhLMT26Ka/x7p70nd3dzDg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/scope-manager": "8.59.0", + "@typescript-eslint/types": "8.59.0", + "@typescript-eslint/typescript-estree": "8.59.0", + "@typescript-eslint/visitor-keys": "8.59.0", + "debug": "^4.4.3" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/project-service": { + "version": "8.59.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.59.0.tgz", + "integrity": "sha512-Lw5ITrR5s5TbC19YSvlr63ZfLaJoU6vtKTHyB0GQOpX0W7d5/Ir6vUahWi/8Sps/nOukZQ0IB3SmlxZnjaKVnw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/tsconfig-utils": "^8.59.0", + "@typescript-eslint/types": "^8.59.0", + "debug": "^4.4.3" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/scope-manager": { + "version": "8.59.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.59.0.tgz", + "integrity": "sha512-UzR16Ut8IpA3Mc4DbgAShlPPkVm8xXMWafXxB0BocaVRHs8ZGakAxGRskF7FId3sdk9lgGD73GSFaWmWFDE4dg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.59.0", + "@typescript-eslint/visitor-keys": "8.59.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/tsconfig-utils": { + "version": "8.59.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.59.0.tgz", + "integrity": "sha512-91Sbl3s4Kb3SybliIY6muFBmHVv+pYXfybC4Oolp3dvk8BvIE3wOPc+403CWIT7mJNkfQRGtdqghzs2+Z91Tqg==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/type-utils": { + "version": "8.59.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.59.0.tgz", + "integrity": "sha512-3TRiZaQSltGqGeNrJzzr1+8YcEobKH9rHnqIp/1psfKFmhRQDNMGP5hBufanYTGznwShzVLs3Mz+gDN7HkWfXg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.59.0", + "@typescript-eslint/typescript-estree": "8.59.0", + "@typescript-eslint/utils": "8.59.0", + "debug": "^4.4.3", + "ts-api-utils": "^2.5.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/types": { + "version": "8.59.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.59.0.tgz", + "integrity": "sha512-nLzdsT1gdOgFxxxwrlNVUBzSNBEEHJ86bblmk4QAS6stfig7rcJzWKqCyxFy3YRRHXDWEkb2NralA1nOYkkm/A==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/typescript-estree": { + "version": "8.59.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.59.0.tgz", + "integrity": "sha512-O9Re9P1BmBLFJyikRbQpLku/QA3/AueZNO9WePLBwQrvkixTmDe8u76B6CYUAITRl/rHawggEqUGn5QIkVRLMw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/project-service": "8.59.0", + "@typescript-eslint/tsconfig-utils": "8.59.0", + "@typescript-eslint/types": "8.59.0", + "@typescript-eslint/visitor-keys": "8.59.0", + "debug": "^4.4.3", + "minimatch": "^10.2.2", + "semver": "^7.7.3", + "tinyglobby": "^0.2.15", + "ts-api-utils": "^2.5.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/semver": { + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", + "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@typescript-eslint/utils": { + "version": "8.59.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.59.0.tgz", + "integrity": "sha512-I1R/K7V07XsMJ12Oaxg/O9GfrysGTmCRhvZJBv0RE0NcULMzjqVpR5kRRQjHsz3J/bElU7HwCO7zkqL+MSUz+g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.9.1", + "@typescript-eslint/scope-manager": "8.59.0", + "@typescript-eslint/types": "8.59.0", + "@typescript-eslint/typescript-estree": "8.59.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/visitor-keys": { + "version": "8.59.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.59.0.tgz", + "integrity": "sha512-/uejZt4dSere1bx12WLlPfv8GktzcaDtuJ7s42/HEZ5zGj9oxRaD4bj7qwSunXkf+pbAhFt2zjpHYUiT5lHf0Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.59.0", + "eslint-visitor-keys": "^5.0.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@vitejs/plugin-react": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-6.0.1.tgz", + "integrity": "sha512-l9X/E3cDb+xY3SWzlG1MOGt2usfEHGMNIaegaUGFsLkb3RCn/k8/TOXBcab+OndDI4TBtktT8/9BwwW8Vi9KUQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@rolldown/pluginutils": "1.0.0-rc.7" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "peerDependencies": { + "@rolldown/plugin-babel": "^0.1.7 || ^0.2.0", + "babel-plugin-react-compiler": "^1.0.0", + "vite": "^8.0.0" + }, + "peerDependenciesMeta": { + "@rolldown/plugin-babel": { + "optional": true + }, + "babel-plugin-react-compiler": { + "optional": true + } + } + }, + "node_modules/acorn": { + "version": "8.16.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.16.0.tgz", + "integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==", + "dev": true, + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-jsx": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", + "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/ajv": { + "version": "6.15.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.15.0.tgz", + "integrity": "sha512-fgFx7Hfoq60ytK2c7DhnF8jIvzYgOMxfugjLOSMHjLIPgenqa7S7oaagATUq99mV6IYvN2tRmC0wnTYX6iPbMw==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ansis": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/ansis/-/ansis-4.2.0.tgz", + "integrity": "sha512-HqZ5rWlFjGiV0tDm3UxxgNRqsOTniqoKZu0pIAfh7TZQMGuZK+hH0drySty0si0QXj1ieop4+SkSfPZBPPkHig==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=14" + } + }, + "node_modules/anymatch": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", + "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", + "dev": true, + "license": "ISC", + "dependencies": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/anymatch/node_modules/picomatch": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz", + "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/aria-hidden": { + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/aria-hidden/-/aria-hidden-1.2.6.tgz", + "integrity": "sha512-ik3ZgC9dY/lYVVM++OISsaYDeg1tb0VtP5uL3ouh1koGOaUMDPpbFIei4JkFimWUFPn90sbMNMXQAIVOlnYKJA==", + "license": "MIT", + "dependencies": { + "tslib": "^2.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/babel-dead-code-elimination": { + "version": "1.0.12", + "resolved": "https://registry.npmjs.org/babel-dead-code-elimination/-/babel-dead-code-elimination-1.0.12.tgz", + "integrity": "sha512-GERT7L2TiYcYDtYk1IpD+ASAYXjKbLTDPhBtYj7X1NuRMDTMtAx9kyBenub1Ev41lo91OHCKdmP+egTDmfQ7Ig==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.23.7", + "@babel/parser": "^7.23.6", + "@babel/traverse": "^7.23.7", + "@babel/types": "^7.23.6" + } + }, + "node_modules/balanced-match": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", + "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/baseline-browser-mapping": { + "version": "2.10.22", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.22.tgz", + "integrity": "sha512-6qruVrb5rse6WylFkU0FhBKKGuecWseqdpQfhkawn6ztyk2QlfwSRjsDxMCLJrkfmfN21qvhl9ABgaMeRkuwww==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "baseline-browser-mapping": "dist/cli.cjs" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/binary-extensions": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", + "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/brace-expansion": { + "version": "5.0.5", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.5.tgz", + "integrity": "sha512-VZznLgtwhn+Mact9tfiwx64fA9erHH/MCXEUfB/0bX/6Fz6ny5EGTXYltMocqg4xFAQZtnO3DHWWXi8RiuN7cQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^4.0.2" + }, + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/braces": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "dev": true, + "license": "MIT", + "dependencies": { + "fill-range": "^7.1.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/browserslist": { + "version": "4.28.2", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.2.tgz", + "integrity": "sha512-48xSriZYYg+8qXna9kwqjIVzuQxi+KYWp2+5nCYnYKPTr0LvD89Jqk2Or5ogxz0NUMfIjhh2lIUX/LyX9B4oIg==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "baseline-browser-mapping": "^2.10.12", + "caniuse-lite": "^1.0.30001782", + "electron-to-chromium": "^1.5.328", + "node-releases": "^2.0.36", + "update-browserslist-db": "^1.2.3" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001790", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001790.tgz", + "integrity": "sha512-bOoxfJPyYo+ds6W0YfptaCWbFnJYjh2Y1Eow5lRv+vI2u8ganPZqNm1JwNh0t2ELQCqIWg4B3dWEusgAmsoyOw==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "CC-BY-4.0" + }, + "node_modules/chokidar": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", + "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", + "dev": true, + "license": "MIT", + "dependencies": { + "anymatch": "~3.1.2", + "braces": "~3.0.2", + "glob-parent": "~5.1.2", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.6.0" + }, + "engines": { + "node": ">= 8.10.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + } + }, + "node_modules/chokidar/node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/class-variance-authority": { + "version": "0.7.1", + "resolved": "https://registry.npmjs.org/class-variance-authority/-/class-variance-authority-0.7.1.tgz", + "integrity": "sha512-Ka+9Trutv7G8M6WT6SeiRWz792K5qEqIGEGzXKhAE6xOWAY6pPH8U+9IY3oCMv6kqTmLsv7Xh/2w2RigkePMsg==", + "license": "Apache-2.0", + "dependencies": { + "clsx": "^2.1.1" + }, + "funding": { + "url": "https://polar.sh/cva" + } + }, + "node_modules/clsx": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz", + "integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true, + "license": "MIT" + }, + "node_modules/cookie-es": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/cookie-es/-/cookie-es-3.1.1.tgz", + "integrity": "sha512-UaXxwISYJPTr9hwQxMFYZ7kNhSXboMXP+Z3TRX6f1/NyaGPfuNUZOWP1pUEb75B2HjfklIYLVRfWiFZJyC6Npg==", + "license": "MIT" + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/csstype": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", + "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", + "license": "MIT" + }, + "node_modules/d3-array": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/d3-array/-/d3-array-3.2.4.tgz", + "integrity": "sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg==", + "license": "ISC", + "dependencies": { + "internmap": "1 - 2" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-color": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-color/-/d3-color-3.1.0.tgz", + "integrity": "sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-ease": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-ease/-/d3-ease-3.0.1.tgz", + "integrity": "sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-format": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/d3-format/-/d3-format-3.1.2.tgz", + "integrity": "sha512-AJDdYOdnyRDV5b6ArilzCPPwc1ejkHcoyFarqlPqT7zRYjhavcT3uSrqcMvsgh2CgoPbK3RCwyHaVyxYcP2Arg==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-interpolate": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-interpolate/-/d3-interpolate-3.0.1.tgz", + "integrity": "sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g==", + "license": "ISC", + "dependencies": { + "d3-color": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-path": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-path/-/d3-path-3.1.0.tgz", + "integrity": "sha512-p3KP5HCf/bvjBSSKuXid6Zqijx7wIfNW+J/maPs+iwR35at5JCbLUT0LzF1cnjbCHWhqzQTIN2Jpe8pRebIEFQ==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-scale": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/d3-scale/-/d3-scale-4.0.2.tgz", + "integrity": "sha512-GZW464g1SH7ag3Y7hXjf8RoUuAFIqklOAq3MRl4OaWabTFJY9PN/E1YklhXLh+OQ3fM9yS2nOkCoS+WLZ6kvxQ==", + "license": "ISC", + "dependencies": { + "d3-array": "2.10.0 - 3", + "d3-format": "1 - 3", + "d3-interpolate": "1.2.0 - 3", + "d3-time": "2.1.1 - 3", + "d3-time-format": "2 - 4" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-shape": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/d3-shape/-/d3-shape-3.2.0.tgz", + "integrity": "sha512-SaLBuwGm3MOViRq2ABk3eLoxwZELpH6zhl3FbAoJ7Vm1gofKx6El1Ib5z23NUEhF9AsGl7y+dzLe5Cw2AArGTA==", + "license": "ISC", + "dependencies": { + "d3-path": "^3.1.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-time": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-time/-/d3-time-3.1.0.tgz", + "integrity": "sha512-VqKjzBLejbSMT4IgbmVgDjpkYrNWUYJnbCGo874u7MMKIWsILRX+OpX/gTk8MqjpT1A/c6HY2dCA77ZN0lkQ2Q==", + "license": "ISC", + "dependencies": { + "d3-array": "2 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-time-format": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/d3-time-format/-/d3-time-format-4.1.0.tgz", + "integrity": "sha512-dJxPBlzC7NugB2PDLwo9Q8JiTR3M3e4/XANkreKSUxF8vvXKqm1Yfq4Q5dl8budlunRVlUUaDUgFt7eA8D6NLg==", + "license": "ISC", + "dependencies": { + "d3-time": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-timer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-timer/-/d3-timer-3.0.1.tgz", + "integrity": "sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/decimal.js-light": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/decimal.js-light/-/decimal.js-light-2.5.1.tgz", + "integrity": "sha512-qIMFpTMZmny+MMIitAB6D7iVPEorVw6YQRWkvarTkT4tBeSLLiHzcwj6q0MmYSFCiVpiqPJTJEYIrpcPzVEIvg==", + "license": "MIT" + }, + "node_modules/deep-is": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", + "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/detect-libc": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=8" + } + }, + "node_modules/detect-node-es": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/detect-node-es/-/detect-node-es-1.1.0.tgz", + "integrity": "sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ==", + "license": "MIT" + }, + "node_modules/diff": { + "version": "8.0.4", + "resolved": "https://registry.npmjs.org/diff/-/diff-8.0.4.tgz", + "integrity": "sha512-DPi0FmjiSU5EvQV0++GFDOJ9ASQUVFh5kD+OzOnYdi7n3Wpm9hWWGfB/O2blfHcMVTL5WkQXSnRiK9makhrcnw==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.3.1" + } + }, + "node_modules/electron-to-chromium": { + "version": "1.5.344", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.344.tgz", + "integrity": "sha512-4MxfbmNDm+KPh066EZy+eUnkcDPcZ35wNmOWzFuh/ijvHsve6kbLTLURy88uCNK5FbpN+yk2nQY6BYh1GEt+wg==", + "dev": true, + "license": "ISC" + }, + "node_modules/enhanced-resolve": { + "version": "5.21.0", + "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.21.0.tgz", + "integrity": "sha512-otxSQPw4lkOZWkHpB3zaEQs6gWYEsmX4xQF68ElXC/TWvGxGMSGOvoNbaLXm6/cS/fSfHtsEdw90y20PCd+sCA==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.4", + "tapable": "^2.3.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/es-toolkit": { + "version": "1.46.0", + "resolved": "https://registry.npmjs.org/es-toolkit/-/es-toolkit-1.46.0.tgz", + "integrity": "sha512-IToJ6ct9OLl5zz6WsC/1vZEwfSZ7Myil+ygl5Tf30Xjn9AEkzNB4kqp2G7VUJKF1DtTx/ra5M5KLlXvzOg51BA==", + "license": "MIT", + "workspaces": [ + "docs", + "benchmarks" + ] + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint": { + "version": "10.2.1", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-10.2.1.tgz", + "integrity": "sha512-wiyGaKsDgqXvF40P8mDwiUp/KQjE1FdrIEJsM8PZ3XCiniTMXS3OHWWUe5FI5agoCnr8x4xPrTDZuxsBlNHl+Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.8.0", + "@eslint-community/regexpp": "^4.12.2", + "@eslint/config-array": "^0.23.5", + "@eslint/config-helpers": "^0.5.5", + "@eslint/core": "^1.2.1", + "@eslint/plugin-kit": "^0.7.1", + "@humanfs/node": "^0.16.6", + "@humanwhocodes/module-importer": "^1.0.1", + "@humanwhocodes/retry": "^0.4.2", + "@types/estree": "^1.0.6", + "ajv": "^6.14.0", + "cross-spawn": "^7.0.6", + "debug": "^4.3.2", + "escape-string-regexp": "^4.0.0", + "eslint-scope": "^9.1.2", + "eslint-visitor-keys": "^5.0.1", + "espree": "^11.2.0", + "esquery": "^1.7.0", + "esutils": "^2.0.2", + "fast-deep-equal": "^3.1.3", + "file-entry-cache": "^8.0.0", + "find-up": "^5.0.0", + "glob-parent": "^6.0.2", + "ignore": "^5.2.0", + "imurmurhash": "^0.1.4", + "is-glob": "^4.0.0", + "json-stable-stringify-without-jsonify": "^1.0.1", + "minimatch": "^10.2.4", + "natural-compare": "^1.4.0", + "optionator": "^0.9.3" + }, + "bin": { + "eslint": "bin/eslint.js" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://eslint.org/donate" + }, + "peerDependencies": { + "jiti": "*" + }, + "peerDependenciesMeta": { + "jiti": { + "optional": true + } + } + }, + "node_modules/eslint-plugin-react-hooks": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-7.1.1.tgz", + "integrity": "sha512-f2I7Gw6JbvCexzIInuSbZpfdQ44D7iqdWX01FKLvrPgqxoE7oMj8clOfto8U6vYiz4yd5oKu39rRSVOe1zRu0g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.24.4", + "@babel/parser": "^7.24.4", + "hermes-parser": "^0.25.1", + "zod": "^3.25.0 || ^4.0.0", + "zod-validation-error": "^3.5.0 || ^4.0.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "eslint": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 || ^9.0.0 || ^10.0.0" + } + }, + "node_modules/eslint-plugin-react-refresh": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/eslint-plugin-react-refresh/-/eslint-plugin-react-refresh-0.5.2.tgz", + "integrity": "sha512-hmgTH57GfzoTFjVN0yBwTggnsVUF2tcqi7RJZHqi9lIezSs4eFyAMktA68YD4r5kNw1mxyY4dmkyoFDb3FIqrA==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "eslint": "^9 || ^10" + } + }, + "node_modules/eslint-scope": { + "version": "9.1.2", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-9.1.2.tgz", + "integrity": "sha512-xS90H51cKw0jltxmvmHy2Iai1LIqrfbw57b79w/J7MfvDfkIkFZ+kj6zC3BjtUwh150HsSSdxXZcsuv72miDFQ==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "@types/esrecurse": "^4.3.1", + "@types/estree": "^1.0.8", + "esrecurse": "^4.3.0", + "estraverse": "^5.2.0" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-visitor-keys": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-5.0.1.tgz", + "integrity": "sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/espree": { + "version": "11.2.0", + "resolved": "https://registry.npmjs.org/espree/-/espree-11.2.0.tgz", + "integrity": "sha512-7p3DrVEIopW1B1avAGLuCSh1jubc01H2JHc8B4qqGblmg5gI9yumBgACjWo4JlIc04ufug4xJ3SQI8HkS/Rgzw==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "acorn": "^8.16.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^5.0.1" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/esquery": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.7.0.tgz", + "integrity": "sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "estraverse": "^5.1.0" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/esrecurse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", + "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "estraverse": "^5.2.0" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/eventemitter3": { + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-5.0.4.tgz", + "integrity": "sha512-mlsTRyGaPBjPedk6Bvw+aqbsXDtoAyAzm5MO7JgU+yVRyMQ5O8bD4Kcci7BS85f93veegeCPkL8R4GLClnjLFw==", + "license": "MIT" + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-levenshtein": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/file-entry-cache": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz", + "integrity": "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "flat-cache": "^4.0.0" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/fill-range": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "dev": true, + "license": "MIT", + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/find-up": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", + "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", + "dev": true, + "license": "MIT", + "dependencies": { + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/flat-cache": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-4.0.1.tgz", + "integrity": "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==", + "dev": true, + "license": "MIT", + "dependencies": { + "flatted": "^3.2.9", + "keyv": "^4.5.4" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/flatted": { + "version": "3.4.2", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.4.2.tgz", + "integrity": "sha512-PjDse7RzhcPkIJwy5t7KPWQSZ9cAbzQXcafsetQoD7sOJRQlGikNbx7yZp2OotDnJyrDcbyRq3Ttb18iYOqkxA==", + "dev": true, + "license": "ISC" + }, + "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/gensync": { + "version": "1.0.0-beta.2", + "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/get-nonce": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-nonce/-/get-nonce-1.0.1.tgz", + "integrity": "sha512-FJhYRoDaiatfEkUK8HKlicmu/3SGFD51q3itKDGoSTysQJBnfOcxU5GxnhE1E6soB76MbT0MBtnKJuXyAx+96Q==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/globals": { + "version": "17.5.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-17.5.0.tgz", + "integrity": "sha512-qoV+HK2yFl/366t2/Cb3+xxPUo5BuMynomoDmiaZBIdbs+0pYbjfZU+twLhGKp4uCZ/+NbtpVepH5bGCxRyy2g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/goober": { + "version": "2.1.18", + "resolved": "https://registry.npmjs.org/goober/-/goober-2.1.18.tgz", + "integrity": "sha512-2vFqsaDVIT9Gz7N6kAL++pLpp41l3PfDuusHcjnGLfR6+huZkl6ziX+zgVC3ZxpqWhzH6pyDdGrCeDhMIvwaxw==", + "license": "MIT", + "peerDependencies": { + "csstype": "^3.0.10" + } + }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/hermes-estree": { + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/hermes-estree/-/hermes-estree-0.25.1.tgz", + "integrity": "sha512-0wUoCcLp+5Ev5pDW2OriHC2MJCbwLwuRx+gAqMTOkGKJJiBCLjtrvy4PWUGn6MIVefecRpzoOZ/UV6iGdOr+Cw==", + "dev": true, + "license": "MIT" + }, + "node_modules/hermes-parser": { + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/hermes-parser/-/hermes-parser-0.25.1.tgz", + "integrity": "sha512-6pEjquH3rqaI6cYAXYPcz9MS4rY6R4ngRgrgfDshRptUZIc3lw0MCIJIGDj9++mfySOuPTHB4nrSW99BCvOPIA==", + "dev": true, + "license": "MIT", + "dependencies": { + "hermes-estree": "0.25.1" + } + }, + "node_modules/ignore": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", + "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/immer": { + "version": "10.2.0", + "resolved": "https://registry.npmjs.org/immer/-/immer-10.2.0.tgz", + "integrity": "sha512-d/+XTN3zfODyjr89gM3mPq1WNX2B8pYsu7eORitdwyA2sBubnTl3laYlBk4sXY5FUa5qTZGBDPJICVbvqzjlbw==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/immer" + } + }, + "node_modules/imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8.19" + } + }, + "node_modules/internmap": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/internmap/-/internmap-2.0.3.tgz", + "integrity": "sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/is-binary-path": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", + "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", + "dev": true, + "license": "MIT", + "dependencies": { + "binary-extensions": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/isbot": { + "version": "5.1.39", + "resolved": "https://registry.npmjs.org/isbot/-/isbot-5.1.39.tgz", + "integrity": "sha512-obH0yYahGXdzNxo+djmHhBYThUKDkz565cxkIlt2L9hXfv1NlaLKoDBHo6KxXsYrIXx2RK3x5vY36CfZcobxEw==", + "license": "Unlicense", + "engines": { + "node": ">=18" + } + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true, + "license": "ISC" + }, + "node_modules/jiti": { + "version": "2.6.1", + "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.6.1.tgz", + "integrity": "sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ==", + "dev": true, + "license": "MIT", + "bin": { + "jiti": "lib/jiti-cli.mjs" + } + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/jsesc": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", + "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", + "dev": true, + "license": "MIT", + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/json-buffer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", + "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-stable-stringify-without-jsonify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", + "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "dev": true, + "license": "MIT", + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/keyv": { + "version": "4.5.4", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", + "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", + "dev": true, + "license": "MIT", + "dependencies": { + "json-buffer": "3.0.1" + } + }, + "node_modules/levn": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", + "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1", + "type-check": "~0.4.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/lightningcss": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.32.0.tgz", + "integrity": "sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ==", + "dev": true, + "license": "MPL-2.0", + "dependencies": { + "detect-libc": "^2.0.3" + }, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + }, + "optionalDependencies": { + "lightningcss-android-arm64": "1.32.0", + "lightningcss-darwin-arm64": "1.32.0", + "lightningcss-darwin-x64": "1.32.0", + "lightningcss-freebsd-x64": "1.32.0", + "lightningcss-linux-arm-gnueabihf": "1.32.0", + "lightningcss-linux-arm64-gnu": "1.32.0", + "lightningcss-linux-arm64-musl": "1.32.0", + "lightningcss-linux-x64-gnu": "1.32.0", + "lightningcss-linux-x64-musl": "1.32.0", + "lightningcss-win32-arm64-msvc": "1.32.0", + "lightningcss-win32-x64-msvc": "1.32.0" + } + }, + "node_modules/lightningcss-android-arm64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.32.0.tgz", + "integrity": "sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-arm64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.32.0.tgz", + "integrity": "sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-x64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.32.0.tgz", + "integrity": "sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-freebsd-x64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.32.0.tgz", + "integrity": "sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm-gnueabihf": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.32.0.tgz", + "integrity": "sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-gnu": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.32.0.tgz", + "integrity": "sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-musl": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.32.0.tgz", + "integrity": "sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-gnu": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.32.0.tgz", + "integrity": "sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-musl": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.32.0.tgz", + "integrity": "sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-arm64-msvc": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.32.0.tgz", + "integrity": "sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-x64-msvc": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.32.0.tgz", + "integrity": "sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/locate-path": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", + "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-locate": "^5.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/lru-cache": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^3.0.2" + } + }, + "node_modules/lucide-react": { + "version": "1.11.0", + "resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-1.11.0.tgz", + "integrity": "sha512-UOhjdztXCgdBReRcIhsvz2siIBogfv/lhJEIViCpLt924dO+GDms9T7DNoucI23s6kEPpe988m5N0D2ajnzb2g==", + "license": "ISC", + "peerDependencies": { + "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, + "node_modules/magic-string": { + "version": "0.30.21", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", + "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.5" + } + }, + "node_modules/minimatch": { + "version": "10.2.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.5.tgz", + "integrity": "sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "brace-expansion": "^5.0.5" + }, + "engines": { + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "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==", + "dev": true, + "license": "MIT" + }, + "node_modules/nanoid": { + "version": "3.3.11", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", + "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/natural-compare": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", + "dev": true, + "license": "MIT" + }, + "node_modules/node-releases": { + "version": "2.0.38", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.38.tgz", + "integrity": "sha512-3qT/88Y3FbH/Kx4szpQQ4HzUbVrHPKTLVpVocKiLfoYvw9XSGOX2FmD2d6DrXbVYyAQTF2HeF6My8jmzx7/CRw==", + "dev": true, + "license": "MIT" + }, + "node_modules/normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/optionator": { + "version": "0.9.4", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", + "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==", + "dev": true, + "license": "MIT", + "dependencies": { + "deep-is": "^0.1.3", + "fast-levenshtein": "^2.0.6", + "levn": "^0.4.1", + "prelude-ls": "^1.2.1", + "type-check": "^0.4.0", + "word-wrap": "^1.2.5" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", + "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-limit": "^3.0.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/pathe": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", + "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", + "dev": true, + "license": "MIT" + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", + "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/postcss": { + "version": "8.5.10", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.10.tgz", + "integrity": "sha512-pMMHxBOZKFU6HgAZ4eyGnwXF/EvPGGqUr0MnZ5+99485wwW41kW91A4LOGxSHhgugZmSChL5AlElNdwlNgcnLQ==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.11", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/prelude-ls": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", + "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/prettier": { + "version": "3.8.3", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.8.3.tgz", + "integrity": "sha512-7igPTM53cGHMW8xWuVTydi2KO233VFiTNyF5hLJqpilHfmn8C8gPf+PS7dUT64YcXFbiMGZxS9pCSxL/Dxm/Jw==", + "dev": true, + "license": "MIT", + "bin": { + "prettier": "bin/prettier.cjs" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/prettier/prettier?sponsor=1" + } + }, + "node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/radix-ui": { + "version": "1.4.3", + "resolved": "https://registry.npmjs.org/radix-ui/-/radix-ui-1.4.3.tgz", + "integrity": "sha512-aWizCQiyeAenIdUbqEpXgRA1ya65P13NKn/W8rWkcN0OPkRDxdBVLWnIEDsS2RpwCK2nobI7oMUSmexzTDyAmA==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-accessible-icon": "1.1.7", + "@radix-ui/react-accordion": "1.2.12", + "@radix-ui/react-alert-dialog": "1.1.15", + "@radix-ui/react-arrow": "1.1.7", + "@radix-ui/react-aspect-ratio": "1.1.7", + "@radix-ui/react-avatar": "1.1.10", + "@radix-ui/react-checkbox": "1.3.3", + "@radix-ui/react-collapsible": "1.1.12", + "@radix-ui/react-collection": "1.1.7", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-context-menu": "2.2.16", + "@radix-ui/react-dialog": "1.1.15", + "@radix-ui/react-direction": "1.1.1", + "@radix-ui/react-dismissable-layer": "1.1.11", + "@radix-ui/react-dropdown-menu": "2.1.16", + "@radix-ui/react-focus-guards": "1.1.3", + "@radix-ui/react-focus-scope": "1.1.7", + "@radix-ui/react-form": "0.1.8", + "@radix-ui/react-hover-card": "1.1.15", + "@radix-ui/react-label": "2.1.7", + "@radix-ui/react-menu": "2.1.16", + "@radix-ui/react-menubar": "1.1.16", + "@radix-ui/react-navigation-menu": "1.2.14", + "@radix-ui/react-one-time-password-field": "0.1.8", + "@radix-ui/react-password-toggle-field": "0.1.3", + "@radix-ui/react-popover": "1.1.15", + "@radix-ui/react-popper": "1.2.8", + "@radix-ui/react-portal": "1.1.9", + "@radix-ui/react-presence": "1.1.5", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-progress": "1.1.7", + "@radix-ui/react-radio-group": "1.3.8", + "@radix-ui/react-roving-focus": "1.1.11", + "@radix-ui/react-scroll-area": "1.2.10", + "@radix-ui/react-select": "2.2.6", + "@radix-ui/react-separator": "1.1.7", + "@radix-ui/react-slider": "1.3.6", + "@radix-ui/react-slot": "1.2.3", + "@radix-ui/react-switch": "1.2.6", + "@radix-ui/react-tabs": "1.1.13", + "@radix-ui/react-toast": "1.2.15", + "@radix-ui/react-toggle": "1.1.10", + "@radix-ui/react-toggle-group": "1.1.11", + "@radix-ui/react-toolbar": "1.1.11", + "@radix-ui/react-tooltip": "1.2.8", + "@radix-ui/react-use-callback-ref": "1.1.1", + "@radix-ui/react-use-controllable-state": "1.2.2", + "@radix-ui/react-use-effect-event": "0.0.2", + "@radix-ui/react-use-escape-keydown": "1.1.1", + "@radix-ui/react-use-is-hydrated": "0.1.0", + "@radix-ui/react-use-layout-effect": "1.1.1", + "@radix-ui/react-use-size": "1.1.1", + "@radix-ui/react-visually-hidden": "1.2.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/react": { + "version": "19.2.5", + "resolved": "https://registry.npmjs.org/react/-/react-19.2.5.tgz", + "integrity": "sha512-llUJLzz1zTUBrskt2pwZgLq59AemifIftw4aB7JxOqf1HY2FDaGDxgwpAPVzHU1kdWabH7FauP4i1oEeer2WCA==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-dom": { + "version": "19.2.5", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.5.tgz", + "integrity": "sha512-J5bAZz+DXMMwW/wV3xzKke59Af6CHY7G4uYLN1OvBcKEsWOs4pQExj86BBKamxl/Ik5bx9whOrvBlSDfWzgSag==", + "license": "MIT", + "dependencies": { + "scheduler": "^0.27.0" + }, + "peerDependencies": { + "react": "^19.2.5" + } + }, + "node_modules/react-is": { + "version": "19.2.5", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-19.2.5.tgz", + "integrity": "sha512-Dn0t8IQhCmeIT3wu+Apm1/YVsJXsGWi6k4sPdnBIdqMVtHtv0IGi6dcpNpNkNac0zB2uUAqNX3MHzN8c+z2rwQ==", + "license": "MIT", + "peer": true + }, + "node_modules/react-redux": { + "version": "9.2.0", + "resolved": "https://registry.npmjs.org/react-redux/-/react-redux-9.2.0.tgz", + "integrity": "sha512-ROY9fvHhwOD9ySfrF0wmvu//bKCQ6AeZZq1nJNtbDC+kk5DuSuNX/n6YWYF/SYy7bSba4D4FSz8DJeKY/S/r+g==", + "license": "MIT", + "dependencies": { + "@types/use-sync-external-store": "^0.0.6", + "use-sync-external-store": "^1.4.0" + }, + "peerDependencies": { + "@types/react": "^18.2.25 || ^19", + "react": "^18.0 || ^19", + "redux": "^5.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "redux": { + "optional": true + } + } + }, + "node_modules/react-remove-scroll": { + "version": "2.7.2", + "resolved": "https://registry.npmjs.org/react-remove-scroll/-/react-remove-scroll-2.7.2.tgz", + "integrity": "sha512-Iqb9NjCCTt6Hf+vOdNIZGdTiH1QSqr27H/Ek9sv/a97gfueI/5h1s3yRi1nngzMUaOOToin5dI1dXKdXiF+u0Q==", + "license": "MIT", + "dependencies": { + "react-remove-scroll-bar": "^2.3.7", + "react-style-singleton": "^2.2.3", + "tslib": "^2.1.0", + "use-callback-ref": "^1.3.3", + "use-sidecar": "^1.1.3" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/react-remove-scroll-bar": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/react-remove-scroll-bar/-/react-remove-scroll-bar-2.3.8.tgz", + "integrity": "sha512-9r+yi9+mgU33AKcj6IbT9oRCO78WriSj6t/cF8DWBZJ9aOGPOTEDvdUDz1FwKim7QXWwmHqtdHnRJfhAxEG46Q==", + "license": "MIT", + "dependencies": { + "react-style-singleton": "^2.2.2", + "tslib": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/react-style-singleton": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/react-style-singleton/-/react-style-singleton-2.2.3.tgz", + "integrity": "sha512-b6jSvxvVnyptAiLjbkWLE/lOnR4lfTtDAl+eUC7RZy+QQWc6wRzIV2CE6xBuMmDxc2qIihtDCZD5NPOFl7fRBQ==", + "license": "MIT", + "dependencies": { + "get-nonce": "^1.0.0", + "tslib": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/readdirp": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", + "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", + "dev": true, + "license": "MIT", + "dependencies": { + "picomatch": "^2.2.1" + }, + "engines": { + "node": ">=8.10.0" + } + }, + "node_modules/readdirp/node_modules/picomatch": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz", + "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/recharts": { + "version": "3.8.0", + "resolved": "https://registry.npmjs.org/recharts/-/recharts-3.8.0.tgz", + "integrity": "sha512-Z/m38DX3L73ExO4Tpc9/iZWHmHnlzWG4njQbxsF5aSjwqmHNDDIm0rdEBArkwsBvR8U6EirlEHiQNYWCVh9sGQ==", + "license": "MIT", + "workspaces": [ + "www" + ], + "dependencies": { + "@reduxjs/toolkit": "^1.9.0 || 2.x.x", + "clsx": "^2.1.1", + "decimal.js-light": "^2.5.1", + "es-toolkit": "^1.39.3", + "eventemitter3": "^5.0.1", + "immer": "^10.1.1", + "react-redux": "8.x.x || 9.x.x", + "reselect": "5.1.1", + "tiny-invariant": "^1.3.3", + "use-sync-external-store": "^1.2.2", + "victory-vendor": "^37.0.2" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", + "react-dom": "^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", + "react-is": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, + "node_modules/redux": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/redux/-/redux-5.0.1.tgz", + "integrity": "sha512-M9/ELqF6fy8FwmkpnF0S3YKOqMyoWJ4+CS5Efg2ct3oY9daQvd/Pc71FpGZsVsbl3Cpb+IIcjBDUnnyBdQbq4w==", + "license": "MIT" + }, + "node_modules/redux-thunk": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/redux-thunk/-/redux-thunk-3.1.0.tgz", + "integrity": "sha512-NW2r5T6ksUKXCabzhL9z+h206HQw/NJkcLm1GPImRQ8IzfXwRGqjVhKJGauHirT0DAuyy6hjdnMZaRoAcy0Klw==", + "license": "MIT", + "peerDependencies": { + "redux": "^5.0.0" + } + }, + "node_modules/reselect": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/reselect/-/reselect-5.1.1.tgz", + "integrity": "sha512-K/BG6eIky/SBpzfHZv/dd+9JBFiS4SWV7FIujVyJRux6e45+73RaUHXLmIR1f7WOMaQ0U1km6qwklRQxpJJY0w==", + "license": "MIT" + }, + "node_modules/rolldown": { + "version": "1.0.0-rc.17", + "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.0.0-rc.17.tgz", + "integrity": "sha512-ZrT53oAKrtA4+YtBWPQbtPOxIbVDbxT0orcYERKd63VJTF13zPcgXTvD4843L8pcsI7M6MErt8QtON6lrB9tyA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@oxc-project/types": "=0.127.0", + "@rolldown/pluginutils": "1.0.0-rc.17" + }, + "bin": { + "rolldown": "bin/cli.mjs" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "optionalDependencies": { + "@rolldown/binding-android-arm64": "1.0.0-rc.17", + "@rolldown/binding-darwin-arm64": "1.0.0-rc.17", + "@rolldown/binding-darwin-x64": "1.0.0-rc.17", + "@rolldown/binding-freebsd-x64": "1.0.0-rc.17", + "@rolldown/binding-linux-arm-gnueabihf": "1.0.0-rc.17", + "@rolldown/binding-linux-arm64-gnu": "1.0.0-rc.17", + "@rolldown/binding-linux-arm64-musl": "1.0.0-rc.17", + "@rolldown/binding-linux-ppc64-gnu": "1.0.0-rc.17", + "@rolldown/binding-linux-s390x-gnu": "1.0.0-rc.17", + "@rolldown/binding-linux-x64-gnu": "1.0.0-rc.17", + "@rolldown/binding-linux-x64-musl": "1.0.0-rc.17", + "@rolldown/binding-openharmony-arm64": "1.0.0-rc.17", + "@rolldown/binding-wasm32-wasi": "1.0.0-rc.17", + "@rolldown/binding-win32-arm64-msvc": "1.0.0-rc.17", + "@rolldown/binding-win32-x64-msvc": "1.0.0-rc.17" + } + }, + "node_modules/rolldown/node_modules/@rolldown/pluginutils": { + "version": "1.0.0-rc.17", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-rc.17.tgz", + "integrity": "sha512-n8iosDOt6Ig1UhJ2AYqoIhHWh/isz0xpicHTzpKBeotdVsTEcxsSA/i3EVM7gQAj0rU27OLAxCjzlj15IWY7bg==", + "dev": true, + "license": "MIT" + }, + "node_modules/scheduler": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.27.0.tgz", + "integrity": "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==", + "license": "MIT" + }, + "node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/seroval": { + "version": "1.5.2", + "resolved": "https://registry.npmjs.org/seroval/-/seroval-1.5.2.tgz", + "integrity": "sha512-xcRN39BdsnO9Tf+VzsE7b3JyTJASItIV1FVFewJKCFcW4s4haIKS3e6vj8PGB9qBwC7tnuOywQMdv5N4qkzi7Q==", + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/seroval-plugins": { + "version": "1.5.2", + "resolved": "https://registry.npmjs.org/seroval-plugins/-/seroval-plugins-1.5.2.tgz", + "integrity": "sha512-qpY0Cl+fKYFn4GOf3cMiq6l72CpuVaawb6ILjubOQ+diJ54LfOWaSSPsaswN8DRPIPW4Yq+tE1k5aKd7ILyaFg==", + "license": "MIT", + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "seroval": "^1.0" + } + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/tailwind-merge": { + "version": "3.5.0", + "resolved": "https://registry.npmjs.org/tailwind-merge/-/tailwind-merge-3.5.0.tgz", + "integrity": "sha512-I8K9wewnVDkL1NTGoqWmVEIlUcB9gFriAEkXkfCjX5ib8ezGxtR3xD7iZIxrfArjEsH7F1CHD4RFUtxefdqV/A==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/dcastil" + } + }, + "node_modules/tailwindcss": { + "version": "4.2.4", + "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.2.4.tgz", + "integrity": "sha512-HhKppgO81FQof5m6TEnuBWCZGgfRAWbaeOaGT00KOy/Pf/j6oUihdvBpA7ltCeAvZpFhW3j0PTclkxsd4IXYDA==", + "dev": true, + "license": "MIT" + }, + "node_modules/tapable": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.3.3.tgz", + "integrity": "sha512-uxc/zpqFg6x7C8vOE7lh6Lbda8eEL9zmVm/PLeTPBRhh1xCgdWaQ+J1CUieGpIfm2HdtsUpRv+HshiasBMcc6A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, + "node_modules/tiny-invariant": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/tiny-invariant/-/tiny-invariant-1.3.3.tgz", + "integrity": "sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg==", + "license": "MIT" + }, + "node_modules/tinyglobby": { + "version": "0.2.16", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.16.tgz", + "integrity": "sha512-pn99VhoACYR8nFHhxqix+uvsbXineAasWm5ojXoN8xEwK5Kd3/TrhNn1wByuD52UxWRLy8pu+kRMniEi6Eq9Zg==", + "dev": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.4" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/ts-api-utils": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.5.0.tgz", + "integrity": "sha512-OJ/ibxhPlqrMM0UiNHJ/0CKQkoKF243/AEmplt3qpRgkW8VG7IfOS41h7V8TjITqdByHzrjcS/2si+y4lIh8NA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18.12" + }, + "peerDependencies": { + "typescript": ">=4.8.4" + } + }, + "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/type-check": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", + "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/typescript": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-6.0.3.tgz", + "integrity": "sha512-y2TvuxSZPDyQakkFRPZHKFm+KKVqIisdg9/CZwm9ftvKXLP8NRWj38/ODjNbr43SsoXqNuAisEf1GdCxqWcdBw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/typescript-eslint": { + "version": "8.59.0", + "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.59.0.tgz", + "integrity": "sha512-BU3ONW9X+v90EcCH9ZS6LMackcVtxRLlI3XrYyqZIwVSHIk7Qf7bFw1z0M9Q0IUxhTMZCf8piY9hTYaNEIASrw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/eslint-plugin": "8.59.0", + "@typescript-eslint/parser": "8.59.0", + "@typescript-eslint/typescript-estree": "8.59.0", + "@typescript-eslint/utils": "8.59.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/undici-types": { + "version": "7.16.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.16.0.tgz", + "integrity": "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==", + "dev": true, + "license": "MIT" + }, + "node_modules/unplugin": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/unplugin/-/unplugin-3.0.0.tgz", + "integrity": "sha512-0Mqk3AT2TZCXWKdcoaufeXNukv2mTrEZExeXlHIOZXdqYoHHr4n51pymnwV8x2BOVxwXbK2HLlI7usrqMpycdg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/remapping": "^2.3.5", + "picomatch": "^4.0.3", + "webpack-virtual-modules": "^0.6.2" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/update-browserslist-db": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz", + "integrity": "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "escalade": "^3.2.0", + "picocolors": "^1.1.1" + }, + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" + } + }, + "node_modules/uri-js": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "punycode": "^2.1.0" + } + }, + "node_modules/use-callback-ref": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/use-callback-ref/-/use-callback-ref-1.3.3.tgz", + "integrity": "sha512-jQL3lRnocaFtu3V00JToYz/4QkNWswxijDaCVNZRiRTO3HQDLsdu1ZtmIUvV4yPp+rvWm5j0y0TG/S61cuijTg==", + "license": "MIT", + "dependencies": { + "tslib": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/use-sidecar": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/use-sidecar/-/use-sidecar-1.1.3.tgz", + "integrity": "sha512-Fedw0aZvkhynoPYlA5WXrMCAMm+nSWdZt6lzJQ7Ok8S6Q+VsHmHpRWndVRJ8Be0ZbkfPc5LRYH+5XrzXcEeLRQ==", + "license": "MIT", + "dependencies": { + "detect-node-es": "^1.1.0", + "tslib": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/use-sync-external-store": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.6.0.tgz", + "integrity": "sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w==", + "license": "MIT", + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, + "node_modules/victory-vendor": { + "version": "37.3.6", + "resolved": "https://registry.npmjs.org/victory-vendor/-/victory-vendor-37.3.6.tgz", + "integrity": "sha512-SbPDPdDBYp+5MJHhBCAyI7wKM3d5ivekigc2Dk2s7pgbZ9wIgIBYGVw4zGHBml/qTFbexrofXW6Gu4noGxrOwQ==", + "license": "MIT AND ISC", + "dependencies": { + "@types/d3-array": "^3.0.3", + "@types/d3-ease": "^3.0.0", + "@types/d3-interpolate": "^3.0.1", + "@types/d3-scale": "^4.0.2", + "@types/d3-shape": "^3.1.0", + "@types/d3-time": "^3.0.0", + "@types/d3-timer": "^3.0.0", + "d3-array": "^3.1.6", + "d3-ease": "^3.0.1", + "d3-interpolate": "^3.0.1", + "d3-scale": "^4.0.2", + "d3-shape": "^3.1.0", + "d3-time": "^3.0.0", + "d3-timer": "^3.0.1" + } + }, + "node_modules/vite": { + "version": "8.0.10", + "resolved": "https://registry.npmjs.org/vite/-/vite-8.0.10.tgz", + "integrity": "sha512-rZuUu9j6J5uotLDs+cAA4O5H4K1SfPliUlQwqa6YEwSrWDZzP4rhm00oJR5snMewjxF5V/K3D4kctsUTsIU9Mw==", + "dev": true, + "license": "MIT", + "dependencies": { + "lightningcss": "^1.32.0", + "picomatch": "^4.0.4", + "postcss": "^8.5.10", + "rolldown": "1.0.0-rc.17", + "tinyglobby": "^0.2.16" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^20.19.0 || >=22.12.0", + "@vitejs/devtools": "^0.1.0", + "esbuild": "^0.27.0 || ^0.28.0", + "jiti": ">=1.21.0", + "less": "^4.0.0", + "sass": "^1.70.0", + "sass-embedded": "^1.70.0", + "stylus": ">=0.54.8", + "sugarss": "^5.0.0", + "terser": "^5.16.0", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "@vitejs/devtools": { + "optional": true + }, + "esbuild": { + "optional": true + }, + "jiti": { + "optional": true + }, + "less": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + }, + "node_modules/webpack-virtual-modules": { + "version": "0.6.2", + "resolved": "https://registry.npmjs.org/webpack-virtual-modules/-/webpack-virtual-modules-0.6.2.tgz", + "integrity": "sha512-66/V2i5hQanC51vBQKPH4aI8NMAcBW59FVBs+rC7eGHupMyfn34q7rZIE+ETlJ+XTevqfUhVVBgSUNSW2flEUQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/word-wrap": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", + "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "dev": true, + "license": "ISC" + }, + "node_modules/yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/zod": { + "version": "4.3.6", + "resolved": "https://registry.npmjs.org/zod/-/zod-4.3.6.tgz", + "integrity": "sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + }, + "node_modules/zod-validation-error": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/zod-validation-error/-/zod-validation-error-4.0.2.tgz", + "integrity": "sha512-Q6/nZLe6jxuU80qb/4uJ4t5v2VEZ44lzQjPDhYJNztRQ4wyWc6VF3D3Kb/fAuPetZQnhS3hnajCf9CsWesghLQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18.0.0" + }, + "peerDependencies": { + "zod": "^3.25.0 || ^4.0.0" + } + } + } +} diff --git a/dashboard/package.json b/dashboard/package.json new file mode 100644 index 00000000..8542f1e4 --- /dev/null +++ b/dashboard/package.json @@ -0,0 +1,42 @@ +{ + "name": "dashboard", + "private": true, + "version": "0.0.0", + "type": "module", + "scripts": { + "dev": "vite", + "build": "vite build", + "lint": "eslint .", + "preview": "vite preview" + }, + "dependencies": { + "@tanstack/react-query": "^5.100.5", + "@tanstack/react-router": "^1.168.24", + "@tanstack/router-devtools": "^1.166.13", + "class-variance-authority": "^0.7.1", + "clsx": "^2.1.1", + "lucide-react": "^1.11.0", + "radix-ui": "^1.4.3", + "react": "^19.2.5", + "react-dom": "^19.2.5", + "recharts": "^3.8.0", + "tailwind-merge": "^3.5.0" + }, + "devDependencies": { + "@eslint/js": "^10.0.1", + "@tailwindcss/vite": "^4.2.4", + "@tanstack/router-plugin": "^1.167.27", + "@types/node": "^24.12.2", + "@types/react": "^19.2.14", + "@types/react-dom": "^19.2.3", + "@vitejs/plugin-react": "^6.0.1", + "eslint": "^10.2.1", + "eslint-plugin-react-hooks": "^7.1.1", + "eslint-plugin-react-refresh": "^0.5.2", + "globals": "^17.5.0", + "tailwindcss": "^4.2.4", + "typescript": "~6.0.2", + "typescript-eslint": "^8.58.2", + "vite": "^8.0.10" + } +} diff --git a/dashboard/public/favicon.svg b/dashboard/public/favicon.svg new file mode 100644 index 00000000..6893eb13 --- /dev/null +++ b/dashboard/public/favicon.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/dashboard/public/icons.svg b/dashboard/public/icons.svg new file mode 100644 index 00000000..e9522193 --- /dev/null +++ b/dashboard/public/icons.svg @@ -0,0 +1,24 @@ + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/dashboard/src/components/ui/badge.tsx b/dashboard/src/components/ui/badge.tsx new file mode 100644 index 00000000..6eb2a057 --- /dev/null +++ b/dashboard/src/components/ui/badge.tsx @@ -0,0 +1,48 @@ +import * as React from "react" +import { cva, type VariantProps } from "class-variance-authority" +import { Slot } from "radix-ui" + +import { cn } from "@/lib/utils" + +const badgeVariants = cva( + "inline-flex w-fit shrink-0 items-center justify-center gap-1 overflow-hidden rounded-full border border-transparent px-2 py-0.5 text-xs font-medium whitespace-nowrap transition-[color,box-shadow] focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 aria-invalid:border-destructive aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 [&>svg]:pointer-events-none [&>svg]:size-3", + { + variants: { + variant: { + default: "bg-primary text-primary-foreground [a&]:hover:bg-primary/90", + secondary: + "bg-secondary text-secondary-foreground [a&]:hover:bg-secondary/90", + destructive: + "bg-destructive text-white focus-visible:ring-destructive/20 dark:bg-destructive/60 dark:focus-visible:ring-destructive/40 [a&]:hover:bg-destructive/90", + outline: + "border-border text-foreground [a&]:hover:bg-accent [a&]:hover:text-accent-foreground", + ghost: "[a&]:hover:bg-accent [a&]:hover:text-accent-foreground", + link: "text-primary underline-offset-4 [a&]:hover:underline", + }, + }, + defaultVariants: { + variant: "default", + }, + } +) + +function Badge({ + className, + variant = "default", + asChild = false, + ...props +}: React.ComponentProps<"span"> & + VariantProps & { asChild?: boolean }) { + const Comp = asChild ? Slot.Root : "span" + + return ( + + ) +} + +export { Badge, badgeVariants } diff --git a/dashboard/src/components/ui/button.tsx b/dashboard/src/components/ui/button.tsx new file mode 100644 index 00000000..4d38506c --- /dev/null +++ b/dashboard/src/components/ui/button.tsx @@ -0,0 +1,64 @@ +import * as React from "react" +import { cva, type VariantProps } from "class-variance-authority" +import { Slot } from "radix-ui" + +import { cn } from "@/lib/utils" + +const buttonVariants = cva( + "inline-flex shrink-0 items-center justify-center gap-2 rounded-md text-sm font-medium whitespace-nowrap transition-all outline-none focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 disabled:pointer-events-none disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4", + { + variants: { + variant: { + default: "bg-primary text-primary-foreground hover:bg-primary/90", + destructive: + "bg-destructive text-white hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:bg-destructive/60 dark:focus-visible:ring-destructive/40", + outline: + "border bg-background shadow-xs hover:bg-accent hover:text-accent-foreground dark:border-input dark:bg-input/30 dark:hover:bg-input/50", + secondary: + "bg-secondary text-secondary-foreground hover:bg-secondary/80", + ghost: + "hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50", + link: "text-primary underline-offset-4 hover:underline", + }, + size: { + default: "h-9 px-4 py-2 has-[>svg]:px-3", + xs: "h-6 gap-1 rounded-md px-2 text-xs has-[>svg]:px-1.5 [&_svg:not([class*='size-'])]:size-3", + sm: "h-8 gap-1.5 rounded-md px-3 has-[>svg]:px-2.5", + lg: "h-10 rounded-md px-6 has-[>svg]:px-4", + icon: "size-9", + "icon-xs": "size-6 rounded-md [&_svg:not([class*='size-'])]:size-3", + "icon-sm": "size-8", + "icon-lg": "size-10", + }, + }, + defaultVariants: { + variant: "default", + size: "default", + }, + } +) + +function Button({ + className, + variant = "default", + size = "default", + asChild = false, + ...props +}: React.ComponentProps<"button"> & + VariantProps & { + asChild?: boolean + }) { + const Comp = asChild ? Slot.Root : "button" + + return ( + + ) +} + +export { Button, buttonVariants } diff --git a/dashboard/src/components/ui/card.tsx b/dashboard/src/components/ui/card.tsx new file mode 100644 index 00000000..acf57dc5 --- /dev/null +++ b/dashboard/src/components/ui/card.tsx @@ -0,0 +1,92 @@ +import * as React from "react" + +import { cn } from "@/lib/utils" + +function Card({ className, ...props }: React.ComponentProps<"div">) { + return ( +
    + ) +} + +function CardHeader({ className, ...props }: React.ComponentProps<"div">) { + return ( +
    + ) +} + +function CardTitle({ className, ...props }: React.ComponentProps<"div">) { + return ( +
    + ) +} + +function CardDescription({ className, ...props }: React.ComponentProps<"div">) { + return ( +
    + ) +} + +function CardAction({ className, ...props }: React.ComponentProps<"div">) { + return ( +
    + ) +} + +function CardContent({ className, ...props }: React.ComponentProps<"div">) { + return ( +
    + ) +} + +function CardFooter({ className, ...props }: React.ComponentProps<"div">) { + return ( +
    + ) +} + +export { + Card, + CardHeader, + CardFooter, + CardTitle, + CardAction, + CardDescription, + CardContent, +} diff --git a/dashboard/src/components/ui/chart.tsx b/dashboard/src/components/ui/chart.tsx new file mode 100644 index 00000000..6947c2e7 --- /dev/null +++ b/dashboard/src/components/ui/chart.tsx @@ -0,0 +1,372 @@ +import * as React from "react" +import * as RechartsPrimitive from "recharts" +import type { TooltipValueType } from "recharts" + +import { cn } from "@/lib/utils" + +// Format: { THEME_NAME: CSS_SELECTOR } +const THEMES = { light: "", dark: ".dark" } as const + +const INITIAL_DIMENSION = { width: 320, height: 200 } as const +type TooltipNameType = number | string + +export type ChartConfig = Record< + string, + { + label?: React.ReactNode + icon?: React.ComponentType + } & ( + | { color?: string; theme?: never } + | { color?: never; theme: Record } + ) +> + +type ChartContextProps = { + config: ChartConfig +} + +const ChartContext = React.createContext(null) + +function useChart() { + const context = React.useContext(ChartContext) + + if (!context) { + throw new Error("useChart must be used within a ") + } + + return context +} + +function ChartContainer({ + id, + className, + children, + config, + initialDimension = INITIAL_DIMENSION, + ...props +}: React.ComponentProps<"div"> & { + config: ChartConfig + children: React.ComponentProps< + typeof RechartsPrimitive.ResponsiveContainer + >["children"] + initialDimension?: { + width: number + height: number + } +}) { + const uniqueId = React.useId() + const chartId = `chart-${id ?? uniqueId.replace(/:/g, "")}` + + return ( + +
    + + + {children} + +
    +
    + ) +} + +const ChartStyle = ({ id, config }: { id: string; config: ChartConfig }) => { + const colorConfig = Object.entries(config).filter( + ([, config]) => config.theme ?? config.color + ) + + if (!colorConfig.length) { + return null + } + + return ( + - - - - ${body} - -`; - -async function handleReport(res) { - try { - const { stdout } = await execFileAsync('node', ['scripts/report.js']); - res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' }); - res.end(HTML(`

    Performance Report

    ${stdout}
    `)); - } catch (err) { - res.writeHead(500, { 'Content-Type': 'text/plain' }); - res.end(err.message); - } +// ── CSV parsing ────────────────────────────────────────────────────────────── + +function parseCsv(filepath) { + if (!existsSync(filepath)) return []; + const lines = readFileSync(filepath, "utf8").trim().split("\n"); + if (lines.length < 2) return []; + const headers = lines[0].split(","); + return lines.slice(1).map((line) => { + const vals = line.split(","); + return Object.fromEntries(headers.map((h, i) => [h, vals[i] ?? ""])); + }); } -async function handleLogsList(res) { - try { - const files = (await readdir(LOGS_DIR)) - .filter(f => ALLOWED_EXTENSIONS.has(path.extname(f))) - .sort(); - const links = files.map(f => `
  • ${f}
  • `).join('\n'); - res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' }); - res.end(HTML(`

    Log Files

      ${links}
    `)); - } catch (err) { - res.writeHead(500); res.end(err.message); +function parseNum(v) { + const n = parseFloat(v); + return Number.isFinite(n) ? n : null; +} + +function coerceTrades(rows) { + return rows.map((r) => ({ + ...r, + entry_price: parseNum(r.entry_price), + exit_price: parseNum(r.exit_price), + shares: parseNum(r.shares), + invested: parseNum(r.invested), + exit_value: parseNum(r.exit_value), + pnl: parseNum(r.pnl), + roi_pct: parseNum(r.roi_pct), + duration_s: parseNum(r.duration_s), + ptb_at_entry: parseNum(r.ptb_at_entry), + btc_at_entry: parseNum(r.btc_at_entry), + btc_vs_ptb_at_entry: parseNum(r.btc_vs_ptb_at_entry), + market_up_at_entry: parseNum(r.market_up_at_entry), + market_down_at_entry: parseNum(r.market_down_at_entry), + })); +} + +function coerceSignals15m(rows) { + return rows.map((r) => ({ + ...r, + time_left_min: parseNum(r.time_left_min), + btc_price: parseNum(r.btc_price), + market_up: parseNum(r.market_up), + market_down: parseNum(r.market_down), + model_up: parseNum(r.model_up), + model_down: parseNum(r.model_down), + edge_up: parseNum(r.edge_up), + edge_down: parseNum(r.edge_down), + rsi: parseNum(r.rsi), + rsi_slope: parseNum(r.rsi_slope), + macd_hist: parseNum(r.macd_hist), + ha_count: parseNum(r.ha_count), + vwap: parseNum(r.vwap), + vwap_dist_pct: parseNum(r.vwap_dist_pct), + vwap_slope: parseNum(r.vwap_slope), + sim_entry_price: parseNum(r.sim_entry_price), + sim_current_price: parseNum(r.sim_current_price), + sim_roi_pct: parseNum(r.sim_roi_pct), + sim_pnl: parseNum(r.sim_pnl), + sim_cum_pnl: parseNum(r.sim_cum_pnl) ?? 0, + })); +} + +function coerceSignals5m(rows) { + return rows.map((r) => ({ + ...r, + time_left_min: parseNum(r.time_left_min), + btc_price: parseNum(r.btc_price), + market_up: parseNum(r.market_up), + market_down: parseNum(r.market_down), + model_up: parseNum(r.model_up), + model_down: parseNum(r.model_down), + edge_up: parseNum(r.edge_up), + edge_down: parseNum(r.edge_down), + ofi_30s: parseNum(r.ofi_30s), + ofi_1m: parseNum(r.ofi_1m), + ofi_2m: parseNum(r.ofi_2m), + roc1: parseNum(r.roc1), + roc3: parseNum(r.roc3), + rsi: parseNum(r.rsi), + ha_count: parseNum(r.ha_count), + vwap: parseNum(r.vwap), + vwap_dist_pct: parseNum(r.vwap_dist_pct), + vwap_slope: parseNum(r.vwap_slope), + sim_entry_price: parseNum(r.sim_entry_price), + sim_current_price: parseNum(r.sim_current_price), + sim_roi_pct: parseNum(r.sim_roi_pct), + sim_pnl: parseNum(r.sim_pnl), + sim_cum_pnl: parseNum(r.sim_cum_pnl) ?? 0, + })); +} + +// ── Stats computation ──────────────────────────────────────────────────────── + +function computeStats(trades) { + if (trades.length === 0) { + return { + totalTrades: 0, wins: 0, losses: 0, winRate: 0, + totalPnl: 0, avgPnl: 0, avgWin: 0, avgLoss: 0, profitFactor: 0, + maxWinRoi: 0, maxLossRoi: 0, avgDurationS: 0, + maxWinStreak: 0, maxLossStreak: 0, + firstEntry: null, lastExit: null, + byReason: {}, bySide: {}, pnlCurve: [], + }; + } + + const wins = trades.filter((t) => t.pnl > 0).length; + const losses = trades.length - wins; + const totalPnl = trades.reduce((s, t) => s + t.pnl, 0); + const winTrades = trades.filter((t) => t.pnl > 0); + const lossTrades = trades.filter((t) => t.pnl <= 0); + const avgWin = winTrades.length ? winTrades.reduce((s, t) => s + t.pnl, 0) / winTrades.length : 0; + const avgLoss = lossTrades.length ? lossTrades.reduce((s, t) => s + t.pnl, 0) / lossTrades.length : 0; + const profitFactor = lossTrades.length && avgLoss !== 0 + ? Math.abs(avgWin * winTrades.length) / Math.abs(avgLoss * lossTrades.length) + : Infinity; + + const byReason = {}; + const bySide = {}; + for (const t of trades) { + const r = t.exit_reason || "UNKNOWN"; + if (!byReason[r]) byReason[r] = { count: 0, pnl: 0 }; + byReason[r].count++; + byReason[r].pnl += t.pnl; + const s = t.side || "UNKNOWN"; + if (!bySide[s]) bySide[s] = { count: 0, wins: 0, pnl: 0 }; + bySide[s].count++; + bySide[s].pnl += t.pnl; + if (t.pnl > 0) bySide[s].wins++; + } + + let maxWinStreak = 0, maxLossStreak = 0, curWin = 0, curLoss = 0; + for (const t of trades) { + if (t.pnl > 0) { curWin++; curLoss = 0; maxWinStreak = Math.max(maxWinStreak, curWin); } + else { curLoss++; curWin = 0; maxLossStreak = Math.max(maxLossStreak, curLoss); } } + + const rois = trades.map((t) => t.roi_pct); + let cum = 0; + const pnlCurve = trades.map((t) => { cum += t.pnl; return { time: t.exit_time, pnl: parseFloat(cum.toFixed(4)) }; }); + + return { + totalTrades: trades.length, + wins, + losses, + winRate: wins / trades.length, + totalPnl: parseFloat(totalPnl.toFixed(4)), + avgPnl: parseFloat((totalPnl / trades.length).toFixed(4)), + avgWin: parseFloat(avgWin.toFixed(4)), + avgLoss: parseFloat(avgLoss.toFixed(4)), + profitFactor: profitFactor === Infinity ? 9999 : parseFloat(profitFactor.toFixed(4)), + maxWinRoi: Math.max(...rois), + maxLossRoi: Math.min(...rois), + avgDurationS: trades.reduce((s, t) => s + (t.duration_s ?? 0), 0) / trades.length, + maxWinStreak, + maxLossStreak, + firstEntry: trades[0]?.entry_time ?? null, + lastExit: trades[trades.length - 1]?.exit_time ?? null, + byReason, + bySide, + pnlCurve, + }; } -async function handleLogFile(filename, res) { - const safe = path.basename(filename); - if (!ALLOWED_EXTENSIONS.has(path.extname(safe))) { - res.writeHead(403); res.end('Forbidden'); return; +// ── Static file serving ────────────────────────────────────────────────────── + +const MIME = { + ".html": "text/html; charset=utf-8", + ".js": "application/javascript; charset=utf-8", + ".mjs": "application/javascript; charset=utf-8", + ".css": "text/css; charset=utf-8", + ".svg": "image/svg+xml", + ".png": "image/png", + ".ico": "image/x-icon", + ".json": "application/json", + ".woff2": "font/woff2", + ".woff": "font/woff", +}; + +function serveStatic(urlPath, res) { + if (!existsSync(DIST_DIR)) { + res.writeHead(503, { "Content-Type": "text/plain" }); + res.end("Dashboard not built yet. Run: cd dashboard && npm run build"); + return; + } + + let filePath = path.join(DIST_DIR, urlPath === "/" ? "index.html" : urlPath); + if (!existsSync(filePath) || statSync(filePath).isDirectory()) { + filePath = path.join(DIST_DIR, "index.html"); } - const filepath = path.join(LOGS_DIR, safe); - if (!existsSync(filepath)) { - res.writeHead(404); res.end('Not found'); return; + + try { + const data = readFileSync(filePath); + const ext = path.extname(filePath); + res.writeHead(200, { "Content-Type": MIME[ext] ?? "application/octet-stream" }); + res.end(data); + } catch { + res.writeHead(404); res.end("Not found"); } - const data = await readFile(filepath); +} + +// ── Request handler ────────────────────────────────────────────────────────── + +function json(res, data) { + const body = JSON.stringify(data); res.writeHead(200, { - 'Content-Type': 'text/plain; charset=utf-8', - 'Content-Disposition': `attachment; filename="${safe}"`, + "Content-Type": "application/json", + "Access-Control-Allow-Origin": "*", + "Cache-Control": "no-store", }); - res.end(data); + res.end(body); } -const server = http.createServer(async (req, res) => { - const url = new URL(req.url, 'http://localhost'); +const server = http.createServer((req, res) => { + const url = new URL(req.url, "http://localhost"); + const p = url.pathname; + + if (req.method === "OPTIONS") { + res.writeHead(204, { "Access-Control-Allow-Origin": "*", "Access-Control-Allow-Methods": "GET" }); + res.end(); return; + } + try { - if (url.pathname === '/' || url.pathname === '/report') { - await handleReport(res); - } else if (url.pathname === '/logs') { - await handleLogsList(res); - } else if (url.pathname.startsWith('/logs/')) { - await handleLogFile(decodeURIComponent(url.pathname.slice(6)), res); - } else { - res.writeHead(404); res.end('Not found'); + if (p === "/api/trades/15m") { + const rows = coerceTrades(parseCsv(path.join(LOGS_DIR, "dryrun_15m_trades.csv"))); + return json(res, rows); + } + + if (p === "/api/trades/5m") { + const rows = coerceTrades(parseCsv(path.join(LOGS_DIR, "dryrun_5m_trades.csv"))); + return json(res, rows); + } + + if (p === "/api/stats") { + const t15 = coerceTrades(parseCsv(path.join(LOGS_DIR, "dryrun_15m_trades.csv"))); + const t5 = coerceTrades(parseCsv(path.join(LOGS_DIR, "dryrun_5m_trades.csv"))); + return json(res, { "15m": computeStats(t15), "5m": computeStats(t5) }); } + + if (p === "/api/live") { + const rows15 = coerceSignals15m(parseCsv(path.join(LOGS_DIR, "dryrun_15m.csv"))); + const rows5 = coerceSignals5m(parseCsv(path.join(LOGS_DIR, "dryrun_5m.csv"))); + return json(res, { + "15m": rows15.length ? rows15[rows15.length - 1] : null, + "5m": rows5.length ? rows5[rows5.length - 1] : null, + }); + } + + if (p.startsWith("/api/")) { + res.writeHead(404, { "Content-Type": "application/json" }); + res.end(JSON.stringify({ error: "not found" })); return; + } + + serveStatic(p, res); } catch (err) { + console.error(err); res.writeHead(500); res.end(err.message); } }); -server.listen(PORT, () => console.log(`Log server on http://0.0.0.0:${PORT}`)); +server.listen(PORT, () => console.log(`Dashboard server on http://0.0.0.0:${PORT}`)); From c7c3043608942ac33581b9a66b47f95edbb9c24f Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 26 Apr 2026 03:35:01 +0000 Subject: [PATCH 30/49] fix(dashboard): improve mobile UI/UX across all pages MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - __root: hide sidebar on mobile; add top bar with hamburger + Sheet drawer that closes on navigation; keep desktop sidebar unchanged - trades: replace ScrollArea with overflow-x/y-auto + min-w-[700px] table so all 9 columns scroll horizontally on narrow viewports - signals: add flex-wrap + shrink-0 to rec/badge/time rows; add min-w-0 + gap to Kv rows; use items-start on card headers so timestamps don't overflow; reduce gap-x-6 → gap-x-4 in indicator grids - index: p-6 → p-4 md:p-6; truncate stat card value/sub text; flex-wrap on page header; YAxis width 80→72 for tighter chart on mobile https://claude.ai/code/session_01VkxSGCs14ai9xCwT3HDne1 --- dashboard/src/routes/__root.tsx | 75 +++++++++++++++++----- dashboard/src/routes/index.tsx | 14 ++--- dashboard/src/routes/signals.tsx | 59 ++++++++++-------- dashboard/src/routes/trades.tsx | 103 ++++++++++++++++--------------- 4 files changed, 151 insertions(+), 100 deletions(-) diff --git a/dashboard/src/routes/__root.tsx b/dashboard/src/routes/__root.tsx index 383ec397..89a0bbc6 100644 --- a/dashboard/src/routes/__root.tsx +++ b/dashboard/src/routes/__root.tsx @@ -1,7 +1,10 @@ +import { useState } from "react" import { createRootRoute, Link, Outlet } from "@tanstack/react-router" import { TanStackRouterDevtools } from "@tanstack/router-devtools" -import { Activity, BarChart3, Table2, Wifi } from "lucide-react" +import { Activity, BarChart3, Menu, Table2, Wifi } from "lucide-react" import { Separator } from "@/components/ui/separator" +import { Sheet, SheetContent, SheetHeader, SheetTitle } from "@/components/ui/sheet" +import { Button } from "@/components/ui/button" export const Route = createRootRoute({ component: RootLayout, @@ -13,29 +16,71 @@ const navItems = [ { to: "/signals", label: "Live Signals", icon: Wifi, exact: false }, ] +function NavLinks({ onNavigate }: { onNavigate?: () => void }) { + return ( + <> + {navItems.map(({ to, label, icon: Icon, exact }) => ( + + + {label} + + ))} + + ) +} + function RootLayout() { + const [mobileOpen, setMobileOpen] = useState(false) + return ( -
    -