โ† ะะฐะทะฐะด
'use strict'; const express = require('express'); const prisma = require('../services/db'); const { checkApiKey } = require('../middleware/auth'); const logger = require('../utils/logger'); const router = express.Router(); // Valid metrics that can be monitored const VALID_METRICS = [ 'iv_rank', // IV Rank 0-100% 'iv_percentile', // IV Percentile 0-100% 'put_call_ratio', // P/C Ratio (volume) 'put_call_ratio_oi', // P/C Ratio (OI) 'unusual_volume', // V/OI ratio threshold 'max_pain_distance', // Distance from max pain (%) 'atm_iv', // ATM Implied Volatility (%) 'oi_change', // OI change % (24h) 'iv_skew', // 25-delta skew value 'spot_price', // Underlying spot price ]; const VALID_CONDITIONS = ['gt', 'lt', 'gte', 'lte', 'eq', 'crosses_above', 'crosses_below']; const VALID_UNDERLYINGS = ['BTC', 'ETH', 'SOL', 'DOGE', 'XRP', 'BNB', 'ALL']; // โ”€โ”€โ”€ GET /api/custom-alerts โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ // List all custom alerts with recent trigger count router.get('/api/custom-alerts', checkApiKey, async (req, res) => { try { const alerts = await prisma.customAlert.findMany({ orderBy: { createdAt: 'desc' }, include: { triggers: { orderBy: { createdAt: 'desc' }, take: 5, }, }, }); res.json({ success: true, data: alerts.map(a => ({ ...a, triggerCount: a.triggers.length, lastTrigger: a.triggers[0] || null, })), }); } catch (err) { logger.error(`GET /api/custom-alerts error: ${err.message}`); res.status(500).json({ success: false, error: err.message }); } }); // โ”€โ”€โ”€ GET /api/custom-alerts/metrics โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ // Return available metrics and conditions for the frontend form router.get('/api/custom-alerts/metrics', checkApiKey, (req, res) => { const metrics = [ { id: 'iv_rank', label: 'IV Rank', unit: '%', description: 'IV Rank relative to 30-day history (0-100%)' }, { id: 'iv_percentile', label: 'IV Percentile', unit: '%', description: 'IV Percentile (0-100%)' }, { id: 'put_call_ratio', label: 'Put/Call Ratio (Volume)', unit: '', description: 'Volume-based put/call ratio' }, { id: 'put_call_ratio_oi', label: 'Put/Call Ratio (OI)', unit: '', description: 'Open interest-based put/call ratio' }, { id: 'unusual_volume', label: 'Unusual Volume (V/OI)', unit: 'x', description: 'Volume / Open Interest ratio' }, { id: 'max_pain_distance', label: 'Max Pain Distance', unit: '%', description: 'Spot distance from max pain level' }, { id: 'atm_iv', label: 'ATM IV', unit: '%', description: 'At-the-money implied volatility' }, { id: 'oi_change', label: 'OI Change (24h)', unit: '%', description: 'Open interest change in last 24h' }, { id: 'iv_skew', label: 'IV Skew (25-delta)', unit: '', description: '25-delta put IV minus call IV' }, { id: 'spot_price', label: 'Spot Price', unit: '$', description: 'Current spot price of underlying' }, ]; res.json({ success: true, data: { metrics, conditions: VALID_CONDITIONS, underlyings: VALID_UNDERLYINGS, }, }); }); // โ”€โ”€โ”€ POST /api/custom-alerts โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ // Create a new custom alert router.post('/api/custom-alerts', checkApiKey, async (req, res) => { try { const { name, underlying, metric, condition, threshold, telegramEnabled, pushEnabled, cooldownMinutes } = req.body; // Validation if (!name || !underlying || !metric || !condition || threshold === undefined) { return res.status(400).json({ success: false, error: 'Missing required fields: name, underlying, metric, condition, threshold' }); } if (!VALID_METRICS.includes(metric)) { return res.status(400).json({ success: false, error: `Invalid metric. Valid: ${VALID_METRICS.join(', ')}` }); } if (!VALID_CONDITIONS.includes(condition)) { return res.status(400).json({ success: false, error: `Invalid condition. Valid: ${VALID_CONDITIONS.join(', ')}` }); } if (!VALID_UNDERLYINGS.includes(underlying.toUpperCase())) { return res.status(400).json({ success: false, error: `Invalid underlying. Valid: ${VALID_UNDERLYINGS.join(', ')}` }); } if (typeof threshold !== 'number' || isNaN(threshold)) { return res.status(400).json({ success: false, error: 'Threshold must be a number' }); } // Max 20 alerts const count = await prisma.customAlert.count(); if (count >= 20) { return res.status(400).json({ success: false, error: 'Maximum 20 alerts reached. Delete some first.' }); } const alert = await prisma.customAlert.create({ data: { name: name.slice(0, 100), underlying: underlying.toUpperCase(), metric, condition, threshold: parseFloat(threshold), telegramEnabled: telegramEnabled !== false, pushEnabled: pushEnabled !== false, cooldownMinutes: parseInt(cooldownMinutes) || 30, }, }); logger.info(`Custom alert created: #${alert.id} "${alert.name}" (${alert.underlying} ${alert.metric} ${alert.condition} ${alert.threshold})`); res.json({ success: true, data: alert }); } catch (err) { logger.error(`POST /api/custom-alerts error: ${err.message}`); res.status(500).json({ success: false, error: err.message }); } }); // โ”€โ”€โ”€ PUT /api/custom-alerts/:id โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ // Update an existing alert router.put('/api/custom-alerts/:id', checkApiKey, async (req, res) => { try { const id = parseInt(req.params.id); const { name, underlying, metric, condition, threshold, enabled, telegramEnabled, pushEnabled, cooldownMinutes } = req.body; const existing = await prisma.customAlert.findUnique({ where: { id } }); if (!existing) { return res.status(404).json({ success: false, error: 'Alert not found' }); } // Validate only provided fields if (metric && !VALID_METRICS.includes(metric)) { return res.status(400).json({ success: false, error: `Invalid metric` }); } if (condition && !VALID_CONDITIONS.includes(condition)) { return res.status(400).json({ success: false, error: `Invalid condition` }); } if (underlying && !VALID_UNDERLYINGS.includes(underlying.toUpperCase())) { return res.status(400).json({ success: false, error: `Invalid underlying` }); } const updateData = {}; if (name !== undefined) updateData.name = name.slice(0, 100); if (underlying !== undefined) updateData.underlying = underlying.toUpperCase(); if (metric !== undefined) updateData.metric = metric; if (condition !== undefined) updateData.condition = condition; if (threshold !== undefined) updateData.threshold = parseFloat(threshold); if (enabled !== undefined) updateData.enabled = Boolean(enabled); if (telegramEnabled !== undefined) updateData.telegramEnabled = Boolean(telegramEnabled); if (pushEnabled !== undefined) updateData.pushEnabled = Boolean(pushEnabled); if (cooldownMinutes !== undefined) updateData.cooldownMinutes = parseInt(cooldownMinutes) || 30; const alert = await prisma.customAlert.update({ where: { id }, data: updateData }); logger.info(`Custom alert updated: #${alert.id} "${alert.name}"`); res.json({ success: true, data: alert }); } catch (err) { logger.error(`PUT /api/custom-alerts/:id error: ${err.message}`); res.status(500).json({ success: false, error: err.message }); } }); // โ”€โ”€โ”€ POST /api/custom-alerts/:id/toggle โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ // Quick enable/disable toggle router.post('/api/custom-alerts/:id/toggle', checkApiKey, async (req, res) => { try { const id = parseInt(req.params.id); const existing = await prisma.customAlert.findUnique({ where: { id } }); if (!existing) { return res.status(404).json({ success: false, error: 'Alert not found' }); } const alert = await prisma.customAlert.update({ where: { id }, data: { enabled: !existing.enabled }, }); logger.info(`Custom alert #${alert.id} toggled: ${alert.enabled ? 'ON' : 'OFF'}`); res.json({ success: true, data: alert }); } catch (err) { logger.error(`POST /api/custom-alerts/:id/toggle error: ${err.message}`); res.status(500).json({ success: false, error: err.message }); } }); // โ”€โ”€โ”€ DELETE /api/custom-alerts/:id โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ router.delete('/api/custom-alerts/:id', checkApiKey, async (req, res) => { try { const id = parseInt(req.params.id); const existing = await prisma.customAlert.findUnique({ where: { id } }); if (!existing) { return res.status(404).json({ success: false, error: 'Alert not found' }); } await prisma.customAlert.delete({ where: { id } }); logger.info(`Custom alert deleted: #${id} "${existing.name}"`); res.json({ success: true, data: { deleted: id } }); } catch (err) { logger.error(`DELETE /api/custom-alerts/:id error: ${err.message}`); res.status(500).json({ success: false, error: err.message }); } }); // โ”€โ”€โ”€ GET /api/custom-alerts/:id/triggers โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ // History of trigger events for a specific alert router.get('/api/custom-alerts/:id/triggers', checkApiKey, async (req, res) => { try { const id = parseInt(req.params.id); const limit = Math.min(parseInt(req.query.limit) || 20, 100); const triggers = await prisma.alertTrigger.findMany({ where: { alertId: id }, orderBy: { createdAt: 'desc' }, take: limit, }); res.json({ success: true, data: triggers }); } catch (err) { logger.error(`GET /api/custom-alerts/:id/triggers error: ${err.message}`); res.status(500).json({ success: false, error: err.message }); } }); module.exports = router;