v.13
This commit is contained in:
parent
aa472d16a2
commit
a824cabd7e
75
api/alerts.php
Normal file
75
api/alerts.php
Normal 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);
|
||||||
@ -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 = [
|
||||||
|
|||||||
@ -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();
|
||||||
});
|
});
|
||||||
19
db/migrations/001_create_price_history_table.php
Normal file
19
db/migrations/001_create_price_history_table.php
Normal 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());
|
||||||
|
}
|
||||||
27
index.php
27
index.php
@ -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],
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user