From ed849852377a2fc7bc6d713c11b22b65976fab80 Mon Sep 17 00:00:00 2001 From: Manohar Date: Mon, 11 May 2026 04:45:22 +0000 Subject: [PATCH] feat: 30s polling, market open/close Telegram alerts, mobile responsive UI --- public/index.html | 37 +++++++++++++++++++++++++++++++++++++ src/index.ts | 8 +++++++- src/tracker/poll.ts | 43 +++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 87 insertions(+), 1 deletion(-) diff --git a/public/index.html b/public/index.html index 0e68b5b..72b1682 100644 --- a/public/index.html +++ b/public/index.html @@ -95,6 +95,43 @@ .collapse-body { overflow:hidden; transition:max-height .35s cubic-bezier(.4,0,.2,1); max-height:5000px; } .collapsible.collapsed .collapse-body { max-height:0 !important; } + + /* ── Mobile responsive ── */ + @media (max-width: 768px) { + .wrap { padding: 12px; } + nav { padding: 10px 14px; flex-wrap: wrap; gap: 8px; } + .nav-sub { display: none; } + .ts { display: none; } + .g3, .g6 { grid-template-columns: 1fr 1fr; gap: 8px; margin-bottom: 10px; } + .g6 { grid-template-columns: repeat(3, 1fr); } + .pcard { padding: 16px 14px 12px; } + .pval { font-size: 1.6rem; } + .pcard.pt .pval { font-size: 2rem; } + .mcard { padding: 10px 12px; } + .mprice { font-size: 1.1rem; } + .scard { padding: 10px 12px; } + .card-head { padding: 12px 14px; flex-wrap: wrap; gap: 6px; } + .chart-wrap { height: 160px; padding: 8px 10px 12px; } + .range-btns { gap: 2px; } + .rbtn { padding: 2px 7px; font-size: .65rem; } + /* Tables: horizontal scroll */ + .card > .collapse-body { overflow-x: auto; } + table { min-width: 520px; } + td, thead th { padding: 9px 10px; font-size: .75rem; } + .sym { font-size: .72rem; } + .sym-meta, .tte { font-size: .6rem; } + .mono { font-size: .72rem; } + /* Settings */ + .sg { grid-template-columns: 1fr; gap: 10px; } + tfoot td { padding: 8px 10px; font-size: .72rem; } + } + @media (max-width: 480px) { + .g3 { grid-template-columns: 1fr; } + .g6 { grid-template-columns: repeat(2, 1fr); } + .pval { font-size: 1.4rem; } + .pcard.pt .pval { font-size: 1.7rem; } + } + diff --git a/src/index.ts b/src/index.ts index 94e15b6..fc31a81 100644 --- a/src/index.ts +++ b/src/index.ts @@ -2,7 +2,7 @@ import cron from 'node-cron'; import { initDb } from './db/client.js'; import { login } from './angel/auth.js'; -import { pollTick, forcePoll } from './tracker/poll.js'; +import { pollTick, forcePoll, sendMarketOpenAlert, sendPreCloseAlert } from './tracker/poll.js'; import { createServer } from './api/server.js'; import { sendServiceNotification } from './notify/telegram.js'; @@ -39,6 +39,12 @@ async function main() { console.log(`[main] Polling every ${POLL_SECONDS}s`); } + + // Market open alert — 9:15 IST = 3:45 UTC + cron.schedule("45 3 * * 1-5", sendMarketOpenAlert, { timezone: "UTC" }); + // Pre-close alert — 3:25 IST = 9:55 UTC + cron.schedule("55 9 * * 1-5", sendPreCloseAlert, { timezone: "UTC" }); + console.log("[main] Market open/close alerts scheduled"); // 6. Notify Telegram that service started await sendServiceNotification('start'); diff --git a/src/tracker/poll.ts b/src/tracker/poll.ts index e3920f6..c457e3e 100644 --- a/src/tracker/poll.ts +++ b/src/tracker/poll.ts @@ -218,3 +218,46 @@ function recordSnapshot(positions: Position[]): void { VALUES (?, ?, ?, ?, datetime('now')) `).run(totalUnrealised, totalRealised, totalPnl, positions.length); } + +/** + * Market open alert (9:15 IST) — summary of all positions at open + */ +export async function sendMarketOpenAlert(): Promise { + try { + const positions = await fetchAllPositions(); + if (!positions.length) { + await sendTelegram("🔔 *Market Open* — No open positions"); + return; + } + const totalPnl = positions.reduce((s, p) => s + p.totalPnl, 0); + const lines = positions.map(p => + ` • ${p.tradingsymbol}: ₹${p.totalPnl >= 0 ? "+" : ""}${p.totalPnl.toFixed(0)} (qty ${p.netqty})` + ).join("\n"); + await sendTelegram( + `🔔 *Market Open — Position Summary*\n\n${lines}\n\n*Total P&L: ₹${totalPnl >= 0 ? "+" : ""}${totalPnl.toFixed(0)}*` + ); + } catch (e) { + console.error("[poll] Market open alert error:", e); + } +} + +/** + * Market close warning (3:25 IST) — 5 min before close + */ +export async function sendPreCloseAlert(): Promise { + try { + const positions = await fetchAllPositions(); + const totalUnrealised = positions.reduce((s, p) => s + p.unrealisedPnl, 0); + const totalRealised = positions.reduce((s, p) => s + p.realisedPnl, 0); + const totalPnl = positions.reduce((s, p) => s + p.totalPnl, 0); + await sendTelegram( + `⚠️ *5 Min to Close — Review Positions*\n\n` + + `Unrealised: ₹${totalUnrealised >= 0 ? "+" : ""}${totalUnrealised.toFixed(0)}\n` + + `Realised: ₹${totalRealised >= 0 ? "+" : ""}${totalRealised.toFixed(0)}\n` + + `*Total: ₹${totalPnl >= 0 ? "+" : ""}${totalPnl.toFixed(0)}*\n\n` + + `${positions.length} open positions — consider closing before 3:30` + ); + } catch (e) { + console.error("[poll] Pre-close alert error:", e); + } +}