This commit is contained in:
Flatlogic Bot 2025-11-15 11:26:12 +00:00
parent ffa84da26a
commit a116dc2b1f
4 changed files with 369 additions and 128 deletions

View File

@ -1,75 +1,77 @@
<?php
header('Content-Type: application/json');
require_once __DIR__ . '/../db/config.php';
require_once __DIR__ . '/analysis.php';
// --- Configuration ---
// Example: /api/alerts.php?symbol=BTCUSDT
$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.'
];
function check_bearish_alerts($symbol) {
$alerts = [];
try {
// Fetch latest price
$pdo = db();
$stmt = $pdo->prepare("SELECT close FROM candlestick_data WHERE symbol = :symbol ORDER BY timestamp DESC LIMIT 1");
$stmt->bindParam(':symbol', $symbol, PDO::PARAM_STR);
$stmt->execute();
$current_price = $stmt->fetchColumn();
// 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;
if (!$current_price) {
return [];
}
// 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.";
// Fetch analysis data
$sma = calculate_sma($symbol, 20);
$rsi = calculate_rsi($symbol, 14);
$macd_data = calculate_macd($symbol);
$patterns = calculate_patterns($symbol);
// Condition 1: Price vs. SMA
$price_below_sma = $current_price < $sma;
// Condition 2: RSI indicates weakening momentum (e.g., dropping from overbought)
// For simplicity, we'll check if RSI is above a certain threshold and not rising.
// A more complex implementation would track RSI changes over time.
$rsi_weakening = $rsi > 50; // Simplified: RSI is in the upper range, potential for reversal
// Condition 3: MACD Bearish Crossover
$macd_bearish_crossover = false;
if ($macd_data && $macd_data['macd'] < $macd_data['signal']) {
// Check if it just crossed
// This requires historical data, for now, we check the current state
$macd_bearish_crossover = true; // Simplified: MACD is below signal line
}
} catch (PDOException $e) {
http_response_code(500);
$response['status'] = 'Error';
$response['details'] = 'Database error: ' . $e->getMessage();
// Check original combo
if ($price_below_sma && $rsi_weakening && $macd_bearish_crossover) {
$alerts[] = [
'type' => 'Strong Bearish Signal',
'indicator' => 'SMA, RSI, MACD Combination',
'message' => 'Price dropped below 20-period SMA, RSI is high, and MACD shows a bearish state.'
];
}
echo json_encode($response);
// Check for candlestick patterns
if (!empty($patterns)) {
foreach ($patterns as $pattern_name => $detected) {
if ($detected) {
$alerts[] = [
'type' => 'Strong Bearish Signal',
'indicator' => 'Candlestick Pattern',
'message' => 'Detected bearish pattern: ' . ucwords(str_replace('_', ' ', $pattern_name))
];
}
}
}
return $alerts;
}
$alerts = check_bearish_alerts($symbol);
echo json_encode([
'symbol' => $symbol,
'alerts' => $alerts,
'timestamp' => time()
]);

View File

@ -3,10 +3,10 @@ header('Content-Type: application/json');
require_once __DIR__ . '/../db/config.php';
// --- Parameters ---
// Example: /api/analysis.php?symbol=BTCUSDT&indicator=sma&period=20
// Example: /api/analysis.php?symbol=BTCUSDT&indicators=sma,rsi,macd,patterns&period=20
$symbol = $_GET['symbol'] ?? 'BTCUSDT';
$indicator = $_GET['indicator'] ?? 'sma';
$indicators_str = $_GET['indicators'] ?? 'sma';
$indicators = array_map('trim', explode(',', strtolower($indicators_str)));
$period = (int)($_GET['period'] ?? 20);
if ($period <= 0) {
@ -16,45 +16,218 @@ if ($period <= 0) {
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 = $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;
}
return array_sum($closes) / $period;
}
function calculate_rsi($symbol, $period = 14) {
$pdo = db();
$limit = $period + 1;
$stmt = $pdo->prepare("SELECT close FROM candlestick_data WHERE symbol = :symbol ORDER BY timestamp DESC LIMIT :limit");
$stmt->bindParam(':symbol', $symbol, PDO::PARAM_STR);
$stmt->bindParam(':limit', $limit, PDO::PARAM_INT);
$stmt->execute();
$closes = array_reverse($stmt->fetchAll(PDO::FETCH_COLUMN));
if (count($closes) < $limit) {
return null;
}
$gains = 0;
$losses = 0;
for ($i = 1; $i < count($closes); $i++) {
$change = $closes[$i] - $closes[$i - 1];
if ($change > 0) {
$gains += $change;
} else {
$losses += abs($change);
}
}
if ($period == 0) return 100;
$avg_gain = $gains / $period;
$avg_loss = $losses / $period;
if ($avg_loss == 0) {
return 100;
}
$rs = $avg_gain / $avg_loss;
return 100 - (100 / (1 + $rs));
}
function _calculate_ema_series($prices, $period) {
if (count($prices) < $period) {
return [];
}
$smoothing_factor = 2 / ($period + 1);
$emas = [];
$initial_prices = array_slice($prices, 0, $period);
$sma = array_sum($initial_prices) / count($initial_prices);
$emas[] = $sma;
$remaining_prices = array_slice($prices, $period);
$last_ema = $sma;
foreach ($remaining_prices as $price) {
$current_ema = ($price - $last_ema) * $smoothing_factor + $last_ema;
$emas[] = $current_ema;
$last_ema = $current_ema;
}
return $emas;
}
function calculate_macd($symbol, $fast_period = 12, $slow_period = 26, $signal_period = 9) {
$pdo = db();
$limit = $slow_period + $signal_period + 50; // Fetch extra data for stability
$stmt = $pdo->prepare("SELECT close FROM candlestick_data WHERE symbol = :symbol ORDER BY timestamp ASC LIMIT :limit");
$stmt->bindParam(':symbol', $symbol, PDO::PARAM_STR);
$stmt->bindParam(':limit', $limit, PDO::PARAM_INT);
$stmt->execute();
$closes = $stmt->fetchAll(PDO::FETCH_COLUMN);
if (count($closes) < $period) {
return null; // Not enough data to calculate SMA
if (count($closes) < $slow_period + $signal_period) {
return null;
}
$sum = array_sum($closes);
$sma = $sum / $period;
$ema_fast_all = _calculate_ema_series($closes, $fast_period);
$ema_slow_all = _calculate_ema_series($closes, $slow_period);
return $sma;
$ema_fast_aligned = array_slice($ema_fast_all, count($ema_fast_all) - count($ema_slow_all));
$macd_line_series = [];
for ($i = 0; $i < count($ema_slow_all); $i++) {
$macd_line_series[] = $ema_fast_aligned[$i] - $ema_slow_all[$i];
}
$result = null;
if (strtolower($indicator) === 'sma') {
$result = calculate_sma($symbol, $period);
} else {
echo json_encode(['error' => 'Invalid indicator specified.']);
exit;
if (count($macd_line_series) < $signal_period) {
return null;
}
$signal_line_series = _calculate_ema_series($macd_line_series, $signal_period);
$macd_line = end($macd_line_series);
$signal_line = end($signal_line_series);
$histogram = $macd_line - $signal_line;
return [
'macd' => $macd_line,
'signal' => $signal_line,
'histogram' => $histogram
];
}
function calculate_patterns($symbol) {
$pdo = db();
// Fetch last 30 candles for pattern recognition
$stmt = $pdo->prepare("SELECT open, high, low, close, timestamp FROM candlestick_data WHERE symbol = :symbol ORDER BY timestamp DESC LIMIT 30");
$stmt->bindParam(':symbol', $symbol, PDO::PARAM_STR);
$stmt->execute();
$candles = $stmt->fetchAll(PDO::FETCH_ASSOC);
if (count($candles) < 5) { // Need at least a few candles for any pattern
return [];
}
$patterns = [];
$latest_candle = $candles[0];
$prev_candle = $candles[1];
// 1. Shooting Star
$body_size = abs($latest_candle['open'] - $latest_candle['close']);
$upper_wick = $latest_candle['high'] - max($latest_candle['open'], $latest_candle['close']);
$lower_wick = min($latest_candle['open'], $latest_candle['close']) - $latest_candle['low'];
if ($upper_wick > $body_size * 2 && $lower_wick < $body_size) {
$patterns['shooting_star'] = true;
}
// 2. Bearish Engulfing
if ($prev_candle['close'] > $prev_candle['open'] && // Previous candle is bullish
$latest_candle['open'] > $prev_candle['close'] && // Current candle opens above previous close
$latest_candle['close'] < $prev_candle['open']) { // Current candle closes below previous open
$patterns['bearish_engulfing'] = true;
}
// 3. Gravestone Doji
$is_doji = $body_size <= ($latest_candle['high'] - $latest_candle['low']) * 0.1;
if ($is_doji && $lower_wick < $body_size && $upper_wick > $body_size * 3) {
$patterns['gravestone_doji'] = true;
}
// 4. Evening Star (3-candle pattern)
if (count($candles) >= 3) {
$c1 = $candles[2]; // Large bullish candle
$c2 = $candles[1]; // Small body candle (star)
$c3 = $candles[0]; // Bearish candle
if (($c1['close'] > $c1['open']) && (abs($c1['close'] - $c1['open']) > ($c1['high'] - $c1['low']) * 0.7) && // c1 is strong bullish
($c2['open'] > $c1['close']) && (abs($c2['close'] - $c2['open']) < ($c2['high'] - $c2['low']) * 0.3) && // c2 is small body, gapped up
($c3['close'] < $c3['open']) && ($c3['open'] < $c2['open']) && ($c3['close'] < $c1['close'])) { // c3 is bearish, closes below midpoint of c1
$patterns['evening_star'] = true;
}
}
// 5. Double Top (Structural)
// This is a more complex pattern requiring finding two distinct peaks.
// We'll look for two highs that are close in price, separated by a valley.
if(count($candles) >= 15) {
$recent_highs = array_map(function($c) { return $c['high']; }, array_slice($candles, 0, 15));
$max_high = max($recent_highs);
$peaks = [];
foreach($candles as $i => $c) {
if ($i > 1 && $i < count($candles) -1 && $c['high'] >= $max_high * 0.98) {
if($candles[$i-1]['high'] < $c['high'] && $candles[$i+1]['high'] < $c['high']) {
$peaks[] = ['index' => $i, 'price' => $c['high']];
}
}
}
if (count($peaks) >= 2) {
// Check if the two most recent peaks form a double top
$peak1 = $peaks[1]; // More recent peak in time, but second in array
$peak2 = $peaks[0]; // Older peak
$price_diff = abs($peak1['price'] - $peak2['price']) / $peak2['price'];
$time_diff = $peak1['index'] - $peak2['index'];
// Peaks should be close in price, but not too close in time
if ($price_diff < 0.02 && $time_diff > 5) {
// Find the low point (valley) between the peaks
$valley_slice = array_slice($candles, $peak2['index'] + 1, $time_diff -1);
$valley_low = min(array_map(function($c){ return $c['low']; }, $valley_slice));
// Check if current price has broken below the valley low (neckline)
if($latest_candle['close'] < $valley_low) {
$patterns['double_top'] = true;
}
}
}
}
return $patterns;
}
$results = [];
foreach ($indicators as $indicator) {
switch ($indicator) {
case 'sma':
$results['sma'] = calculate_sma($symbol, $period);
break;
case 'rsi':
$results['rsi'] = calculate_rsi($symbol, 14);
break;
case 'macd':
$results['macd'] = calculate_macd($symbol);
break;
case 'patterns':
$results['patterns'] = calculate_patterns($symbol);
break;
}
}
$response = [
'symbol' => $symbol,
'indicator' => 'SMA',
'period' => $period,
'value' => $result,
'values' => $results,
'timestamp' => time()
];

View File

@ -1,5 +1,6 @@
document.addEventListener('DOMContentLoaded', function () {
const POLLING_INTERVAL = 3000; // 3 seconds
const POLLING_INTERVAL = 3000; // 3 seconds for price/analysis
const ALERT_POLLING_INTERVAL = 10000; // 10 seconds for alerts
const liveTickers = [
{ symbol: 'BTCUSDT', elementId: 'live-crypto-row-btc', lastPrice: 0 },
@ -46,26 +47,32 @@ document.addEventListener('DOMContentLoaded', function () {
async function fetchAnalysis(ticker, rowElement) {
try {
const response = await fetch(`api/analysis.php?symbol=${ticker.symbol}&period=20`);
const response = await fetch(`api/analysis.php?symbol=${ticker.symbol}&indicators=sma,rsi,macd&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);
updateAnalysisData(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';
rowElement.querySelector('.sma').textContent = 'Error';
rowElement.querySelector('.rsi').textContent = 'Error';
}
}
function updateAnalysis(data, rowElement, currentPrice) {
function updateAnalysisData(data, rowElement, currentPrice) {
const smaCell = rowElement.querySelector('.sma');
const signalCell = rowElement.querySelector('.signal');
if (!smaCell || !signalCell) return;
const rsiCell = rowElement.querySelector('.rsi');
const macdCell = rowElement.querySelector('.macd');
const macdSignalCell = rowElement.querySelector('.macd-signal');
if (!smaCell || !signalCell || !rsiCell || !macdCell || !macdSignalCell) return;
const sma = parseFloat(data.sma);
const sma = parseFloat(data.values.sma);
const rsi = parseFloat(data.values.rsi);
const macdData = data.values.macd;
if (!isNaN(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) {
@ -73,32 +80,69 @@ document.addEventListener('DOMContentLoaded', function () {
} else {
signalCell.innerHTML = '<span class="badge bg-secondary-subtle text-secondary-emphasis">At SMA</span>';
}
} else {
smaCell.textContent = '-';
signalCell.textContent = '-';
}
async function fetchAlert(ticker, rowElement) {
if (!isNaN(rsi)) {
rsiCell.textContent = rsi.toFixed(2);
} else {
rsiCell.textContent = '-';
}
if (macdData && typeof macdData.macd !== 'undefined' && typeof macdData.signal !== 'undefined') {
const macd = parseFloat(macdData.macd);
const macdSignal = parseFloat(macdData.signal);
if (!isNaN(macd)) {
macdCell.textContent = macd.toFixed(4);
}
if (!isNaN(macdSignal)) {
macdSignalCell.textContent = macdSignal.toFixed(4);
}
} else {
macdCell.textContent = '-';
macdSignalCell.textContent = '-';
}
}
async function fetchAndDisplayAlerts() {
const alertsContainer = document.getElementById('alerts-container');
if (!alertsContainer) return;
let allAlerts = [];
for (const ticker of liveTickers) {
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);
if (data.alerts && data.alerts.length > 0) {
data.alerts.forEach(alert => {
allAlerts.push({ symbol: data.symbol, ...alert });
});
}
} catch (error) {
console.error(`Failed to fetch alert for ${ticker.symbol}:`, error);
console.error(`Failed to fetch alerts 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');
if (allAlerts.length > 0) {
alertsContainer.innerHTML = ''; // Clear previous alerts
allAlerts.forEach(alert => {
const alertEl = document.createElement('div');
alertEl.className = 'alert alert-danger d-flex align-items-center';
alertEl.innerHTML = `
<i class="bi bi-exclamation-triangle-fill me-3"></i>
<div>
<strong class="d-block">${alert.type} on ${alert.symbol}</strong>
${alert.message}
</div>
`;
alertsContainer.appendChild(alertEl);
});
} else {
alertsContainer.innerHTML = '<p class="text-muted">No alerts triggered yet. The system is monitoring the market.</p>';
}
}
@ -114,12 +158,15 @@ document.addEventListener('DOMContentLoaded', function () {
fetchAndupdate(ticker, rowElement).then(() => {
fetchAnalysis(ticker, rowElement);
});
fetchAlert(ticker, rowElement);
};
fetchAll();
setInterval(fetchAll, POLLING_INTERVAL);
});
// Setup alert polling
fetchAndDisplayAlerts();
setInterval(fetchAndDisplayAlerts, ALERT_POLLING_INTERVAL);
}
initializeLiveTickers();

View File

@ -51,6 +51,9 @@
<th scope="col">PRICE</th>
<th scope="col">SMA (20m)</th>
<th scope="col">SIGNAL</th>
<th scope="col">RSI (14m)</th>
<th scope="col">MACD</th>
<th scope="col">MACD Signal</th>
<th scope="col" class="text-end">STATUS</th>
</tr>
</thead>
@ -67,6 +70,9 @@
<td class="fw-bold fs-5 price">$0.00</td>
<td class="fw-medium sma">-</td>
<td class="fw-medium signal">-</td>
<td class="fw-medium rsi">-</td>
<td class="fw-medium macd">-</td>
<td class="fw-medium macd-signal">-</td>
<td class="text-end status">-</td>
</tr>
@ -82,6 +88,9 @@
<td class="fw-bold fs-5 price">$0.00</td>
<td class="fw-medium sma">-</td>
<td class="fw-medium signal">-</td>
<td class="fw-medium rsi">-</td>
<td class="fw-medium macd">-</td>
<td class="fw-medium macd-signal">-</td>
<td class="text-end status">-</td>
</tr>
</tbody>
@ -89,6 +98,16 @@
</div>
</div>
</div>
<div class="card mt-4">
<div class="card-header bg-white border-bottom-0 pt-4 pb-0">
<h4 class="card-title fw-bold">Triggered Alerts</h4>
</div>
<div class="card-body" id="alerts-container">
<p class="text-muted">No alerts triggered yet. The system is monitoring the market.</p>
<!-- Alerts will be dynamically inserted here -->
</div>
</div>
</main>
<footer class="text-center text-muted py-4">