feat: 30s polling, market open/close Telegram alerts, mobile responsive UI

This commit is contained in:
Manohar 2026-05-11 04:45:22 +00:00
parent ea6af0ea82
commit ed84985237
3 changed files with 87 additions and 1 deletions

View file

@ -95,6 +95,43 @@
.collapse-body { overflow:hidden; transition:max-height .35s cubic-bezier(.4,0,.2,1); max-height:5000px; } .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; } .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; }
}
</style> </style>
</head> </head>
<body> <body>

View file

@ -2,7 +2,7 @@
import cron from 'node-cron'; import cron from 'node-cron';
import { initDb } from './db/client.js'; import { initDb } from './db/client.js';
import { login } from './angel/auth.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 { createServer } from './api/server.js';
import { sendServiceNotification } from './notify/telegram.js'; import { sendServiceNotification } from './notify/telegram.js';
@ -39,6 +39,12 @@ async function main() {
console.log(`[main] Polling every ${POLL_SECONDS}s`); 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 // 6. Notify Telegram that service started
await sendServiceNotification('start'); await sendServiceNotification('start');

View file

@ -218,3 +218,46 @@ function recordSnapshot(positions: Position[]): void {
VALUES (?, ?, ?, ?, datetime('now')) VALUES (?, ?, ?, ?, datetime('now'))
`).run(totalUnrealised, totalRealised, totalPnl, positions.length); `).run(totalUnrealised, totalRealised, totalPnl, positions.length);
} }
/**
* Market open alert (9:15 IST) summary of all positions at open
*/
export async function sendMarketOpenAlert(): Promise<void> {
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<void> {
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);
}
}