This commit is contained in:
Flatlogic Bot 2025-11-15 10:10:41 +00:00
parent aa472d16a2
commit a824cabd7e
5 changed files with 194 additions and 36 deletions

75
api/alerts.php Normal file
View File

@ -0,0 +1,75 @@
<?php
header('Content-Type: application/json');
require_once __DIR__ . '/../db/config.php';
// --- Configuration ---
$symbol = $_GET['symbol'] ?? 'BTCUSDT';
$time_window_minutes = 30;
$crash_threshold = -20.0; // -20%
$pump_threshold = 100.0; // +100%
// --- Response Structure ---
$response = [
'status' => '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);

View File

@ -2,7 +2,8 @@
header('Content-Type: application/json'); header('Content-Type: application/json');
// --- Configuration --- // --- 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; $api_url = "https://api.bitget.com/api/v2/spot/market/tickers?symbol=" . $symbol;
// --- cURL Execution --- // --- cURL Execution ---
@ -54,6 +55,22 @@ if (!$ticker_data) {
exit; 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 --- // --- Sanitize and Structure Output ---
// We select and sanitize the fields we want to send to the frontend. // We select and sanitize the fields we want to send to the frontend.
$output = [ $output = [

View File

@ -1,23 +1,22 @@
document.addEventListener('DOMContentLoaded', function () { document.addEventListener('DOMContentLoaded', function () {
const POLLING_INTERVAL = 3000; // 3 seconds const POLLING_INTERVAL = 3000; // 3 seconds
const liveRow = document.getElementById('live-crypto-row'); // --- Configuration for Live Tickers ---
if (!liveRow) { const liveTickers = [
console.error('Live crypto row with ID #live-crypto-row not found.'); { symbol: 'BTCUSDT', elementId: 'live-crypto-row-btc', lastPrice: 0 },
return; { symbol: 'ETHUSDT', elementId: 'live-crypto-row-eth', lastPrice: 0 }
} ];
// Select cells to update // --- Generic Fetch and Update Functions ---
const priceCell = liveRow.querySelector('.price');
const changeCell = liveRow.querySelector('.change');
const symbolCell = liveRow.querySelector('.symbol');
const exchangeCell = liveRow.querySelector('.exchange');
let lastPrice = 0; /**
* Fetches the latest price for a given symbol and updates its corresponding row.
async function fetchLivePrice() { * @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 { try {
const response = await fetch('api/ticker.php'); const response = await fetch(`api/ticker.php?symbol=${ticker.symbol}`);
if (!response.ok) { if (!response.ok) {
throw new Error(`HTTP error! Status: ${response.status}`); throw new Error(`HTTP error! Status: ${response.status}`);
} }
@ -27,23 +26,39 @@ document.addEventListener('DOMContentLoaded', function () {
throw new Error(`API Error: ${data.error}`); throw new Error(`API Error: ${data.error}`);
} }
updateRow(data); updateRow(data, ticker, rowElement);
} catch (error) { } catch (error) {
console.error('Failed to fetch live price:', error); console.error(`Failed to fetch live price for ${ticker.symbol}:`, error);
// Optionally, display an error state in the UI // Optionally, display an error state in the specific row
priceCell.textContent = 'Error'; const priceCell = rowElement.querySelector('.price');
changeCell.textContent = '--'; 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 currentPrice = parseFloat(data.price);
const priceChange = currentPrice - lastPrice; const priceChange = currentPrice - ticker.lastPrice;
// Update text content // Update text content
priceCell.textContent = `$${currentPrice.toLocaleString('en-US', { minimumFractionDigits: 2, maximumFractionDigits: 2 })}`; 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; exchangeCell.textContent = data.exchange;
// Update 24h change // Update 24h change
@ -53,16 +68,35 @@ document.addEventListener('DOMContentLoaded', function () {
changeCell.classList.toggle('text-danger', changePercent < 0); changeCell.classList.toggle('text-danger', changePercent < 0);
// Visual flash on price change // Visual flash on price change
if (lastPrice !== 0 && priceChange !== 0) { if (ticker.lastPrice !== 0 && priceChange !== 0) {
const flashClass = priceChange > 0 ? 'flash-success' : 'flash-danger'; const flashClass = priceChange > 0 ? 'flash-success' : 'flash-danger';
priceCell.classList.add(flashClass); priceCell.classList.add(flashClass);
setTimeout(() => priceCell.classList.remove(flashClass), 750); setTimeout(() => priceCell.classList.remove(flashClass), 750);
} }
lastPrice = currentPrice; ticker.lastPrice = currentPrice; // Update the last price for this specific ticker
} }
// Initial fetch and start polling // --- Initialization ---
fetchLivePrice();
setInterval(fetchLivePrice, POLLING_INTERVAL); /**
}); * 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();
});

View File

@ -0,0 +1,19 @@
<?php
require_once __DIR__ . '/../config.php';
try {
$pdo = db();
$sql = "
CREATE TABLE IF NOT EXISTS `price_history` (
`id` INT AUTO_INCREMENT PRIMARY KEY,
`symbol` VARCHAR(20) NOT NULL,
`price` DECIMAL(18, 8) NOT NULL,
`timestamp` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
INDEX `symbol_timestamp_idx` (`symbol`, `timestamp`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
";
$pdo->exec($sql);
echo "Table 'price_history' created successfully or already exists." . PHP_EOL;
} catch (PDOException $e) {
die("DB ERROR: " . $e->getMessage());
}

View File

@ -54,20 +54,34 @@
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
<!-- Live Row (Updated by JavaScript) --> <!-- Live Row 1 (Updated by JavaScript) -->
<tr id="live-crypto-row"> <tr id="live-crypto-row-btc">
<td class="fw-medium text-muted exchange">Bitget</td> <td class="fw-medium text-muted exchange">Bitget</td>
<td> <td>
<div class="d-flex align-items-center"> <div class="d-flex align-items-center">
<img src="https://via.placeholder.com/24/0d6efd/FFFFFF?Text=B" class="symbol-logo rounded-circle" alt=""> <img src="https://via.placeholder.com/24/0d6efd/FFFFFF?Text=B" class="symbol-logo rounded-circle" alt="BTC">
<span class="fw-bold symbol">BTC/USDT</span> <span class="fw-bold symbol">BTC/USDT</span>
</div> </div>
</td> </td>
<td class="fw-bold fs-5 price">$0.00</td> <td class="fw-bold fs-5 price">$0.00</td>
<td class="text-center"> <td class="text-center">
<span class="fw-bold change"> <span class="fw-bold change">--%</span>
--% </td>
</span> <td class="text-end signal">-</td>
</tr>
<!-- Live Row 2 (Updated by JavaScript) -->
<tr id="live-crypto-row-eth">
<td class="fw-medium text-muted exchange">Bitget</td>
<td>
<div class="d-flex align-items-center">
<img src="https://via.placeholder.com/24/6f42c1/FFFFFF?Text=E" class="symbol-logo rounded-circle" alt="ETH">
<span class="fw-bold symbol">ETH/USDT</span>
</div>
</td>
<td class="fw-bold fs-5 price">$0.00</td>
<td class="text-center">
<span class="fw-bold change">--%</span>
</td> </td>
<td class="text-end signal">-</td> <td class="text-end signal">-</td>
</tr> </tr>
@ -75,7 +89,6 @@
<?php <?php
// Static mock data for UI variety // Static mock data for UI variety
$mock_data = [ $mock_data = [
['exchange' => 'Bitget', 'symbol' => 'ETH/USDT', 'price' => '3,789.12', 'change' => -21.8, 'signal' => 'Crash Alert'],
['exchange' => 'WEEX', 'symbol' => 'PEPE/USDT', 'price' => '0.00001234', 'change' => 255.1, 'signal' => 'Pump Alert'], ['exchange' => 'WEEX', 'symbol' => 'PEPE/USDT', 'price' => '0.00001234', 'change' => 255.1, 'signal' => 'Pump Alert'],
['exchange' => 'KuCoin', 'symbol' => 'SOL/USDT', 'price' => '165.80', 'change' => -1.2, 'signal' => 'Bearish Reversal'], ['exchange' => 'KuCoin', 'symbol' => 'SOL/USDT', 'price' => '165.80', 'change' => -1.2, 'signal' => 'Bearish Reversal'],
['exchange' => 'KCEX', 'symbol' => 'DOGE/USDT', 'price' => '0.1588', 'change' => 5.6, 'signal' => null], ['exchange' => 'KCEX', 'symbol' => 'DOGE/USDT', 'price' => '0.1588', 'change' => 5.6, 'signal' => null],