ee2ab5dda4
Two fs.watch callbacks can fire for the same row within the 300ms debounce window. Both enter the setTimeout, both read prevSide=null before either sets it, both call send(). Result: duplicate push within ~20ms. Fix: track lastSentTs. Same row timestamp = skip. Only a NEW row from the bot (or a new manual append) can trigger another push. Also log the slug before each push so we can diagnose the missing link.
139 lines
4.7 KiB
JavaScript
139 lines
4.7 KiB
JavaScript
import fs from "node:fs";
|
|
import path from "node:path";
|
|
import { fileURLToPath } from "node:url";
|
|
|
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
const ROOT = path.resolve(__dirname, "..");
|
|
|
|
try {
|
|
const { applyGlobalProxyFromEnv } = await import(path.join(ROOT, "src/net/proxy.js"));
|
|
applyGlobalProxyFromEnv();
|
|
} catch {}
|
|
|
|
const BOT_TOKEN = process.env.TELEGRAM_BOT_TOKEN;
|
|
const CHAT_ID = process.env.TELEGRAM_CHAT_ID;
|
|
const CSV_PATH = process.env.SIGNALS_CSV || path.join(ROOT, "logs/signals.csv");
|
|
const COOLDOWN_MS = Number(process.env.TELEGRAM_COOLDOWN_MS) || 30_000;
|
|
const DRY_RUN = process.env.DRY_RUN === "true";
|
|
const POLY_BASE = process.env.POLYMARKET_BASE_URL || "https://polymarket.com/zh/event";
|
|
|
|
if (!DRY_RUN && (!BOT_TOKEN || !CHAT_ID)) {
|
|
console.error("[watcher] Missing TELEGRAM_BOT_TOKEN or TELEGRAM_CHAT_ID");
|
|
console.error("[watcher] Or set DRY_RUN=true to log without sending.");
|
|
process.exit(1);
|
|
}
|
|
|
|
let prevSide = null;
|
|
let lastSentAt = 0;
|
|
let lastSentTs = null;
|
|
|
|
async function send(text) {
|
|
if (DRY_RUN) {
|
|
console.log("[watcher] DRY_RUN:", text.replace(/\n/g, " | "));
|
|
return;
|
|
}
|
|
try {
|
|
const r = await fetch(`https://api.telegram.org/bot${BOT_TOKEN}/sendMessage`, {
|
|
method: "POST",
|
|
headers: { "Content-Type": "application/json" },
|
|
body: JSON.stringify({ chat_id: CHAT_ID, text, parse_mode: "HTML" })
|
|
});
|
|
if (!r.ok) console.error("[watcher] Telegram error:", await r.text());
|
|
else console.log(`[watcher] sent ${new Date().toISOString()}`);
|
|
} catch (e) {
|
|
console.error("[watcher] send failed:", e.message);
|
|
}
|
|
}
|
|
|
|
function fmtTime(min) {
|
|
const m = Math.floor(min);
|
|
const s = Math.floor((min - m) * 60);
|
|
return `${String(m).padStart(2, "0")}:${String(s).padStart(2, "0")}`;
|
|
}
|
|
|
|
function buildMessage(hdr, row, side, phase, strength, slug) {
|
|
const v = (k) => row[hdr.indexOf(k)];
|
|
const isUp = side === "UP";
|
|
const edge = Number(isUp ? v("edge_up") : v("edge_down"));
|
|
const model = Number(isUp ? v("model_up") : v("model_down"));
|
|
const mktYes = Number(isUp ? v("mkt_up") : v("mkt_down"));
|
|
const mktNo = Number(isUp ? v("mkt_down") : v("mkt_up"));
|
|
const mktSum = mktYes + mktNo;
|
|
const mkt = mktSum > 0 ? mktYes / mktSum : mktYes;
|
|
const lines = [
|
|
`🚨 ENTER <b>${side}</b> — ${phase} (${strength})`,
|
|
`Edge: ${edge >= 0 ? "+" : ""}${(edge * 100).toFixed(1)}%`,
|
|
`Model ${(model * 100).toFixed(1)}% vs Market ${(mkt * 100).toFixed(1)}%`,
|
|
`Time left: ${fmtTime(Number(v("time_left_min")))}`,
|
|
`Regime: ${v("regime")}`,
|
|
v("timestamp")
|
|
];
|
|
if (slug) lines.push(`🔗 <a href="${POLY_BASE}/${slug}">Open on Polymarket</a>`);
|
|
return lines.join("\n");
|
|
}
|
|
|
|
function getCurrentSlug() {
|
|
if (!fs.existsSync("./logs")) return null;
|
|
const files = fs.readdirSync("./logs")
|
|
.filter((f) => f.startsWith("polymarket_market_") && f.endsWith(".json"))
|
|
.map((f) => ({ f, mtime: fs.statSync(path.join("./logs", f)).mtimeMs }))
|
|
.sort((a, b) => b.mtime - a.mtime);
|
|
if (!files.length) return null;
|
|
try {
|
|
const m = JSON.parse(fs.readFileSync(path.join("./logs", files[0].f), "utf8"));
|
|
return m.slug || null;
|
|
} catch {
|
|
return null;
|
|
}
|
|
}
|
|
|
|
function readLast() {
|
|
if (!fs.existsSync(CSV_PATH)) return null;
|
|
const content = fs.readFileSync(CSV_PATH, "utf8");
|
|
const lines = content.trim().split("\n");
|
|
if (lines.length < 2) return null;
|
|
const hdr = lines[0].split(",");
|
|
const row = lines[lines.length - 1].split(",");
|
|
return { hdr, row, rec: row[hdr.indexOf("recommendation")] || "" };
|
|
}
|
|
|
|
function watch() {
|
|
if (!fs.existsSync(CSV_PATH)) {
|
|
console.log(`[watcher] waiting for ${CSV_PATH} ...`);
|
|
setTimeout(watch, 1000);
|
|
return;
|
|
}
|
|
console.log(`[watcher] watching ${CSV_PATH} (cooldown ${COOLDOWN_MS}ms${DRY_RUN ? ", DRY_RUN" : ""})`);
|
|
|
|
fs.watch(CSV_PATH, { persistent: true }, () => {
|
|
setTimeout(async () => {
|
|
try {
|
|
const data = readLast();
|
|
if (!data) return;
|
|
const side = data.rec.startsWith("ENTER") ? data.rec.split(":")[1] : null;
|
|
if (!side) {
|
|
prevSide = null;
|
|
return;
|
|
}
|
|
const [, , phase, strength] = data.rec.split(":");
|
|
const ts = data.row[data.hdr.indexOf("timestamp")];
|
|
const flipped = side !== prevSide;
|
|
const cooled = Date.now() - lastSentAt > COOLDOWN_MS;
|
|
const newRow = ts !== lastSentTs;
|
|
if (flipped && cooled && newRow) {
|
|
const slug = getCurrentSlug();
|
|
console.log("[watcher] slug for push:", slug);
|
|
const msg = buildMessage(data.hdr, data.row, side, phase, strength, slug);
|
|
await send(msg);
|
|
lastSentAt = Date.now();
|
|
lastSentTs = ts;
|
|
}
|
|
prevSide = side;
|
|
} catch (e) {
|
|
console.error("[watcher] error:", e.message);
|
|
}
|
|
}, 300);
|
|
});
|
|
}
|
|
|
|
watch(); |