diff --git a/api/alerts.php b/api/alerts.php new file mode 100644 index 0000000..6f9e6cc --- /dev/null +++ b/api/alerts.php @@ -0,0 +1,75 @@ + 'Nominal', + 'symbol' => $symbol, + 'change_percent' => 0, + 'details' => 'No significant price change detected.' +]; + +try { + $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']; + + // 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; + } + + // 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."; + } + +} catch (PDOException $e) { + http_response_code(500); + $response['status'] = 'Error'; + $response['details'] = 'Database error: ' . $e->getMessage(); +} + +echo json_encode($response); diff --git a/api/ticker.php b/api/ticker.php index 4a0350f..088a2f6 100644 --- a/api/ticker.php +++ b/api/ticker.php @@ -2,7 +2,8 @@ header('Content-Type: application/json'); // --- Configuration --- -$symbol = 'BTCUSDT'; // Bitget symbol for BTC/USDT spot market +// Default to BTCUSDT if no symbol is provided in the URL query string. +$symbol = isset($_GET['symbol']) ? strtoupper($_GET['symbol']) : 'BTCUSDT'; $api_url = "https://api.bitget.com/api/v2/spot/market/tickers?symbol=" . $symbol; // --- cURL Execution --- @@ -54,6 +55,22 @@ if (!$ticker_data) { exit; } +// --- Log to Database --- +try { + require_once __DIR__ . '/../db/config.php'; + $price_to_log = $ticker_data['lastPr'] ?? null; + if ($price_to_log) { + $pdo = db(); + $stmt = $pdo->prepare( + "INSERT INTO price_history (symbol, price) VALUES (:symbol, :price)" + ); + $stmt->execute(['symbol' => $symbol, 'price' => $price_to_log]); + } +} catch (Exception $e) { + // If DB fails, log error but don't crash the API endpoint + error_log('DB logging failed in ticker.php: ' . $e->getMessage()); +} + // --- Sanitize and Structure Output --- // We select and sanitize the fields we want to send to the frontend. $output = [ diff --git a/assets/js/main.js b/assets/js/main.js index 9a5de2f..c4f9d7d 100644 --- a/assets/js/main.js +++ b/assets/js/main.js @@ -1,23 +1,22 @@ document.addEventListener('DOMContentLoaded', function () { const POLLING_INTERVAL = 3000; // 3 seconds - const liveRow = document.getElementById('live-crypto-row'); - if (!liveRow) { - console.error('Live crypto row with ID #live-crypto-row not found.'); - return; - } + // --- Configuration for Live Tickers --- + const liveTickers = [ + { symbol: 'BTCUSDT', elementId: 'live-crypto-row-btc', lastPrice: 0 }, + { symbol: 'ETHUSDT', elementId: 'live-crypto-row-eth', lastPrice: 0 } + ]; - // Select cells to update - const priceCell = liveRow.querySelector('.price'); - const changeCell = liveRow.querySelector('.change'); - const symbolCell = liveRow.querySelector('.symbol'); - const exchangeCell = liveRow.querySelector('.exchange'); + // --- Generic Fetch and Update Functions --- - let lastPrice = 0; - - async function fetchLivePrice() { + /** + * Fetches the latest price for a given symbol and updates its corresponding row. + * @param {object} ticker - The ticker object from the liveTickers array. + * @param {HTMLElement} rowElement - The table row element to update. + */ + async function fetchAndupdate(ticker, rowElement) { try { - const response = await fetch('api/ticker.php'); + const response = await fetch(`api/ticker.php?symbol=${ticker.symbol}`); if (!response.ok) { throw new Error(`HTTP error! Status: ${response.status}`); } @@ -27,23 +26,39 @@ document.addEventListener('DOMContentLoaded', function () { throw new Error(`API Error: ${data.error}`); } - updateRow(data); + updateRow(data, ticker, rowElement); } catch (error) { - console.error('Failed to fetch live price:', error); - // Optionally, display an error state in the UI - priceCell.textContent = 'Error'; - changeCell.textContent = '--'; + console.error(`Failed to fetch live price for ${ticker.symbol}:`, error); + // Optionally, display an error state in the specific row + const priceCell = rowElement.querySelector('.price'); + if (priceCell) priceCell.textContent = 'Error'; } } - function updateRow(data) { + /** + * Updates the DOM of a specific row with new data. + * @param {object} data - The data object from the API. + * @param {object} ticker - The ticker object being updated. + * @param {HTMLElement} rowElement - The table row element. + */ + function updateRow(data, ticker, rowElement) { + const priceCell = rowElement.querySelector('.price'); + const changeCell = rowElement.querySelector('.change'); + const symbolCell = rowElement.querySelector('.symbol'); + const exchangeCell = rowElement.querySelector('.exchange'); + + if (!priceCell || !changeCell || !symbolCell || !exchangeCell) { + console.error(`One or more required elements not found in row for ${ticker.symbol}`); + return; + } + const currentPrice = parseFloat(data.price); - const priceChange = currentPrice - lastPrice; + const priceChange = currentPrice - ticker.lastPrice; // Update text content priceCell.textContent = `$${currentPrice.toLocaleString('en-US', { minimumFractionDigits: 2, maximumFractionDigits: 2 })}`; - symbolCell.textContent = data.symbol.replace('_SPBL', ''); // Clean up symbol name + symbolCell.textContent = data.symbol.replace('_SPBL', ''); exchangeCell.textContent = data.exchange; // Update 24h change @@ -53,16 +68,35 @@ document.addEventListener('DOMContentLoaded', function () { changeCell.classList.toggle('text-danger', changePercent < 0); // Visual flash on price change - if (lastPrice !== 0 && priceChange !== 0) { + if (ticker.lastPrice !== 0 && priceChange !== 0) { const flashClass = priceChange > 0 ? 'flash-success' : 'flash-danger'; priceCell.classList.add(flashClass); setTimeout(() => priceCell.classList.remove(flashClass), 750); } - lastPrice = currentPrice; + ticker.lastPrice = currentPrice; // Update the last price for this specific ticker } - // Initial fetch and start polling - fetchLivePrice(); - setInterval(fetchLivePrice, POLLING_INTERVAL); -}); \ No newline at end of file + // --- Initialization --- + + /** + * Initializes the polling for all configured tickers. + */ + function initializeLiveTickers() { + liveTickers.forEach(ticker => { + const rowElement = document.getElementById(ticker.elementId); + if (!rowElement) { + console.error(`Element with ID #${ticker.elementId} not found for symbol ${ticker.symbol}.`); + return; // Skip this ticker if its row doesn't exist + } + + // Initial fetch + fetchAndupdate(ticker, rowElement); + + // Start polling every X seconds + setInterval(() => fetchAndupdate(ticker, rowElement), POLLING_INTERVAL); + }); + } + + initializeLiveTickers(); +}); diff --git a/db/migrations/001_create_price_history_table.php b/db/migrations/001_create_price_history_table.php new file mode 100644 index 0000000..e3e744d --- /dev/null +++ b/db/migrations/001_create_price_history_table.php @@ -0,0 +1,19 @@ +exec($sql); + echo "Table 'price_history' created successfully or already exists." . PHP_EOL; +} catch (PDOException $e) { + die("DB ERROR: " . $e->getMessage()); +} diff --git a/index.php b/index.php index 386735c..6e003f6 100644 --- a/index.php +++ b/index.php @@ -54,20 +54,34 @@
- -