"$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
Ready to scan
0
Checked
0
Valid
0
Not Ready
0
Invalid
Status: Idle
Current:

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 = []; $validType = null; $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); $matchType = 'Pre-Breakout'; if (!$result) { // Same chart data, no extra call: check it against the 6 Peter Lynch stock types $result = classifyStockShape($body, $maxPrice); $matchType = $result['type'] ?? null; } if ($result) { $validSymbol = $symbol; $validTicker = $info['ticker']; $validPrice = $result['price']; $validVolRatio = $result['volRatio']; $validSparkline = $result['sparkline']; $validType = $matchType; // Save to found file $foundEntry = [ 'ticker' => $validTicker, 'price' => $validPrice, 'volRatio' => round($validVolRatio, 2), 'sparklinePrices' => $validSparkline, 'type' => $validType, '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, 'type' => $validType ] ]); 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 ]; } // Classifies a symbol into one of Peter Lynch's 6 stock-shape categories // (Stalwart, Fast Grower, Cyclical, Turnaround, Asset Play, Slow Grower) // using only the 3mo daily price/volume chart data already fetched for the // pre-breakout check — no additional API calls. Only one type is returned // per stock, picked by priority (most specific pattern first). function classifyStockShape($responseBody, $maxPrice = 4.0) { if (!$responseBody) return null; $data = json_decode($responseBody, true); $result = $data['chart']['result'][0] ?? null; if (!$result) return null; $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 < 30) return null; $currentPrice = $prices[$n - 1]; if ($currentPrice > $maxPrice || $currentPrice < 0.5) return null; // Break the period into chunks to read the overall shape of the move $chunkCount = 6; $chunkSize = (int)floor($n / $chunkCount); if ($chunkSize < 3) return null; $chunkAvgs = []; for ($i = 0; $i < $chunkCount; $i++) { $start = $i * $chunkSize; $end = ($i === $chunkCount - 1) ? $n : $start + $chunkSize; $slice = array_slice($prices, $start, $end - $start); if (empty($slice)) continue; $chunkAvgs[] = array_sum($slice) / count($slice); } $chunks = count($chunkAvgs); if ($chunks < 4) return null; $first = $chunkAvgs[0]; $last = $chunkAvgs[$chunks - 1]; $mid = $chunkAvgs[(int)floor($chunks / 2)]; $totalReturnPct = $first > 0 ? (($last - $first) / $first) * 100 : 0; // Daily volatility (stdev of returns) tells us how smooth/choppy the move is $returns = []; for ($i = 1; $i < $n; $i++) { if ($prices[$i - 1] > 0) $returns[] = ($prices[$i] - $prices[$i - 1]) / $prices[$i - 1]; } $meanRet = !empty($returns) ? array_sum($returns) / count($returns) : 0; $variance = 0; foreach ($returns as $r) $variance += pow($r - $meanRet, 2); $variance = !empty($returns) ? $variance / count($returns) : 0; $volatility = sqrt($variance) * 100; // Count wave-like direction changes chunk-to-chunk (for cyclicals) $directionChanges = 0; $prevDir = null; for ($i = 1; $i < $chunks; $i++) { $diff = $chunkAvgs[$i] - $chunkAvgs[$i - 1]; if (abs($diff) < 0.0001) continue; $dir = $diff > 0 ? 1 : -1; if ($prevDir !== null && $dir !== $prevDir) $directionChanges++; $prevDir = $dir; } // Drop then recovery (for turnarounds) $minChunk = min($chunkAvgs); $minIdx = array_search($minChunk, $chunkAvgs); $dropFromFirstPct = $first > 0 ? (($minChunk - $first) / $first) * 100 : 0; $recoveryFromMinPct = $minChunk > 0 ? (($last - $minChunk) / $minChunk) * 100 : 0; // Long flat stretch then a sudden late repricing jump (for asset plays) $earlyChunks = array_slice($chunkAvgs, 0, $chunks - 1); $earlyRangePct = 0; if (!empty($earlyChunks)) { $eMin = min($earlyChunks); $eMax = max($earlyChunks); $earlyRangePct = $eMin > 0 ? (($eMax - $eMin) / $eMin) * 100 : 0; } $lastJumpPct = $mid > 0 ? (($last - $mid) / $mid) * 100 : 0; $type = null; if ($earlyRangePct < 8 && $lastJumpPct > 20) { $type = 'Asset Play'; } elseif ($dropFromFirstPct <= -15 && $recoveryFromMinPct >= 25 && $minIdx > 0 && $minIdx < $chunks - 1) { $type = 'Turnaround'; } elseif ($directionChanges >= 3) { $type = 'Cyclical'; } elseif ($totalReturnPct >= 25 && $last > $mid && $mid > $first) { $type = 'Fast Grower'; } elseif ($totalReturnPct >= 6 && $totalReturnPct < 25 && $volatility < 4) { $type = 'Stalwart'; } elseif ($totalReturnPct > -5 && $totalReturnPct < 8 && $volatility < 2.5) { $type = 'Slow Grower'; } if (!$type) return null; $recentWindow = min(5, $n); $quietWindow = $n - $recentWindow; $volRatio = 0; if ($quietWindow >= 5) { $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; } $sparklinePrices = array_slice($prices, -30); return [ 'type' => $type, '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; } ?>