"$errstr in $errfile on line $errline"]);
exit;
}
set_error_handler("errorHandler");
define('SYMBOL_RE', '[A-Z]{1,6}(?:-[A-Z]{1,3})?');
$ajaxMode = $_GET['ajax'] ?? '';
$cacheFile = __DIR__ . '/symbols_cache.json';
$notAStockFile = __DIR__ . '/notastock2.json';
$stocksFile = __DIR__ . '/stocks.json';
$progressFile = __DIR__ . '/progress.json';
$foundFile = __DIR__ . '/found_prebreakout.json';
if ($ajaxMode === 'start') {
header('Content-Type: application/json');
$maxPrice = isset($_GET['maxPrice']) ? (float)$_GET['maxPrice'] : 4.0;
file_put_contents($progressFile, json_encode([
'status' => 'starting',
'checked' => 0,
'validFound' => 0,
'notReady' => 0,
'invalid' => 0,
'currentSymbol' => '',
'done' => false,
'result' => null,
'maxPrice' => $maxPrice,
'started' => time()
]));
echo json_encode(['ok' => true]);
exit;
}
if ($ajaxMode === 'poll') {
header('Content-Type: application/json');
if (!file_exists($progressFile)) {
echo json_encode(['error' => 'Progress file not found']);
exit;
}
readfile($progressFile);
exit;
}
if ($ajaxMode === 'run') {
ignore_user_abort(true);
set_time_limit(0);
header('Content-Type: text/plain');
echo "0\n";
ob_flush();
flush();
runPreBreakoutSearch($progressFile);
exit;
}
if ($ajaxMode === 'foundlist') {
header('Content-Type: application/json');
if (!file_exists($foundFile)) {
echo json_encode(['found' => []]);
exit;
}
$data = json_decode(file_get_contents($foundFile), true);
echo json_encode(['found' => $data ?: []]);
exit;
}
if ($ajaxMode === 'clearfound') {
header('Content-Type: application/json');
if (file_exists($foundFile)) unlink($foundFile);
echo json_encode(['ok' => true]);
exit;
}
?>
Pre-Breakout Scanner
Pre-Breakout Scanner
Finding stocks coiling near resistance with rising volume
Found Stocks 0
No stocks found yet. Start scanning to discover pre-breakout candidates.
$v) $excludeSet[$ticker] = 1;
$progress = json_decode(file_get_contents($progressFile), true);
$maxPrice = isset($progress['maxPrice']) ? (float)$progress['maxPrice'] : 4.0;
$validSymbol = null; $validTicker = null; $validPrice = null; $validVolRatio = null; $validSparkline = [];
$checkedSymbols = []; $invalidFound = []; $notReadySymbols = [];
$validFound = 0;
$maxTotal = 3000; $batchSize = 40; $sampleSize = 200;
$invalidTypes = ['ETF','MUTUALFUND','INDEX','CRYPTOCURRENCY','FUTURE','OPTION','BOND','PENNY_STOCK','PREFERRED_STOCK','REIT','UNIT','RIGHT','WARRANT','STRUCTURED','CURRENCY'];
writeProgress($progressFile, [
'status' => 'searching', 'checked' => 0, 'validFound' => 0, 'notReady' => 0,
'invalid' => 0, 'currentSymbol' => '', 'done' => false, 'result' => null
]);
while (!$validSymbol && count($checkedSymbols) < $maxTotal) {
$candidates = streamSampleSymbolsFast($cacheFile, $excludeSet, $sampleSize);
if (empty($candidates)) {
$candidates = streamSampleSymbolsFast($cacheFile, [], $sampleSize);
if (empty($candidates)) break;
}
$offset = 0;
while ($offset < count($candidates) && !$validSymbol) {
$batch = array_slice($candidates, $offset, $batchSize);
$offset += $batchSize;
$batch = array_filter($batch, function($symbol) use ($checkedSymbols) {
return !in_array($symbol, $checkedSymbols);
});
if (empty($batch)) continue;
array_push($checkedSymbols, ...$batch);
writeProgress($progressFile, [
'checked' => count($checkedSymbols),
'currentSymbol' => $batch[0] ?? ''
]);
$mh = curl_multi_init(); $handles = [];
foreach ($batch as $symbol) {
$ch = curl_init("https://query1.finance.yahoo.com/v1/finance/search?q=" . urlencode($symbol) . ""esCount=1&newsCount=0");
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true, CURLOPT_CONNECTTIMEOUT => 3, CURLOPT_TIMEOUT => 10,
CURLOPT_SSL_VERIFYPEER => false, CURLOPT_USERAGENT => 'Mozilla/5.0', CURLOPT_FOLLOWLOCATION => true
]);
curl_multi_add_handle($mh, $ch);
$handles[$symbol] = $ch;
}
do { curl_multi_exec($mh, $running); if ($running > 0) curl_multi_select($mh, 0.05); } while ($running > 0);
$validBatch = []; $newInvalid = [];
foreach ($handles as $symbol => $ch) {
$body = curl_multi_getcontent($ch); curl_multi_remove_handle($mh, $ch); curl_close($ch);
$data = json_decode($body, true); $quote = $data['quotes'][0] ?? null; $quoteType = $quote['quoteType'] ?? '';
if (!$quote || $quoteType !== 'EQUITY' || in_array($quoteType, $invalidTypes)) {
$newInvalid[] = $symbol; $invalidFound[] = $symbol; continue;
}
$ticker = $quote['symbol'] ?? null; if ($ticker) $validBatch[$symbol] = $ticker;
}
curl_multi_close($mh);
if (!empty($newInvalid)) {
appendToNotAStock($notAStockFile, $newInvalid);
foreach ($newInvalid as $symbol) $excludeSet[$symbol] = 1;
}
writeProgress($progressFile, ['invalid' => count($invalidFound)]);
if (empty($validBatch)) continue;
$validFound += count($validBatch);
writeProgress($progressFile, ['validFound' => $validFound]);
$mh2 = curl_multi_init(); $chartHandles = [];
foreach ($validBatch as $symbol => $ticker) {
$ch = curl_init("https://query1.finance.yahoo.com/v8/finance/chart/{$ticker}?interval=1d&range=3mo");
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true, CURLOPT_CONNECTTIMEOUT => 3, CURLOPT_TIMEOUT => 12,
CURLOPT_SSL_VERIFYPEER => false, CURLOPT_USERAGENT => 'Mozilla/5.0', CURLOPT_FOLLOWLOCATION => true
]);
curl_multi_add_handle($mh2, $ch);
$chartHandles[$symbol] = ['ch' => $ch, 'ticker' => $ticker];
}
do { curl_multi_exec($mh2, $running2); if ($running2 > 0) curl_multi_select($mh2, 0.05); } while ($running2 > 0);
foreach ($chartHandles as $symbol => $info) {
$body = curl_multi_getcontent($info['ch']); curl_multi_remove_handle($mh2, $info['ch']); curl_close($info['ch']);
if (!$validSymbol) {
$result = hasPreBreakoutFast($body, $maxPrice);
if ($result) {
$validSymbol = $symbol;
$validTicker = $info['ticker'];
$validPrice = $result['price'];
$validVolRatio = $result['volRatio'];
$validSparkline = $result['sparkline'];
// Save to found file
$foundEntry = [
'ticker' => $validTicker,
'price' => $validPrice,
'volRatio' => round($validVolRatio, 2),
'sparklinePrices' => $validSparkline,
'foundAt' => time()
];
$foundData = [];
if (file_exists($foundFile)) {
$foundData = json_decode(file_get_contents($foundFile), true) ?: [];
}
$foundData[] = $foundEntry;
file_put_contents($foundFile, json_encode($foundData), LOCK_EX);
writeProgress($progressFile, [
'status' => 'found',
'result' => [
'ticker' => $validTicker,
'price' => $validPrice,
'volRatio' => round($validVolRatio, 2),
'sparklinePrices' => $validSparkline
]
]);
curl_multi_close($mh2);
break 3;
} else {
$notReadySymbols[] = $symbol;
}
} else {
$notReadySymbols[] = $symbol;
}
}
curl_multi_close($mh2);
writeProgress($progressFile, ['notReady' => count($notReadySymbols)]);
}
unset($candidates);
}
if (!$validSymbol) {
writeProgress($progressFile, [
'done' => true, 'status' => 'notfound',
'message' => "No pre-breakout stock found after checking " . count($checkedSymbols) . " candidates. " . count($notReadySymbols) . " valid symbols were not in pre-breakout setup."
]);
}
}
function writeProgress($file, array $updates) {
$data = [];
if (file_exists($file)) $data = json_decode(file_get_contents($file), true) ?: [];
$data = array_merge($data, $updates);
file_put_contents($file, json_encode($data), LOCK_EX);
}
function loadExcludeSet($stocksFile, $notAStockFile) {
$excludeSet = [];
if (file_exists($stocksFile)) {
$fh = fopen($stocksFile, 'r');
$buf = '';
while (!feof($fh)) {
$buf .= fread($fh, 8192);
preg_match_all('/"symbol"\s*:\s*"(' . SYMBOL_RE . ')"/', $buf, $m);
foreach ($m[1] as $symbol) $excludeSet[$symbol] = 1;
$buf = substr($buf, -50);
}
fclose($fh);
}
if (file_exists($notAStockFile)) {
$fh = fopen($notAStockFile, 'r');
$buf = '';
while (!feof($fh)) {
$buf .= fread($fh, 8192);
preg_match_all('/(?:[\[,])\s*"(' . SYMBOL_RE . ')"\s*(?:,|\])/', $buf, $m);
foreach ($m[1] as $symbol) $excludeSet[$symbol] = 1;
$buf = substr($buf, -60);
}
fclose($fh);
}
return $excludeSet;
}
function streamSampleSymbolsFast($cacheFile, array $excludeSet, $sampleSize = 200) {
if (!file_exists($cacheFile)) return [];
$exclude = $excludeSet;
$fh = fopen($cacheFile, 'r');
if (!$fh) return [];
$reservoir = []; $count = 0; $buf = '';
while (!feof($fh)) {
$buf .= fread($fh, 8192);
preg_match_all('/(?:[\[,])\s*"(' . SYMBOL_RE . ')"\s*(?:,|\])/', $buf, $matches);
foreach ($matches[1] as $symbol) {
if (isset($exclude[$symbol])) continue;
$count++;
if (count($reservoir) < $sampleSize) {
$reservoir[] = $symbol;
} else {
$j = random_int(0, $count - 1);
if ($j < $sampleSize) $reservoir[$j] = $symbol;
}
}
$buf = substr($buf, -60);
}
fclose($fh);
return $reservoir;
}
function hasPreBreakoutFast($responseBody, $maxPrice = 4.0) {
if (!$responseBody) return false;
$data = json_decode($responseBody, true);
$result = $data['chart']['result'][0] ?? null;
if (!$result) return false;
$closes = $result['indicators']['quote'][0]['close'] ?? [];
$volumes = $result['indicators']['quote'][0]['volume'] ?? [];
$prices = []; $vols = [];
foreach ($closes as $i => $c) {
if (isset($c) && $c > 0) {
$prices[] = $c;
$vols[] = isset($volumes[$i]) ? (int)$volumes[$i] : 0;
}
}
$n = count($prices);
if ($n < 40) return false;
$currentPrice = $prices[$n - 1];
// Max price filter
if ($currentPrice > $maxPrice) return false;
// 1. NOT already gone off: no +50% in 3 months
$totalReturn = (($currentPrice - $prices[0]) / $prices[0]) * 100;
if ($totalReturn > 50) return false;
// 2. NOT recently spiked: no +15% in last 7 days
$last7 = array_slice($prices, -7);
$before7 = array_slice($prices, 0, -7);
if (!empty($before7)) {
$avgBefore7 = array_sum($before7) / count($before7);
$maxLast7 = max($last7);
$spikePct = (($maxLast7 - $avgBefore7) / $avgBefore7) * 100;
if ($spikePct > 15) return false;
}
// 3. Volume building in recent window
$recentWindow = min(5, $n);
$quietWindow = $n - $recentWindow;
if ($quietWindow < 10) return false;
$recentVols = array_slice($vols, -$recentWindow);
$quietVols = array_slice($vols, 0, $quietWindow);
$recentAvgVol = array_sum($recentVols) / count($recentVols);
$quietAvgVol = array_sum($quietVols) / count($quietVols);
$volRatio = $quietAvgVol > 0 ? ($recentAvgVol / $quietAvgVol) : 0;
if ($volRatio < 2.0) return false;
// 4. Price near recent highs (pressing resistance)
$recentPrices = array_slice($prices, -$recentWindow);
$recentHigh = max($recentPrices);
$recentLow = min($recentPrices);
$distanceFromHigh = $recentHigh > 0 ? (($recentHigh - $currentPrice) / $recentHigh) * 100 : 100;
if ($distanceFromHigh > 5) return false;
// 5. Tight consolidation
$recentRangePct = $recentLow > 0 ? (($recentHigh - $recentLow) / $recentLow) * 100 : 100;
if ($recentRangePct > 12) return false;
// 6. Slight upward tilt (accumulation, not distribution)
$quietPrices = array_slice($prices, 0, $quietWindow);
$quietMid = (int)(count($quietPrices) / 2);
$quietFirstHalf = array_slice($quietPrices, 0, $quietMid);
$quietSecondHalf = array_slice($quietPrices, $quietMid);
$avgFirst = array_sum($quietFirstHalf) / max(1, count($quietFirstHalf));
$avgSecond = array_sum($quietSecondHalf) / max(1, count($quietSecondHalf));
if ($avgFirst > 0 && (($avgSecond - $avgFirst) / $avgFirst) * 100 < -5) return false;
// 7. Recent days trending up
$last3 = array_slice($prices, -3);
if (count($last3) >= 3) {
$trendingUp = ($last3[2] > $last3[0]);
if (!$trendingUp) return false;
}
// 8. Minimum volume
if ($recentAvgVol < 50000) return false;
// 9. Price floor
if ($currentPrice < 0.5) return false;
// Build sparkline data (last 30 days)
$sparklinePrices = array_slice($prices, -30);
return [
'price' => $currentPrice,
'volRatio' => $volRatio,
'sparkline' => $sparklinePrices
];
}
function appendToNotAStock($file, array $newSymbols) {
if (empty($newSymbols)) return;
if (!file_exists($file) || filesize($file) < 3) {
$fh = fopen($file, 'w');
fwrite($fh, '["' . implode('","', $newSymbols) . '"]');
fclose($fh);
return;
}
$fh = fopen($file, 'r+');
if (!$fh) return;
$size = filesize($file); $pos = $size - 1;
while ($pos >= 0) {
fseek($fh, $pos); $c = fread($fh, 1);
if ($c === ']') break;
$pos--;
}
fseek($fh, $pos);
fwrite($fh, ',"' . implode('","', $newSymbols) . '"]');
fclose($fh);
}
function updateAllSymbolsCache() {
$cacheFile = __DIR__ . '/symbols_cache.json';
$ch = curl_init();
curl_setopt_array($ch, [
CURLOPT_URL => 'https://www.sec.gov/files/company_tickers.json',
CURLOPT_RETURNTRANSFER => true,
CURLOPT_FOLLOWLOCATION => true,
CURLOPT_SSL_VERIFYPEER => false,
CURLOPT_USERAGENT => 'StockScanner research-tool contact@example.com',
CURLOPT_TIMEOUT => 30
]);
$json = curl_exec($ch);
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
if ($httpCode !== 200 || !$json) return false;
$data = json_decode($json, true);
if (!is_array($data)) return false;
$seen = [];
$symbols = [];
foreach ($data as $row) {
$symbol = strtoupper(trim($row['ticker'] ?? ''));
if ($symbol === '' || !preg_match('/^' . SYMBOL_RE . '$/', $symbol)) continue;
if (isset($seen[$symbol])) continue;
$seen[$symbol] = 1;
$symbols[] = $symbol;
}
$total = count($symbols);
$outFh = fopen($cacheFile, 'w');
fwrite($outFh, '{"timestamp":' . time() . ',"count":' . $total . ',"symbols":[');
foreach ($symbols as $i => $s) {
fwrite($outFh, ($i === 0 ? '' : ',') . '"' . $s . '"');
}
fwrite($outFh, ']}');
fclose($outFh);
return true;
}