From ffa84da26a89327ddde1031f82502d1858150b22 Mon Sep 17 00:00:00 2001 From: Flatlogic Bot Date: Sat, 15 Nov 2025 10:49:49 +0000 Subject: [PATCH] V.1.14 --- api/analysis.php | 61 +++++++ api/ticker.php | 160 ++++++++++-------- assets/css/custom.css | 12 +- assets/js/main.js | 124 ++++++++------ .../002_create_candlestick_data_table.php | 31 ++++ index.php | 70 ++------ 6 files changed, 283 insertions(+), 175 deletions(-) create mode 100644 api/analysis.php create mode 100644 db/migrations/002_create_candlestick_data_table.php diff --git a/api/analysis.php b/api/analysis.php new file mode 100644 index 0000000..fe41966 --- /dev/null +++ b/api/analysis.php @@ -0,0 +1,61 @@ + 'Period must be a positive integer.']); + exit; +} + +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->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 + } + + $sum = array_sum($closes); + $sma = $sum / $period; + + return $sma; +} + +$result = null; +if (strtolower($indicator) === 'sma') { + $result = calculate_sma($symbol, $period); +} else { + echo json_encode(['error' => 'Invalid indicator specified.']); + exit; +} + +$response = [ + 'symbol' => $symbol, + 'indicator' => 'SMA', + 'period' => $period, + 'value' => $result, + 'timestamp' => time() +]; + +echo json_encode($response); diff --git a/api/ticker.php b/api/ticker.php index 088a2f6..e1b8277 100644 --- a/api/ticker.php +++ b/api/ticker.php @@ -1,85 +1,109 @@ $api_url, - CURLOPT_RETURNTRANSFER => true, - CURLOPT_TIMEOUT => 10, - CURLOPT_HTTPHEADER => [ - 'Accept: application/json' - ], - // It's good practice to set a user-agent - CURLOPT_USERAGENT => 'FlatlogicMarketDetector/1.0' -]); +// --- Data Fetching --- +function fetch_candlestick_data($symbol, $interval) { + $api_url = sprintf( + "https://api.binance.com/api/v3/klines?symbol=%s&interval=%s&limit=5", + $symbol, $interval + ); -$response = curl_exec($ch); -$http_code = curl_getinfo($ch, CURLINFO_HTTP_CODE); -$error = curl_error($ch); -curl_close($ch); + $ch = curl_init(); + curl_setopt_array($ch, [ + CURLOPT_URL => $api_url, + CURLOPT_RETURNTRANSFER => true, + CURLOPT_TIMEOUT => 10, + CURLOPT_USERAGENT => 'FlatlogicMarketDetector/1.0' + ]); -// --- Response Handling --- -if ($error) { - http_response_code(500); - echo json_encode(['error' => 'cURL Error: ' . $error]); - exit; + $response = curl_exec($ch); + curl_close($ch); + return json_decode($response, true); } -if ($http_code !== 200) { - http_response_code($http_code); - echo json_encode(['error' => 'Bitget API returned non-200 status', 'details' => json_decode($response)]); - exit; +// --- Database Logging --- +function log_candlestick_data($pdo, $symbol, $exchange, $interval, $kline) { + $sql = <<prepare($sql); + + $stmt->execute([ + ':symbol' => $symbol, + ':exchange' => $exchange, + ':interval_time' => $interval, + ':open_time' => $kline[0], + ':open_price' => $kline[1], + ':high_price' => $kline[2], + ':low_price' => $kline[3], + ':close_price' => $kline[4], + ':volume' => $kline[5], + ':close_time' => $kline[6], + ':quote_asset_volume' => $kline[7], + ':number_of_trades' => $kline[8], + ':taker_buy_base_asset_volume' => $kline[9], + ':taker_buy_quote_asset_volume' => $kline[10], + ]); } -$data = json_decode($response, true); +// --- Main Execution --- +$latest_tickers = []; -// --- Data Validation & Output --- -if (json_last_error() !== JSON_ERROR_NONE || empty($data['data'])) { - http_response_code(500); - echo json_encode(['error' => 'Failed to parse JSON response or data is empty']); - exit; -} - -// Extract the first ticker object from the response data array -$ticker_data = $data['data'][0] ?? null; - -if (!$ticker_data) { - http_response_code(404); - echo json_encode(['error' => 'Ticker data for symbol not found in API response']); - 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]); + $pdo = db(); + foreach ($symbols as $symbol) { + $klines = fetch_candlestick_data($symbol, $interval); + + if (empty($klines) || !is_array($klines)) { + continue; // Skip this symbol if data fetching fails + } + + foreach ($klines as $kline) { + log_candlestick_data($pdo, $symbol, $exchange, $interval, $kline); + } + + // For the frontend, provide the most recent ticker data + $latest_kline = end($klines); + $latest_tickers[] = [ + 'exchange' => $exchange, + 'symbol' => $symbol, + 'price' => $latest_kline[4], // Close price + 'change_24h_percent' => 0, // Placeholder, as kline API doesn't provide this directly + 'signal' => '-' + ]; } } 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()); + http_response_code(500); + // Log error and exit gracefully + error_log('Ticker script failed: ' . $e->getMessage()); + echo json_encode(['error' => 'An internal error occurred.']); + exit; } -// --- Sanitize and Structure Output --- -// We select and sanitize the fields we want to send to the frontend. -$output = [ - 'exchange' => 'Bitget', - 'symbol' => $ticker_data['symbol'] ?? 'N/A', - 'price' => $ticker_data['lastPr'] ?? '0.00', - // Bitget provides the 24h change as a decimal, e.g., 0.025 for +2.5% - 'change_24h_percent' => isset($ticker_data['priceChangePercent']) ? (float)$ticker_data['priceChangePercent'] * 100 : 0, - 'signal' => '-' // Placeholder for future signal detection -]; - -echo json_encode($output); +// --- Output for Frontend --- +// This part is for the main dashboard display, not the alerts API. +if (isset($_GET['symbol'])) { + $symbol_to_find = strtoupper($_GET['symbol']); + foreach ($latest_tickers as $ticker) { + if ($ticker['symbol'] === $symbol_to_find) { + echo json_encode($ticker); + exit; + } + } + // Fallback if the requested symbol wasn't processed + echo json_encode(['error' => 'Data for symbol not found.']); +} else { + // If no symbol is specified, do not return anything for now. + // The frontend will request each symbol individually. + echo json_encode(['status' => 'OK', 'message' => 'Data processed.']); +} \ No newline at end of file diff --git a/assets/css/custom.css b/assets/css/custom.css index 0318f2f..4c55b6a 100644 --- a/assets/css/custom.css +++ b/assets/css/custom.css @@ -62,4 +62,14 @@ body { .flash-danger { animation: flash-danger-anim 0.75s ease-out; -} \ No newline at end of file +} + +/* Alert flash animation */ +@keyframes flash-alert-anim { + 0%, 100% { background-color: transparent; } + 50% { background-color: rgba(220, 53, 69, 0.25); } +} + +.flash-alert { + animation: flash-alert-anim 1.5s infinite; +} diff --git a/assets/js/main.js b/assets/js/main.js index c4f9d7d..f0a7037 100644 --- a/assets/js/main.js +++ b/assets/js/main.js @@ -1,100 +1,124 @@ document.addEventListener('DOMContentLoaded', function () { const POLLING_INTERVAL = 3000; // 3 seconds - // --- Configuration for Live Tickers --- const liveTickers = [ { symbol: 'BTCUSDT', elementId: 'live-crypto-row-btc', lastPrice: 0 }, { symbol: 'ETHUSDT', elementId: 'live-crypto-row-eth', lastPrice: 0 } ]; - // --- Generic Fetch and Update Functions --- - - /** - * 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?symbol=${ticker.symbol}`); - if (!response.ok) { - throw new Error(`HTTP error! Status: ${response.status}`); - } + 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}`); - } - + if (data.error) throw new Error(`API Error: ${data.error}`); updateRow(data, ticker, rowElement); - } catch (error) { 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'; } } - /** - * 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) { + if (!priceCell || !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 - ticker.lastPrice; - - // 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', ''); exchangeCell.textContent = data.exchange; - // Update 24h change - const changePercent = parseFloat(data.change_24h_percent); - changeCell.textContent = `${changePercent.toFixed(2)}%`; - changeCell.classList.toggle('text-success', changePercent >= 0); - changeCell.classList.toggle('text-danger', changePercent < 0); - - // Visual flash on price change - if (ticker.lastPrice !== 0 && priceChange !== 0) { - const flashClass = priceChange > 0 ? 'flash-success' : 'flash-danger'; + if (ticker.lastPrice !== 0 && currentPrice !== ticker.lastPrice) { + const flashClass = currentPrice > ticker.lastPrice ? 'flash-success' : 'flash-danger'; priceCell.classList.add(flashClass); setTimeout(() => priceCell.classList.remove(flashClass), 750); } - ticker.lastPrice = currentPrice; // Update the last price for this specific ticker + ticker.lastPrice = currentPrice; } - // --- Initialization --- + async function fetchAnalysis(ticker, rowElement) { + try { + const response = await fetch(`api/analysis.php?symbol=${ticker.symbol}&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); + } catch (error) { + console.error(`Failed to fetch analysis for ${ticker.symbol}:`, error); + const smaCell = rowElement.querySelector('.sma'); + if (smaCell) smaCell.textContent = 'Error'; + } + } + + function updateAnalysis(data, rowElement, currentPrice) { + const smaCell = rowElement.querySelector('.sma'); + const signalCell = rowElement.querySelector('.signal'); + if (!smaCell || !signalCell) return; + + const sma = parseFloat(data.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'; + } + } + + 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); + } + } + + 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'); + } + } - /** - * 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 + return; } - // Initial fetch - fetchAndupdate(ticker, rowElement); - - // Start polling every X seconds - setInterval(() => fetchAndupdate(ticker, rowElement), POLLING_INTERVAL); + const fetchAll = () => { + fetchAndupdate(ticker, rowElement).then(() => { + fetchAnalysis(ticker, rowElement); + }); + fetchAlert(ticker, rowElement); + }; + + fetchAll(); + setInterval(fetchAll, POLLING_INTERVAL); }); } diff --git a/db/migrations/002_create_candlestick_data_table.php b/db/migrations/002_create_candlestick_data_table.php new file mode 100644 index 0000000..4cc610d --- /dev/null +++ b/db/migrations/002_create_candlestick_data_table.php @@ -0,0 +1,31 @@ +exec($sql); + echo "Migration 002 successful: candlestick_data table created or already exists." . PHP_EOL; +} catch (PDOException $e) { + die("Migration 002 failed: " . $e->getMessage() . PHP_EOL); +} diff --git a/index.php b/index.php index 6e003f6..cb389df 100644 --- a/index.php +++ b/index.php @@ -49,30 +49,30 @@ EXCHANGE SYMBOL PRICE - 24H % - SIGNAL + SMA (20m) + SIGNAL + STATUS - + - Bitget + Binance
- + BTC/USDT
$0.00 - - --% - - - + - + - + - - + - Bitget + Binance
@@ -80,52 +80,10 @@
$0.00 - - --% - - - + - + - + - - - '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' => 'KCEX', 'symbol' => 'DOGE/USDT', 'price' => '0.1588', 'change' => 5.6, 'signal' => null], - ['exchange' => 'Bitget', 'symbol' => 'XRP/USDT', 'price' => '0.5210', 'change' => -0.5, 'signal' => null], - ]; - - foreach ($mock_data as $row): - $change_color = $row['change'] >= 0 ? 'success' : 'danger'; - $change_icon = $row['change'] >= 0 ? 'bi-arrow-up-right' : 'bi-arrow-down-left'; - - $signal_badge = ''; - if ($row['signal'] === 'Crash Alert') { - $signal_badge = 'Crash Alert'; - } elseif ($row['signal'] === 'Pump Alert') { - $signal_badge = 'Pump Alert'; - } elseif ($row['signal'] === 'Bearish Reversal') { - $signal_badge = 'Bearish Reversal'; - } - ?> - - - -
- - -
- - $ - - - - % - - - - -