This commit is contained in:
Flatlogic Bot 2025-11-15 10:49:49 +00:00
parent a824cabd7e
commit ffa84da26a
6 changed files with 283 additions and 175 deletions

61
api/analysis.php Normal file
View File

@ -0,0 +1,61 @@
<?php
header('Content-Type: application/json');
require_once __DIR__ . '/../db/config.php';
// --- Parameters ---
// Example: /api/analysis.php?symbol=BTCUSDT&indicator=sma&period=20
$symbol = $_GET['symbol'] ?? 'BTCUSDT';
$indicator = $_GET['indicator'] ?? 'sma';
$period = (int)($_GET['period'] ?? 20);
if ($period <= 0) {
echo json_encode(['error' => '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);

View File

@ -1,85 +1,109 @@
<?php <?php
header('Content-Type: application/json'); header('Content-Type: application/json');
require_once __DIR__ . '/../db/config.php';
// --- Configuration --- // --- Configuration ---
// Default to BTCUSDT if no symbol is provided in the URL query string. $symbols = ['BTCUSDT', 'ETHUSDT'];
$symbol = isset($_GET['symbol']) ? strtoupper($_GET['symbol']) : 'BTCUSDT'; $exchange = 'Binance';
$api_url = "https://api.bitget.com/api/v2/spot/market/tickers?symbol=" . $symbol; $interval = '1m';
// --- 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
);
// --- cURL Execution ---
$ch = curl_init(); $ch = curl_init();
curl_setopt_array($ch, [ curl_setopt_array($ch, [
CURLOPT_URL => $api_url, CURLOPT_URL => $api_url,
CURLOPT_RETURNTRANSFER => true, CURLOPT_RETURNTRANSFER => true,
CURLOPT_TIMEOUT => 10, CURLOPT_TIMEOUT => 10,
CURLOPT_HTTPHEADER => [
'Accept: application/json'
],
// It's good practice to set a user-agent
CURLOPT_USERAGENT => 'FlatlogicMarketDetector/1.0' CURLOPT_USERAGENT => 'FlatlogicMarketDetector/1.0'
]); ]);
$response = curl_exec($ch); $response = curl_exec($ch);
$http_code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
$error = curl_error($ch);
curl_close($ch); curl_close($ch);
return json_decode($response, true);
// --- Response Handling ---
if ($error) {
http_response_code(500);
echo json_encode(['error' => 'cURL Error: ' . $error]);
exit;
} }
if ($http_code !== 200) { // --- Database Logging ---
http_response_code($http_code); function log_candlestick_data($pdo, $symbol, $exchange, $interval, $kline) {
echo json_encode(['error' => 'Bitget API returned non-200 status', 'details' => json_decode($response)]); $sql = <<<SQL
exit; INSERT INTO candlestick_data (symbol, exchange, interval_time, open_time, open_price, high_price, low_price, close_price, volume, close_time, quote_asset_volume, number_of_trades, taker_buy_base_asset_volume, taker_buy_quote_asset_volume)
VALUES (:symbol, :exchange, :interval_time, :open_time, :open_price, :high_price, :low_price, :close_price, :volume, :close_time, :quote_asset_volume, :number_of_trades, :taker_buy_base_asset_volume, :taker_buy_quote_asset_volume)
ON DUPLICATE KEY UPDATE
close_price = VALUES(close_price), high_price = VALUES(high_price), low_price = VALUES(low_price), volume = VALUES(volume);
SQL;
$stmt = $pdo->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 { try {
require_once __DIR__ . '/../db/config.php';
$price_to_log = $ticker_data['lastPr'] ?? null;
if ($price_to_log) {
$pdo = db(); $pdo = db();
$stmt = $pdo->prepare( foreach ($symbols as $symbol) {
"INSERT INTO price_history (symbol, price) VALUES (:symbol, :price)" $klines = fetch_candlestick_data($symbol, $interval);
);
$stmt->execute(['symbol' => $symbol, 'price' => $price_to_log]); 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) { } catch (Exception $e) {
// If DB fails, log error but don't crash the API endpoint http_response_code(500);
error_log('DB logging failed in ticker.php: ' . $e->getMessage()); // 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 --- // --- Output for Frontend ---
// We select and sanitize the fields we want to send to the frontend. // This part is for the main dashboard display, not the alerts API.
$output = [ if (isset($_GET['symbol'])) {
'exchange' => 'Bitget', $symbol_to_find = strtoupper($_GET['symbol']);
'symbol' => $ticker_data['symbol'] ?? 'N/A', foreach ($latest_tickers as $ticker) {
'price' => $ticker_data['lastPr'] ?? '0.00', if ($ticker['symbol'] === $symbol_to_find) {
// Bitget provides the 24h change as a decimal, e.g., 0.025 for +2.5% echo json_encode($ticker);
'change_24h_percent' => isset($ticker_data['priceChangePercent']) ? (float)$ticker_data['priceChangePercent'] * 100 : 0, exit;
'signal' => '-' // Placeholder for future signal detection }
]; }
// Fallback if the requested symbol wasn't processed
echo json_encode($output); 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.']);
}

View File

@ -63,3 +63,13 @@ body {
.flash-danger { .flash-danger {
animation: flash-danger-anim 0.75s ease-out; animation: flash-danger-anim 0.75s ease-out;
} }
/* 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;
}

View File

@ -1,100 +1,124 @@
document.addEventListener('DOMContentLoaded', function () { document.addEventListener('DOMContentLoaded', function () {
const POLLING_INTERVAL = 3000; // 3 seconds const POLLING_INTERVAL = 3000; // 3 seconds
// --- Configuration for Live Tickers ---
const liveTickers = [ const liveTickers = [
{ symbol: 'BTCUSDT', elementId: 'live-crypto-row-btc', lastPrice: 0 }, { symbol: 'BTCUSDT', elementId: 'live-crypto-row-btc', lastPrice: 0 },
{ symbol: 'ETHUSDT', elementId: 'live-crypto-row-eth', 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) { async function fetchAndupdate(ticker, rowElement) {
try { try {
const response = await fetch(`api/ticker.php?symbol=${ticker.symbol}`); 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}`);
}
const data = await response.json(); 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); updateRow(data, ticker, rowElement);
} catch (error) { } catch (error) {
console.error(`Failed to fetch live price for ${ticker.symbol}:`, 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'); const priceCell = rowElement.querySelector('.price');
if (priceCell) priceCell.textContent = 'Error'; 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) { function updateRow(data, ticker, rowElement) {
const priceCell = rowElement.querySelector('.price'); const priceCell = rowElement.querySelector('.price');
const changeCell = rowElement.querySelector('.change');
const symbolCell = rowElement.querySelector('.symbol'); const symbolCell = rowElement.querySelector('.symbol');
const exchangeCell = rowElement.querySelector('.exchange'); 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}`); console.error(`One or more required elements not found in row for ${ticker.symbol}`);
return; return;
} }
const currentPrice = parseFloat(data.price); const currentPrice = parseFloat(data.price);
const priceChange = currentPrice - ticker.lastPrice; priceCell.textContent = `${currentPrice.toLocaleString('en-US', { minimumFractionDigits: 2, maximumFractionDigits: 2 })}`;
// Update text content
priceCell.textContent = `$${currentPrice.toLocaleString('en-US', { minimumFractionDigits: 2, maximumFractionDigits: 2 })}`;
symbolCell.textContent = data.symbol.replace('_SPBL', ''); symbolCell.textContent = data.symbol.replace('_SPBL', '');
exchangeCell.textContent = data.exchange; exchangeCell.textContent = data.exchange;
// Update 24h change if (ticker.lastPrice !== 0 && currentPrice !== ticker.lastPrice) {
const changePercent = parseFloat(data.change_24h_percent); const flashClass = currentPrice > ticker.lastPrice ? 'flash-success' : 'flash-danger';
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';
priceCell.classList.add(flashClass); priceCell.classList.add(flashClass);
setTimeout(() => priceCell.classList.remove(flashClass), 750); 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 = '<span class="badge bg-success-subtle text-success-emphasis">Above SMA</span>';
} else if (currentPrice < sma) {
signalCell.innerHTML = '<span class="badge bg-danger-subtle text-danger-emphasis">Below SMA</span>';
} else {
signalCell.innerHTML = '<span class="badge bg-secondary-subtle text-secondary-emphasis">At SMA</span>';
}
}
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 = '<span class="badge bg-danger-subtle text-danger-emphasis">Crash Alert</span>';
rowElement.classList.add('flash-alert');
} else if (status === 'Pump Alert') {
statusCell.innerHTML = '<span class="badge bg-success-subtle text-success-emphasis">Pump Alert</span>';
rowElement.classList.add('flash-alert');
}
}
/**
* Initializes the polling for all configured tickers.
*/
function initializeLiveTickers() { function initializeLiveTickers() {
liveTickers.forEach(ticker => { liveTickers.forEach(ticker => {
const rowElement = document.getElementById(ticker.elementId); const rowElement = document.getElementById(ticker.elementId);
if (!rowElement) { if (!rowElement) {
console.error(`Element with ID #${ticker.elementId} not found for symbol ${ticker.symbol}.`); 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 const fetchAll = () => {
fetchAndupdate(ticker, rowElement); fetchAndupdate(ticker, rowElement).then(() => {
fetchAnalysis(ticker, rowElement);
});
fetchAlert(ticker, rowElement);
};
// Start polling every X seconds fetchAll();
setInterval(() => fetchAndupdate(ticker, rowElement), POLLING_INTERVAL); setInterval(fetchAll, POLLING_INTERVAL);
}); });
} }

View File

@ -0,0 +1,31 @@
<?php
require_once __DIR__ . '/../config.php';
try {
$db = db();
$sql = <<<SQL
CREATE TABLE IF NOT EXISTS candlestick_data (
id INT AUTO_INCREMENT PRIMARY KEY,
symbol VARCHAR(20) NOT NULL,
exchange VARCHAR(50) NOT NULL,
interval_time VARCHAR(10) NOT NULL,
open_time BIGINT NOT NULL,
open_price DECIMAL(20, 8) NOT NULL,
high_price DECIMAL(20, 8) NOT NULL,
low_price DECIMAL(20, 8) NOT NULL,
close_price DECIMAL(20, 8) NOT NULL,
volume DECIMAL(20, 8) NOT NULL,
close_time BIGINT NOT NULL,
quote_asset_volume DECIMAL(20, 8),
number_of_trades INT,
taker_buy_base_asset_volume DECIMAL(20, 8),
taker_buy_quote_asset_volume DECIMAL(20, 8),
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
UNIQUE KEY `symbol_exchange_interval_time_open_time` (`symbol`, `exchange`, `interval_time`, `open_time`)
);
SQL;
$db->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);
}

View File

@ -49,30 +49,30 @@
<th scope="col">EXCHANGE</th> <th scope="col">EXCHANGE</th>
<th scope="col">SYMBOL</th> <th scope="col">SYMBOL</th>
<th scope="col">PRICE</th> <th scope="col">PRICE</th>
<th scope="col" class="text-center">24H %</th> <th scope="col">SMA (20m)</th>
<th scope="col" class="text-end">SIGNAL</th> <th scope="col">SIGNAL</th>
<th scope="col" class="text-end">STATUS</th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
<!-- Live Row 1 (Updated by JavaScript) --> <!-- BTC Row (Updated by JavaScript) -->
<tr id="live-crypto-row-btc"> <tr id="live-crypto-row-btc">
<td class="fw-medium text-muted exchange">Bitget</td> <td class="fw-medium text-muted exchange">Binance</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="BTC"> <img src="https://via.placeholder.com/24/f0b90b/000000?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="fw-medium sma">-</td>
<span class="fw-bold change">--%</span> <td class="fw-medium signal">-</td>
</td> <td class="text-end status">-</td>
<td class="text-end signal">-</td>
</tr> </tr>
<!-- Live Row 2 (Updated by JavaScript) --> <!-- ETH Row (Updated by JavaScript) -->
<tr id="live-crypto-row-eth"> <tr id="live-crypto-row-eth">
<td class="fw-medium text-muted exchange">Bitget</td> <td class="fw-medium text-muted exchange">Binance</td>
<td> <td>
<div class="d-flex align-items-center"> <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"> <img src="https://via.placeholder.com/24/6f42c1/FFFFFF?Text=E" class="symbol-logo rounded-circle" alt="ETH">
@ -80,52 +80,10 @@
</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="fw-medium sma">-</td>
<span class="fw-bold change">--%</span> <td class="fw-medium signal">-</td>
</td> <td class="text-end status">-</td>
<td class="text-end signal">-</td>
</tr> </tr>
<?php
// Static mock data for UI variety
$mock_data = [
['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' => '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 = '<span class="badge bg-danger-subtle text-danger-emphasis">Crash Alert</span>';
} elseif ($row['signal'] === 'Pump Alert') {
$signal_badge = '<span class="badge bg-success-subtle text-success-emphasis">Pump Alert</span>';
} elseif ($row['signal'] === 'Bearish Reversal') {
$signal_badge = '<span class="badge bg-warning-subtle text-warning-emphasis">Bearish Reversal</span>';
}
?>
<tr>
<td class="fw-medium text-muted"><?php echo htmlspecialchars($row['exchange']); ?></td>
<td>
<div class="d-flex align-items-center">
<img src="https://via.placeholder.com/24/0d6efd/FFFFFF?Text=<?php echo substr($row['symbol'], 0, 1); ?>" class="symbol-logo rounded-circle" alt="">
<span class="fw-bold"><?php echo htmlspecialchars($row['symbol']); ?></span>
</div>
</td>
<td class="fw-bold fs-5">$<?php echo htmlspecialchars($row['price']); ?></td>
<td class="text-center">
<span class="fw-bold text-<?php echo $change_color; ?>">
<i class="bi <?php echo $change_icon; ?>"></i>
<?php echo htmlspecialchars(abs($row['change'])); ?>%
</span>
</td>
<td class="text-end"><?php echo $signal_badge; ?></td>
</tr>
<?php endforeach; ?>
</tbody> </tbody>
</table> </table>
</div> </div>