โ ะะฐะทะฐะด'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;