diff --git a/api/alerts.php b/api/alerts.php index 6f9e6cc..528154f 100644 --- a/api/alerts.php +++ b/api/alerts.php @@ -1,75 +1,77 @@ 'Nominal', - 'symbol' => $symbol, - 'change_percent' => 0, - 'details' => 'No significant price change detected.' -]; +function check_bearish_alerts($symbol) { + $alerts = []; -try { + // Fetch latest price $pdo = db(); - - // 1. Get the most recent price - $stmt_latest = $pdo->prepare("SELECT price FROM price_history WHERE symbol = :symbol ORDER BY timestamp DESC LIMIT 1"); - $stmt_latest->execute([':symbol' => $symbol]); - $latest_price_row = $stmt_latest->fetch(PDO::FETCH_ASSOC); - - if (!$latest_price_row) { - $response['details'] = 'No recent price data available for this symbol.'; - echo json_encode($response); - exit; - } - $latest_price = $latest_price_row['price']; + $stmt = $pdo->prepare("SELECT close FROM candlestick_data WHERE symbol = :symbol ORDER BY timestamp DESC LIMIT 1"); + $stmt->bindParam(':symbol', $symbol, PDO::PARAM_STR); + $stmt->execute(); + $current_price = $stmt->fetchColumn(); - // 2. Get the oldest price from the 30-minute window - $stmt_oldest = $pdo->prepare( - "SELECT price FROM price_history - WHERE symbol = :symbol AND timestamp >= NOW() - INTERVAL :minutes MINUTE - ORDER BY timestamp ASC LIMIT 1" - ); - $stmt_oldest->execute([':symbol' => $symbol, ':minutes' => $time_window_minutes]); - $oldest_price_row = $stmt_oldest->fetch(PDO::FETCH_ASSOC); - - if (!$oldest_price_row) { - $response['details'] = 'Not enough historical data in the last 30 minutes to calculate change.'; - echo json_encode($response); - exit; - } - $oldest_price = $oldest_price_row['price']; - - // 3. Calculate percentage change - if ($oldest_price > 0) { - $change_percent = (($latest_price - $oldest_price) / $oldest_price) * 100; - $response['change_percent'] = round($change_percent, 2); - } else { - $change_percent = 0; + if (!$current_price) { + return []; } - // 4. Determine status - if ($change_percent <= $crash_threshold) { - $response['status'] = 'Crash Alert'; - $response['details'] = "Price dropped by " . $response['change_percent'] . "% in the last $time_window_minutes minutes."; - } elseif ($change_percent >= $pump_threshold) { - $response['status'] = 'Pump Alert'; - $response['details'] = "Price surged by " . $response['change_percent'] . "% in the last $time_window_minutes minutes."; - } else { - $response['details'] = "Price change of " . $response['change_percent'] . "% is within normal limits."; + // Fetch analysis data + $sma = calculate_sma($symbol, 20); + $rsi = calculate_rsi($symbol, 14); + $macd_data = calculate_macd($symbol); + $patterns = calculate_patterns($symbol); + + // Condition 1: Price vs. SMA + $price_below_sma = $current_price < $sma; + + // Condition 2: RSI indicates weakening momentum (e.g., dropping from overbought) + // For simplicity, we'll check if RSI is above a certain threshold and not rising. + // A more complex implementation would track RSI changes over time. + $rsi_weakening = $rsi > 50; // Simplified: RSI is in the upper range, potential for reversal + + // Condition 3: MACD Bearish Crossover + $macd_bearish_crossover = false; + if ($macd_data && $macd_data['macd'] < $macd_data['signal']) { + // Check if it just crossed + // This requires historical data, for now, we check the current state + $macd_bearish_crossover = true; // Simplified: MACD is below signal line } -} catch (PDOException $e) { - http_response_code(500); - $response['status'] = 'Error'; - $response['details'] = 'Database error: ' . $e->getMessage(); + // Check original combo + if ($price_below_sma && $rsi_weakening && $macd_bearish_crossover) { + $alerts[] = [ + 'type' => 'Strong Bearish Signal', + 'indicator' => 'SMA, RSI, MACD Combination', + 'message' => 'Price dropped below 20-period SMA, RSI is high, and MACD shows a bearish state.' + ]; + } + + // Check for candlestick patterns + if (!empty($patterns)) { + foreach ($patterns as $pattern_name => $detected) { + if ($detected) { + $alerts[] = [ + 'type' => 'Strong Bearish Signal', + 'indicator' => 'Candlestick Pattern', + 'message' => 'Detected bearish pattern: ' . ucwords(str_replace('_', ' ', $pattern_name)) + ]; + } + } + } + + return $alerts; } -echo json_encode($response); +$alerts = check_bearish_alerts($symbol); + +echo json_encode([ + 'symbol' => $symbol, + 'alerts' => $alerts, + 'timestamp' => time() +]); \ No newline at end of file diff --git a/api/analysis.php b/api/analysis.php index fe41966..c58364a 100644 --- a/api/analysis.php +++ b/api/analysis.php @@ -3,10 +3,10 @@ header('Content-Type: application/json'); require_once __DIR__ . '/../db/config.php'; -// --- Parameters --- -// Example: /api/analysis.php?symbol=BTCUSDT&indicator=sma&period=20 +// Example: /api/analysis.php?symbol=BTCUSDT&indicators=sma,rsi,macd,patterns&period=20 $symbol = $_GET['symbol'] ?? 'BTCUSDT'; -$indicator = $_GET['indicator'] ?? 'sma'; +$indicators_str = $_GET['indicators'] ?? 'sma'; +$indicators = array_map('trim', explode(',', strtolower($indicators_str))); $period = (int)($_GET['period'] ?? 20); if ($period <= 0) { @@ -16,46 +16,219 @@ if ($period <= 0) { function calculate_sma($symbol, $period) { $pdo = db(); - - // Fetch the last 'period' number of closing prices for the given symbol, most recent first. - $stmt = $pdo->prepare( - "SELECT close FROM candlestick_data - WHERE symbol = :symbol - ORDER BY timestamp DESC - LIMIT :period" - ); - + $stmt = $pdo->prepare("SELECT close FROM candlestick_data WHERE symbol = :symbol ORDER BY timestamp DESC LIMIT :period"); $stmt->bindParam(':symbol', $symbol, PDO::PARAM_STR); $stmt->bindParam(':period', $period, PDO::PARAM_INT); - $stmt->execute(); - $closes = $stmt->fetchAll(PDO::FETCH_COLUMN); - if (count($closes) < $period) { - return null; // Not enough data to calculate SMA + return null; } - - $sum = array_sum($closes); - $sma = $sum / $period; - - return $sma; + return array_sum($closes) / $period; } -$result = null; -if (strtolower($indicator) === 'sma') { - $result = calculate_sma($symbol, $period); -} else { - echo json_encode(['error' => 'Invalid indicator specified.']); - exit; +function calculate_rsi($symbol, $period = 14) { + $pdo = db(); + $limit = $period + 1; + $stmt = $pdo->prepare("SELECT close FROM candlestick_data WHERE symbol = :symbol ORDER BY timestamp DESC LIMIT :limit"); + $stmt->bindParam(':symbol', $symbol, PDO::PARAM_STR); + $stmt->bindParam(':limit', $limit, PDO::PARAM_INT); + $stmt->execute(); + $closes = array_reverse($stmt->fetchAll(PDO::FETCH_COLUMN)); + if (count($closes) < $limit) { + return null; + } + $gains = 0; + $losses = 0; + for ($i = 1; $i < count($closes); $i++) { + $change = $closes[$i] - $closes[$i - 1]; + if ($change > 0) { + $gains += $change; + } else { + $losses += abs($change); + } + } + if ($period == 0) return 100; + $avg_gain = $gains / $period; + $avg_loss = $losses / $period; + if ($avg_loss == 0) { + return 100; + } + $rs = $avg_gain / $avg_loss; + return 100 - (100 / (1 + $rs)); +} + +function _calculate_ema_series($prices, $period) { + if (count($prices) < $period) { + return []; + } + $smoothing_factor = 2 / ($period + 1); + $emas = []; + $initial_prices = array_slice($prices, 0, $period); + $sma = array_sum($initial_prices) / count($initial_prices); + $emas[] = $sma; + $remaining_prices = array_slice($prices, $period); + $last_ema = $sma; + foreach ($remaining_prices as $price) { + $current_ema = ($price - $last_ema) * $smoothing_factor + $last_ema; + $emas[] = $current_ema; + $last_ema = $current_ema; + } + return $emas; +} + +function calculate_macd($symbol, $fast_period = 12, $slow_period = 26, $signal_period = 9) { + $pdo = db(); + $limit = $slow_period + $signal_period + 50; // Fetch extra data for stability + $stmt = $pdo->prepare("SELECT close FROM candlestick_data WHERE symbol = :symbol ORDER BY timestamp ASC LIMIT :limit"); + $stmt->bindParam(':symbol', $symbol, PDO::PARAM_STR); + $stmt->bindParam(':limit', $limit, PDO::PARAM_INT); + $stmt->execute(); + $closes = $stmt->fetchAll(PDO::FETCH_COLUMN); + + if (count($closes) < $slow_period + $signal_period) { + return null; + } + + $ema_fast_all = _calculate_ema_series($closes, $fast_period); + $ema_slow_all = _calculate_ema_series($closes, $slow_period); + + $ema_fast_aligned = array_slice($ema_fast_all, count($ema_fast_all) - count($ema_slow_all)); + + $macd_line_series = []; + for ($i = 0; $i < count($ema_slow_all); $i++) { + $macd_line_series[] = $ema_fast_aligned[$i] - $ema_slow_all[$i]; + } + + if (count($macd_line_series) < $signal_period) { + return null; + } + + $signal_line_series = _calculate_ema_series($macd_line_series, $signal_period); + + $macd_line = end($macd_line_series); + $signal_line = end($signal_line_series); + $histogram = $macd_line - $signal_line; + + return [ + 'macd' => $macd_line, + 'signal' => $signal_line, + 'histogram' => $histogram + ]; +} + +function calculate_patterns($symbol) { + $pdo = db(); + // Fetch last 30 candles for pattern recognition + $stmt = $pdo->prepare("SELECT open, high, low, close, timestamp FROM candlestick_data WHERE symbol = :symbol ORDER BY timestamp DESC LIMIT 30"); + $stmt->bindParam(':symbol', $symbol, PDO::PARAM_STR); + $stmt->execute(); + $candles = $stmt->fetchAll(PDO::FETCH_ASSOC); + + if (count($candles) < 5) { // Need at least a few candles for any pattern + return []; + } + + $patterns = []; + $latest_candle = $candles[0]; + $prev_candle = $candles[1]; + + // 1. Shooting Star + $body_size = abs($latest_candle['open'] - $latest_candle['close']); + $upper_wick = $latest_candle['high'] - max($latest_candle['open'], $latest_candle['close']); + $lower_wick = min($latest_candle['open'], $latest_candle['close']) - $latest_candle['low']; + if ($upper_wick > $body_size * 2 && $lower_wick < $body_size) { + $patterns['shooting_star'] = true; + } + + // 2. Bearish Engulfing + if ($prev_candle['close'] > $prev_candle['open'] && // Previous candle is bullish + $latest_candle['open'] > $prev_candle['close'] && // Current candle opens above previous close + $latest_candle['close'] < $prev_candle['open']) { // Current candle closes below previous open + $patterns['bearish_engulfing'] = true; + } + + // 3. Gravestone Doji + $is_doji = $body_size <= ($latest_candle['high'] - $latest_candle['low']) * 0.1; + if ($is_doji && $lower_wick < $body_size && $upper_wick > $body_size * 3) { + $patterns['gravestone_doji'] = true; + } + + // 4. Evening Star (3-candle pattern) + if (count($candles) >= 3) { + $c1 = $candles[2]; // Large bullish candle + $c2 = $candles[1]; // Small body candle (star) + $c3 = $candles[0]; // Bearish candle + if (($c1['close'] > $c1['open']) && (abs($c1['close'] - $c1['open']) > ($c1['high'] - $c1['low']) * 0.7) && // c1 is strong bullish + ($c2['open'] > $c1['close']) && (abs($c2['close'] - $c2['open']) < ($c2['high'] - $c2['low']) * 0.3) && // c2 is small body, gapped up + ($c3['close'] < $c3['open']) && ($c3['open'] < $c2['open']) && ($c3['close'] < $c1['close'])) { // c3 is bearish, closes below midpoint of c1 + $patterns['evening_star'] = true; + } + } + + // 5. Double Top (Structural) + // This is a more complex pattern requiring finding two distinct peaks. + // We'll look for two highs that are close in price, separated by a valley. + if(count($candles) >= 15) { + $recent_highs = array_map(function($c) { return $c['high']; }, array_slice($candles, 0, 15)); + $max_high = max($recent_highs); + $peaks = []; + foreach($candles as $i => $c) { + if ($i > 1 && $i < count($candles) -1 && $c['high'] >= $max_high * 0.98) { + if($candles[$i-1]['high'] < $c['high'] && $candles[$i+1]['high'] < $c['high']) { + $peaks[] = ['index' => $i, 'price' => $c['high']]; + } + } + } + + if (count($peaks) >= 2) { + // Check if the two most recent peaks form a double top + $peak1 = $peaks[1]; // More recent peak in time, but second in array + $peak2 = $peaks[0]; // Older peak + + $price_diff = abs($peak1['price'] - $peak2['price']) / $peak2['price']; + $time_diff = $peak1['index'] - $peak2['index']; + + // Peaks should be close in price, but not too close in time + if ($price_diff < 0.02 && $time_diff > 5) { + // Find the low point (valley) between the peaks + $valley_slice = array_slice($candles, $peak2['index'] + 1, $time_diff -1); + $valley_low = min(array_map(function($c){ return $c['low']; }, $valley_slice)); + + // Check if current price has broken below the valley low (neckline) + if($latest_candle['close'] < $valley_low) { + $patterns['double_top'] = true; + } + } + } + } + + return $patterns; +} + + +$results = []; +foreach ($indicators as $indicator) { + switch ($indicator) { + case 'sma': + $results['sma'] = calculate_sma($symbol, $period); + break; + case 'rsi': + $results['rsi'] = calculate_rsi($symbol, 14); + break; + case 'macd': + $results['macd'] = calculate_macd($symbol); + break; + case 'patterns': + $results['patterns'] = calculate_patterns($symbol); + break; + } } $response = [ 'symbol' => $symbol, - 'indicator' => 'SMA', - 'period' => $period, - 'value' => $result, + 'values' => $results, 'timestamp' => time() ]; -echo json_encode($response); +echo json_encode($response); \ No newline at end of file diff --git a/assets/js/main.js b/assets/js/main.js index f0a7037..b584f5f 100644 --- a/assets/js/main.js +++ b/assets/js/main.js @@ -1,5 +1,6 @@ document.addEventListener('DOMContentLoaded', function () { - const POLLING_INTERVAL = 3000; // 3 seconds + const POLLING_INTERVAL = 3000; // 3 seconds for price/analysis + const ALERT_POLLING_INTERVAL = 10000; // 10 seconds for alerts const liveTickers = [ { symbol: 'BTCUSDT', elementId: 'live-crypto-row-btc', lastPrice: 0 }, @@ -46,59 +47,102 @@ document.addEventListener('DOMContentLoaded', function () { async function fetchAnalysis(ticker, rowElement) { try { - const response = await fetch(`api/analysis.php?symbol=${ticker.symbol}&period=20`); + const response = await fetch(`api/analysis.php?symbol=${ticker.symbol}&indicators=sma,rsi,macd&period=20`); if (!response.ok) throw new Error(`HTTP error! Status: ${response.status}`); const data = await response.json(); if (data.error) throw new Error(`API Error: ${data.error}`); - updateAnalysis(data, rowElement, ticker.lastPrice); + updateAnalysisData(data, rowElement, ticker.lastPrice); } catch (error) { console.error(`Failed to fetch analysis for ${ticker.symbol}:`, error); - const smaCell = rowElement.querySelector('.sma'); - if (smaCell) smaCell.textContent = 'Error'; + rowElement.querySelector('.sma').textContent = 'Error'; + rowElement.querySelector('.rsi').textContent = 'Error'; } } - function updateAnalysis(data, rowElement, currentPrice) { + function updateAnalysisData(data, rowElement, currentPrice) { const smaCell = rowElement.querySelector('.sma'); const signalCell = rowElement.querySelector('.signal'); - if (!smaCell || !signalCell) return; + const rsiCell = rowElement.querySelector('.rsi'); + const macdCell = rowElement.querySelector('.macd'); + const macdSignalCell = rowElement.querySelector('.macd-signal'); + if (!smaCell || !signalCell || !rsiCell || !macdCell || !macdSignalCell) return; - const sma = parseFloat(data.sma); - smaCell.textContent = sma.toLocaleString('en-US', { minimumFractionDigits: 2, maximumFractionDigits: 2 }); + const sma = parseFloat(data.values.sma); + const rsi = parseFloat(data.values.rsi); + const macdData = data.values.macd; - if (currentPrice > sma) { - signalCell.innerHTML = 'Above SMA'; - } else if (currentPrice < sma) { - signalCell.innerHTML = 'Below SMA'; + if (!isNaN(sma)) { + smaCell.textContent = sma.toLocaleString('en-US', { minimumFractionDigits: 2, maximumFractionDigits: 2 }); + if (currentPrice > sma) { + signalCell.innerHTML = 'Above SMA'; + } else if (currentPrice < sma) { + signalCell.innerHTML = 'Below SMA'; + } else { + signalCell.innerHTML = 'At SMA'; + } } else { - signalCell.innerHTML = 'At SMA'; + smaCell.textContent = '-'; + signalCell.textContent = '-'; + } + + if (!isNaN(rsi)) { + rsiCell.textContent = rsi.toFixed(2); + } else { + rsiCell.textContent = '-'; + } + + if (macdData && typeof macdData.macd !== 'undefined' && typeof macdData.signal !== 'undefined') { + const macd = parseFloat(macdData.macd); + const macdSignal = parseFloat(macdData.signal); + if (!isNaN(macd)) { + macdCell.textContent = macd.toFixed(4); + } + if (!isNaN(macdSignal)) { + macdSignalCell.textContent = macdSignal.toFixed(4); + } + } else { + macdCell.textContent = '-'; + macdSignalCell.textContent = '-'; } } - async function fetchAlert(ticker, rowElement) { - try { - const response = await fetch(`api/alerts.php?symbol=${ticker.symbol}`); - if (!response.ok) throw new Error(`HTTP error! Status: ${response.status}`); - const data = await response.json(); - updateAlertStatus(data.status, rowElement); - } catch (error) { - console.error(`Failed to fetch alert for ${ticker.symbol}:`, error); + async function fetchAndDisplayAlerts() { + const alertsContainer = document.getElementById('alerts-container'); + if (!alertsContainer) return; + + let allAlerts = []; + + for (const ticker of liveTickers) { + try { + const response = await fetch(`api/alerts.php?symbol=${ticker.symbol}`); + if (!response.ok) throw new Error(`HTTP error! Status: ${response.status}`); + const data = await response.json(); + if (data.alerts && data.alerts.length > 0) { + data.alerts.forEach(alert => { + allAlerts.push({ symbol: data.symbol, ...alert }); + }); + } + } catch (error) { + console.error(`Failed to fetch alerts for ${ticker.symbol}:`, error); + } } - } - function updateAlertStatus(status, rowElement) { - const statusCell = rowElement.querySelector('.status'); - if (!statusCell) return; - - rowElement.classList.remove('flash-alert'); - statusCell.innerHTML = '-'; - - if (status === 'Crash Alert') { - statusCell.innerHTML = 'Crash Alert'; - rowElement.classList.add('flash-alert'); - } else if (status === 'Pump Alert') { - statusCell.innerHTML = 'Pump Alert'; - rowElement.classList.add('flash-alert'); + if (allAlerts.length > 0) { + alertsContainer.innerHTML = ''; // Clear previous alerts + allAlerts.forEach(alert => { + const alertEl = document.createElement('div'); + alertEl.className = 'alert alert-danger d-flex align-items-center'; + alertEl.innerHTML = ` + +
+ ${alert.type} on ${alert.symbol} + ${alert.message} +
+ `; + alertsContainer.appendChild(alertEl); + }); + } else { + alertsContainer.innerHTML = '

No alerts triggered yet. The system is monitoring the market.

'; } } @@ -114,12 +158,15 @@ document.addEventListener('DOMContentLoaded', function () { fetchAndupdate(ticker, rowElement).then(() => { fetchAnalysis(ticker, rowElement); }); - fetchAlert(ticker, rowElement); }; fetchAll(); setInterval(fetchAll, POLLING_INTERVAL); }); + + // Setup alert polling + fetchAndDisplayAlerts(); + setInterval(fetchAndDisplayAlerts, ALERT_POLLING_INTERVAL); } initializeLiveTickers(); diff --git a/index.php b/index.php index cb389df..39b8ed8 100644 --- a/index.php +++ b/index.php @@ -51,6 +51,9 @@ PRICE SMA (20m) SIGNAL + RSI (14m) + MACD + MACD Signal STATUS @@ -67,6 +70,9 @@ $0.00 - - + - + - + - - @@ -82,6 +88,9 @@ $0.00 - - + - + - + - - @@ -89,6 +98,16 @@ + +
+
+

Triggered Alerts

+
+
+

No alerts triggered yet. The system is monitoring the market.

+ +
+