fix: snake_case->camelCase (alerts now work); feat: 2h portfolio digest cron
This commit is contained in:
parent
052db5d934
commit
c46570171a
2 changed files with 88 additions and 2 deletions
|
|
@ -44,7 +44,9 @@ async function main() {
|
||||||
cron.schedule("45 3 * * 1-5", sendMarketOpenAlert, { timezone: "UTC" });
|
cron.schedule("45 3 * * 1-5", sendMarketOpenAlert, { timezone: "UTC" });
|
||||||
// Pre-close alert — 3:25 IST = 9:55 UTC
|
// Pre-close alert — 3:25 IST = 9:55 UTC
|
||||||
cron.schedule("55 9 * * 1-5", sendPreCloseAlert, { timezone: "UTC" });
|
cron.schedule("55 9 * * 1-5", sendPreCloseAlert, { timezone: "UTC" });
|
||||||
console.log("[main] Market open/close alerts scheduled");
|
// 2h digest: 11:15 IST = 5:45 UTC, 13:15 IST = 7:45 UTC (open alert covers 9:15)
|
||||||
|
cron.schedule("45 5,7 * * 1-5", sendPortfolioDigest, { timezone: "UTC" });
|
||||||
|
console.log("[main] Market open/close alerts + 2h digest scheduled");
|
||||||
// 6. Notify Telegram that service started
|
// 6. Notify Telegram that service started
|
||||||
await sendServiceNotification('start');
|
await sendServiceNotification('start');
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -112,7 +112,14 @@ async function evaluateAlerts(positions: Position[], today: string): Promise<voi
|
||||||
// Only evaluate alerts for positions that are still open (netqty != 0)
|
// Only evaluate alerts for positions that are still open (netqty != 0)
|
||||||
const openPositions = positions.filter(p => p.netqty !== 0);
|
const openPositions = positions.filter(p => p.netqty !== 0);
|
||||||
for (const pos of openPositions) {
|
for (const pos of openPositions) {
|
||||||
let state = getBandState.get(pos.key) as BandState | undefined;
|
const _raw = getBandState.get(pos.key) as any;
|
||||||
|
const state: BandState | undefined = _raw ? {
|
||||||
|
positionKey: _raw.position_key,
|
||||||
|
anchorPnl: _raw.anchor_pnl,
|
||||||
|
lastAlertPnl: _raw.last_alert_pnl,
|
||||||
|
lastAlertTime: _raw.last_alert_time ? new Date(_raw.last_alert_time) : null,
|
||||||
|
tradingDate: _raw.trading_date,
|
||||||
|
} : undefined;
|
||||||
|
|
||||||
// First time seeing this position, or new trading day → initialise anchor
|
// First time seeing this position, or new trading day → initialise anchor
|
||||||
if (!state || state.tradingDate !== today) {
|
if (!state || state.tradingDate !== today) {
|
||||||
|
|
@ -263,3 +270,80 @@ export async function sendPreCloseAlert(): Promise<void> {
|
||||||
console.error("[poll] Pre-close alert error:", e);
|
console.error("[poll] Pre-close alert error:", e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 2-hour portfolio digest sent to Telegram during market hours.
|
||||||
|
* Covers: P&L summary, best/worst, expiry warnings, near-alert positions, net direction.
|
||||||
|
*/
|
||||||
|
export async function sendPortfolioDigest(): Promise<void> {
|
||||||
|
try {
|
||||||
|
const positions = await fetchAllPositions();
|
||||||
|
const open = positions.filter(p => p.netqty !== 0);
|
||||||
|
if (!open.length) {
|
||||||
|
await sendTelegram("📊 *Portfolio Digest* — No open positions");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const totalUnrealised = open.reduce((s, p) => s + p.unrealisedPnl, 0);
|
||||||
|
const totalRealised = open.reduce((s, p) => s + p.realisedPnl, 0);
|
||||||
|
const bookedRow = db.prepare(
|
||||||
|
"SELECT COALESCE(SUM(realised_pnl),0) as r FROM positions WHERE netqty=0 AND realised_pnl!=0"
|
||||||
|
).get() as { r: number };
|
||||||
|
const booked = bookedRow.r;
|
||||||
|
const totalPnl = totalUnrealised + totalRealised + booked;
|
||||||
|
|
||||||
|
const sorted = [...open].sort((a, b) => b.totalPnl - a.totalPnl);
|
||||||
|
const best = sorted[0];
|
||||||
|
const worst = sorted[sorted.length - 1];
|
||||||
|
|
||||||
|
const allBands = db.prepare("SELECT * FROM band_state").all() as any[];
|
||||||
|
const bandMap = Object.fromEntries(allBands.map((b: any) => [b.position_key, b]));
|
||||||
|
|
||||||
|
const expiryWarnings: string[] = [];
|
||||||
|
const nearAlert: string[] = [];
|
||||||
|
const now = new Date();
|
||||||
|
|
||||||
|
for (const p of open) {
|
||||||
|
const m = p.tradingsymbol.match(/^([A-Z]+)(\d{2})(\d{1})(\d{2})(\d+)(CE|PE)$/);
|
||||||
|
if (m) {
|
||||||
|
const exp = new Date(`20${m[2]}-${m[3].padStart(2,"0")}-${m[4]}`);
|
||||||
|
const dte = Math.ceil((exp.getTime() - now.getTime()) / 86400000);
|
||||||
|
if (dte <= 2 && dte >= 0)
|
||||||
|
expiryWarnings.push(`⚠️ ${p.tradingsymbol} — *${dte === 0 ? "expires TODAY" : dte + "d to expiry"}*`);
|
||||||
|
}
|
||||||
|
const band = bandMap[p.key];
|
||||||
|
if (band && Math.abs(band.anchor_pnl) >= 100) {
|
||||||
|
const pctMoved = Math.abs((p.totalPnl - band.anchor_pnl) / Math.abs(band.anchor_pnl) * 100);
|
||||||
|
const pctLeft = 5 - pctMoved;
|
||||||
|
if (pctLeft < 1.5 && pctLeft > 0)
|
||||||
|
nearAlert.push(`🔔 ${p.tradingsymbol} — ${pctMoved.toFixed(1)}% moved, *${pctLeft.toFixed(1)}% to alert*`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const longs = open.filter(p => p.netqty > 0).length;
|
||||||
|
const shorts = open.filter(p => p.netqty < 0).length;
|
||||||
|
const direction = shorts > longs ? "📉 Net Short Premium" : "📈 Net Long Premium";
|
||||||
|
const f = (n: number) => (n >= 0 ? "+₹" : "-₹") + Math.abs(n).toLocaleString("en-IN", { maximumFractionDigits: 0 });
|
||||||
|
const ist = new Date().toLocaleTimeString("en-IN", { timeZone: "Asia/Kolkata", hour12: false });
|
||||||
|
|
||||||
|
const lines: string[] = [
|
||||||
|
`📊 *Portfolio Digest — ${ist} IST*`,
|
||||||
|
"",
|
||||||
|
"*P\\&L Summary*",
|
||||||
|
`Unrealised : ${f(totalUnrealised)}`,
|
||||||
|
`Realised : ${f(totalRealised)}`,
|
||||||
|
`Booked : ${f(booked)}`,
|
||||||
|
`*Total : ${f(totalPnl)}*`,
|
||||||
|
"",
|
||||||
|
`*Positions* — ${open.length} open | ${direction}`,
|
||||||
|
`🏆 Best : ${best.tradingsymbol} ${f(best.totalPnl)}`,
|
||||||
|
`💀 Worst: ${worst.tradingsymbol} ${f(worst.totalPnl)}`,
|
||||||
|
];
|
||||||
|
if (expiryWarnings.length) { lines.push("", "*Expiry Warnings*"); lines.push(...expiryWarnings); }
|
||||||
|
if (nearAlert.length) { lines.push("", "*Near Alert Threshold*"); lines.push(...nearAlert); }
|
||||||
|
|
||||||
|
await sendTelegram(lines.join("\n"));
|
||||||
|
} catch (e) {
|
||||||
|
console.error("[digest] Error:", e instanceof Error ? e.message : e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue