'_SPX', 'NDX' => '_NDX', 'RUT' => '_RUT', 'VIX' => '_VIX']; const RISK_FREE_RATE = 0.053; const CONTRACT_SIZE = 100; const MAX_EXPIRATIONS = 6; const RANGE_PCT = 0.12; function env_num(string $k, float $fallback): float { $v = getenv($k); return is_numeric($v) ? (float)$v : $fallback; } function nowNY(): array { return ['date' => date('Y-m-d'), 'time' => date('H:i:s'), 'stamp' => date('His')]; } function ensureDir(string $d): void { if (!is_dir($d)) mkdir($d, 0775, true); } function safeNumber($v, float $fallback = 0.0): float { return is_numeric($v) && is_finite((float)$v) ? (float)$v : $fallback; } function clamp(float $v, float $a, float $b): float { return max($a, min($b, $v)); } function sanitizeSymbol($x): string { return substr(preg_replace('/[^A-Z0-9._-]/', '', strtoupper((string)$x)), 0, 16); } function sanitizeMarketSymbol($x): string { return substr(preg_replace('/[^A-Z0-9._=^-]/', '', strtoupper((string)$x)), 0, 24); } function fmt($n, int $d = 2): string { return is_numeric($n) && is_finite((float)$n) ? number_format((float)$n, $d, '.', '') : 'N/A'; } function json_response($obj, int $status = 200): never { http_response_code($status); header('Content-Type: application/json; charset=utf-8'); header('Access-Control-Allow-Origin: *'); header('Access-Control-Allow-Methods: GET,POST,OPTIONS'); header('Access-Control-Allow-Headers: Content-Type'); echo json_encode($obj, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE); exit; } function send_text($body, string $type = 'text/plain', int $status = 200): never { http_response_code($status); header("Content-Type: {$type}; charset=utf-8"); header('Access-Control-Allow-Origin: *'); echo $body; exit; } function controlledError(string $source, Throwable $e, string $extra = ''): array { error_log("[ERROR] {$source}: {$e->getMessage()} {$extra}"); return ['handled' => true, 'source' => $source, 'error' => $e->getMessage(), 'detail' => $extra]; } function readJsonSafe(string $p, $fallback = null) { if (is_file($p)) { $j = json_decode((string)file_get_contents($p), true); return is_array($j) ? $j : $fallback; } return $fallback; } function writeJsonSafe(string $p, $obj): void { ensureDir(dirname($p)); file_put_contents($p, json_encode($obj, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE)); } function http_get_text(string $url, array $headers = [], int $timeout = 15): string { $headerLines = []; foreach ($headers as $k => $v) $headerLines[] = "$k: $v"; if (function_exists('curl_init')) { $ch = curl_init($url); curl_setopt_array($ch, [CURLOPT_RETURNTRANSFER => true, CURLOPT_FOLLOWLOCATION => true, CURLOPT_TIMEOUT => $timeout, CURLOPT_CONNECTTIMEOUT => $timeout, CURLOPT_HTTPHEADER => $headerLines]); $txt = curl_exec($ch); $code = (int)curl_getinfo($ch, CURLINFO_RESPONSE_CODE); $err = curl_error($ch); curl_close($ch); if ($txt === false) throw new RuntimeException("HTTP fetch failed: {$err}"); if ($code >= 400) throw new RuntimeException("HTTP {$code} {$url}: " . substr((string)$txt, 0, 220)); return (string)$txt; } $ctx = stream_context_create(['http' => ['method' => 'GET', 'timeout' => $timeout, 'header' => implode("\r\n", $headerLines)]]); $txt = @file_get_contents($url, false, $ctx); if ($txt === false) throw new RuntimeException("HTTP fetch failed: {$url}"); return (string)$txt; } function http_get_json(string $url, array $headers = [], int $timeout = 15): array { $txt = http_get_text($url, $headers, $timeout); $j = json_decode($txt, true); if (!is_array($j)) throw new RuntimeException('Invalid JSON from ' . $url); return $j; } function http_post_json(string $url, array $payload, array $headers = [], int $timeout = 30): array { $body = json_encode($payload, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE); $headers['Content-Type'] = 'application/json'; $headerLines = []; foreach ($headers as $k => $v) $headerLines[] = "$k: $v"; if (function_exists('curl_init')) { $ch = curl_init($url); curl_setopt_array($ch, [CURLOPT_RETURNTRANSFER => true, CURLOPT_FOLLOWLOCATION => true, CURLOPT_TIMEOUT => $timeout, CURLOPT_POST => true, CURLOPT_POSTFIELDS => $body, CURLOPT_HTTPHEADER => $headerLines]); $txt = curl_exec($ch); $code = (int)curl_getinfo($ch, CURLINFO_RESPONSE_CODE); $err = curl_error($ch); curl_close($ch); if ($txt === false) throw new RuntimeException("HTTP POST failed: {$err}"); if ($code >= 400) throw new RuntimeException("HTTP {$code}: " . substr((string)$txt, 0, 400)); $j = json_decode((string)$txt, true); if (!is_array($j)) throw new RuntimeException('Invalid JSON POST response'); return $j; } $ctx = stream_context_create(['http' => ['method' => 'POST', 'timeout' => $timeout, 'header' => implode("\r\n", $headerLines), 'content' => $body]]); $txt = @file_get_contents($url, false, $ctx); if ($txt === false) throw new RuntimeException('HTTP POST failed'); $j = json_decode($txt, true); if (!is_array($j)) throw new RuntimeException('Invalid JSON POST response'); return $j; } function cboeSymbol(string $sym): string { global $CBOE_SYMBOL_MAP; return $CBOE_SYMBOL_MAP[$sym] ?? $sym; } function cboeUrl(string $sym): string { return 'https://cdn.cboe.com/api/global/delayed_quotes/options/' . rawurlencode(cboeSymbol($sym)) . '.json'; } function cboeHeaders(string $symbol): array { return ['User-Agent' => 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 Chrome/125 Safari/537.36', 'Accept' => 'application/json,text/plain,*/*', 'Referer' => 'https://www.cboe.com/delayed_quotes/' . strtolower($symbol) . '/quote_table/', 'Origin' => 'https://www.cboe.com']; } function optionCachePath(string $symbol): string { global $OPTION_CACHE_ROOT; ensureDir($OPTION_CACHE_ROOT); return $OPTION_CACHE_ROOT . DIRECTORY_SEPARATOR . sanitizeSymbol($symbol) . '.json'; } function readOptionCache(string $symbol): ?array { $p = optionCachePath($symbol); return is_file($p) ? readJsonSafe($p, null) : null; } function writeOptionCache(string $symbol, string $text): void { $t = nowNY(); writeJsonSafe(optionCachePath($symbol), ['symbol'=>sanitizeSymbol($symbol),'savedAt'=>round(microtime(true)*1000),'savedAtNY'=>"{$t['date']} {$t['time']} NY",'text'=>$text]); } function getOptionsChain(string $symbol, bool $force = false): array { $sym = sanitizeSymbol($symbol); $cache = readOptionCache($sym); $cacheMs = env_num('OPTION_CHAIN_CACHE_MS', 5*60*1000); $now = round(microtime(true)*1000); if (!$force && $cache && isset($cache['savedAt']) && ($now - (float)$cache['savedAt'] < $cacheMs)) return ['text'=>$cache['text'], 'cacheWarning'=>null, 'fromCache'=>true, 'savedAtNY'=>$cache['savedAtNY'] ?? null]; try { $txt = http_get_text(cboeUrl($sym), cboeHeaders($sym), 15); writeOptionCache($sym, $txt); $t = nowNY(); return ['text'=>$txt,'cacheWarning'=>null,'fromCache'=>false,'savedAtNY'=>"{$t['date']} {$t['time']} NY"]; } catch (Throwable $e) { if ($cache) return ['text'=>$cache['text'], 'cacheWarning'=>'CBOE indisponível/rate limit. Showing last cached data from ' . ($cache['savedAtNY'] ?? 'cache') . '.', 'fromCache'=>true, 'savedAtNY'=>$cache['savedAtNY'] ?? null]; throw $e; } } function normPDF(float $x): float { return exp(-0.5*$x*$x) / sqrt(2*pi()); } function normCDF(float $x): float { $a1=.319381530; $a2=-.356563782; $a3=1.781477937; $a4=-1.821255978; $a5=1.330274429; $L=abs($x); $k=1/(1+.2316419*$L); $w=1-normPDF($L)*($a1*$k+$a2*$k*$k+$a3*$k*$k*$k+$a4*pow($k,4)+$a5*pow($k,5)); return $x<0 ? 1-$w : $w; } function bsGreeks(float $S, float $K, float $T, float $r, float $sigma, bool $isCall): array { if ($S<=0 || $K<=0 || $T<=0 || $sigma<=0) return ['delta'=>0,'gamma'=>0,'theta'=>0,'vega'=>0]; $sqrtT=sqrt($T); $d1=(log($S/$K)+($r+.5*$sigma*$sigma)*$T)/($sigma*$sqrtT); $d2=$d1-$sigma*$sqrtT; $gamma=normPDF($d1)/($S*$sigma*$sqrtT); $vega=$S*normPDF($d1)*$sqrtT/100; $delta=$isCall?normCDF($d1):normCDF($d1)-1; $thetaAnnual= -($S*normPDF($d1)*$sigma)/(2*$sqrtT) - ($isCall ? $r*$K*exp(-$r*$T)*normCDF($d2) : -$r*$K*exp(-$r*$T)*normCDF(-$d2)); return ['delta'=>$delta,'gamma'=>$gamma,'theta'=>$thetaAnnual/365,'vega'=>$vega]; } function impliedVolFallback(string $symbol, float $spot, float $strike, float $tte): float { $base = in_array(strtoupper($symbol), ['SPY','QQQ','DIA','IWM','SPX','NDX','RUT','XLF','XLK','XLE','TLT','HYG'], true) ? .20 : .42; $m=abs(log($strike/$spot)); return clamp($base + min(.40,$m*2.4) + ($tte < 3/365 ? .09 : 0), .08, 1.50); } function parseOptionSymbol($option): ?array { if (!preg_match('/([A-Z0-9._-]+?)(\d{6})([CP])(\d{8})$/', (string)$option, $m)) return null; $yy=(int)substr($m[2],0,2); $mm=(int)substr($m[2],2,2); $dd=(int)substr($m[2],4,2); return ['exp'=>sprintf('%04d-%02d-%02d', 2000+$yy, $mm, $dd), 'isCall'=>$m[3]==='C', 'strike'=>((float)$m[4])/1000]; } function getSpot(array $data): float { foreach(['current_price','currentPrice','last','close','price','underlying_price'] as $k) { $v = safeNumber($data[$k] ?? 0); if ($v) return $v; } return 0; } function midPrice(array $o): float { $bid=safeNumber($o['bid'] ?? 0); $ask=safeNumber($o['ask'] ?? 0); $last=safeNumber($o['last_trade_price'] ?? ($o['last'] ?? ($o['close'] ?? ($o['price'] ?? 0)))); return ($bid>0 && $ask>0) ? (($bid+$ask)/2) : ($last ?: ($bid ?: $ask)); } function tteYears(string $exp): float { $ms = strtotime($exp . ' 16:00:00 America/New_York') - time(); return max($ms/(365*86400), .0007); } function normalizeCboe(array $raw, string $symbol, string $targetMode='today', ?string $expiryFilter=null): array { $data = $raw['data'] ?? $raw; $opts = $data['options'] ?? ($raw['options'] ?? []); $spot = getSpot($data); if (!$spot) throw new RuntimeException("CBOE returned no spot/current_price for {$symbol}."); $all = []; foreach($opts as $o){ $p=parseOptionSymbol($o['option'] ?? ''); if($p) $all[$p['exp']] = true; } $allExpiries = array_keys($all); sort($allExpiries); $expiries = $targetMode === 'tomorrow' ? array_slice($allExpiries,1,MAX_EXPIRATIONS) : array_slice($allExpiries,0,MAX_EXPIRATIONS); if (!$expiries) $expiries = array_slice($allExpiries,0,MAX_EXPIRATIONS); if ($expiryFilter && in_array($expiryFilter,$allExpiries,true)) $expiries = [$expiryFilter]; $expSet = array_flip($expiries); $contracts=[]; foreach($opts as $o){ $p=parseOptionSymbol($o['option'] ?? ''); if(!$p || !isset($expSet[$p['exp']])) continue; if(abs($p['strike']-$spot)/$spot > RANGE_PCT) continue; $oi=safeNumber($o['open_interest'] ?? ($o['openInterest'] ?? ($o['oi'] ?? 0))); $vol=safeNumber($o['volume'] ?? ($o['vol'] ?? 0)); if($oi<1 && $vol<1) continue; $oiChange=safeNumber($o['open_interest_change'] ?? ($o['openInterestChange'] ?? ($o['oi_change'] ?? ($o['oiChange'] ?? ($o['change_open_interest'] ?? 0))))); $iv=safeNumber($o['iv'] ?? ($o['implied_volatility'] ?? ($o['impliedVolatility'] ?? ($o['mid_iv'] ?? 0)))); if($iv>3) $iv/=100; $tte=tteYears($p['exp']); if($iv<=.001) $iv=impliedVolFallback($symbol,$spot,$p['strike'],$tte); $g=bsGreeks($spot,$p['strike'],$tte,RISK_FREE_RATE,$iv,$p['isCall']); $contracts[]=['symbol'=>$symbol,'exp'=>$p['exp'],'isCall'=>$p['isCall'],'strike'=>$p['strike'],'oi'=>$oi,'volume'=>$vol,'iv'=>$iv,'tte'=>$tte,'bid'=>safeNumber($o['bid'] ?? 0),'ask'=>safeNumber($o['ask'] ?? 0),'last'=>safeNumber($o['last_trade_price'] ?? ($o['last'] ?? 0)),'mid'=>midPrice($o),'delta'=>$g['delta'],'gamma'=>$g['gamma'],'theta'=>$g['theta'],'vega'=>$g['vega'],'oiChange'=>$oiChange]; } return ['provider'=>'CBOE delayed + local Black-Scholes','symbol'=>$symbol,'spot'=>$spot,'expiries'=>$expiries,'allExpiries'=>$allExpiries,'targetMode'=>$targetMode,'contracts'=>$contracts,'rawCount'=>count($opts)]; } function previousAnalysisPath(string $symbol): string { global $SNAP_ROOT; return $SNAP_ROOT . DIRECTORY_SEPARATOR . strtoupper($symbol) . DIRECTORY_SEPARATOR . 'latest-analysis.json'; } function loadPreviousAnalysis(string $symbol): ?array { $p=previousAnalysisPath($symbol); return is_file($p) ? readJsonSafe($p,null) : null; } function saveAnalysis(string $symbol, array $analysis): void { global $SNAP_ROOT; $dir=$SNAP_ROOT . DIRECTORY_SEPARATOR . strtoupper($symbol); ensureDir($dir); $t=nowNY(); writeJsonSafe($dir . DIRECTORY_SEPARATOR . "{$t['date']}_{$t['stamp']}.json", $analysis); writeJsonSafe(previousAnalysisPath($symbol), $analysis); } function computeAtPrice(array $contracts, float $price): array { $netGEX=0;$netDEX=0;$grossGEX=0;$grossDEX=0; foreach($contracts as $c){ $g=bsGreeks($price,$c['strike'],$c['tte'],RISK_FREE_RATE,$c['iv'],$c['isCall']); $gex=$c['oi']*$g['gamma']*$price*$price*CONTRACT_SIZE*.01; $dex=$c['oi']*$g['delta']*$price*CONTRACT_SIZE; $signed=$c['isCall']?$gex:-$gex; $netGEX+=$signed; $netDEX+=$dex; $grossGEX+=abs($signed); $grossDEX+=abs($dex);} return compact('netGEX','netDEX','grossGEX','grossDEX'); } function findGammaFlip(array $contracts, float $spot): ?float { $prev=null;$prevP=null;$start=$spot*(1-RANGE_PCT);$end=$spot*(1+RANGE_PCT);$steps=160; for($i=0;$i<=$steps;$i++){ $p=$start+($end-$start)*$i/$steps; $cur=computeAtPrice($contracts,$p)['netGEX']; if($prev!==null && (($prev<0 && $cur>=0)||($prev>=0 && $cur<0))){ $ratio=abs($prev)/(abs($prev)+abs($cur)); return $prevP+$ratio*($p-$prevP); } $prev=$cur;$prevP=$p; } return null; } function aggregateByStrike(array $norm): array { $by=[]; $t=['callOI'=>0,'putOI'=>0,'callVol'=>0,'putVol'=>0,'callOIChange'=>0,'putOIChange'=>0,'callPremium'=>0,'putPremium'=>0,'netGEX'=>0,'netDEX'=>0,'grossGEX'=>0,'grossDEX'=>0,'callDEX'=>0,'putDEX'=>0,'callGEX'=>0,'putGEX'=>0]; foreach($norm['contracts'] as $c){ $gex=$c['oi']*$c['gamma']*$norm['spot']*$norm['spot']*CONTRACT_SIZE*.01; $dex=$c['oi']*$c['delta']*$norm['spot']*CONTRACT_SIZE; $premium=$c['volume']*$c['mid']*CONTRACT_SIZE; $k=(string)$c['strike']; if(!isset($by[$k])) $by[$k]=['strike'=>$c['strike'],'callOI'=>0,'putOI'=>0,'callVol'=>0,'putVol'=>0,'callOIChange'=>0,'putOIChange'=>0,'callPremium'=>0,'putPremium'=>0,'callGEX'=>0,'putGEX'=>0,'netGEX'=>0,'callDEX'=>0,'putDEX'=>0,'netDEX'=>0,'pressure'=>0]; if($c['isCall']){ $by[$k]['callOI']+=$c['oi'];$by[$k]['callVol']+=$c['volume'];$by[$k]['callOIChange']+=$c['oiChange'];$by[$k]['callPremium']+=$premium;$by[$k]['callGEX']+=$gex;$by[$k]['callDEX']+=$dex; foreach(['callOI'=>'oi','callVol'=>'volume','callOIChange'=>'oiChange'] as $tk=>$ck){} $t['callOI']+=$c['oi'];$t['callVol']+=$c['volume'];$t['callOIChange']+=$c['oiChange'];$t['callPremium']+=$premium;$t['callGEX']+=$gex;$t['callDEX']+=$dex; } else { $by[$k]['putOI']+=$c['oi'];$by[$k]['putVol']+=$c['volume'];$by[$k]['putOIChange']+=$c['oiChange'];$by[$k]['putPremium']+=$premium;$by[$k]['putGEX']+=$gex;$by[$k]['putDEX']+=$dex; $t['putOI']+=$c['oi'];$t['putVol']+=$c['volume'];$t['putOIChange']+=$c['oiChange'];$t['putPremium']+=$premium;$t['putGEX']+=$gex;$t['putDEX']+=$dex; } $r=&$by[$k]; $r['netGEX']=$r['callGEX']-$r['putGEX']; $r['netDEX']=$r['callDEX']+$r['putDEX']; $r['netOIChange']=$r['callOIChange']-$r['putOIChange']; $r['volumeTotal']=$r['callVol']+$r['putVol']; $r['oiTotal']=$r['callOI']+$r['putOI']; $r['volumeOIRatio']=$r['oiTotal']>0?$r['volumeTotal']/$r['oiTotal']:0; $r['pressure']=$r['netDEX']+$r['netGEX']*8; unset($r); } $t['totalOI']=$t['callOI']+$t['putOI']; $t['totalVolume']=$t['callVol']+$t['putVol']; $t['totalOIChange']=$t['callOIChange']+$t['putOIChange']; $t['netOIChange']=$t['callOIChange']-$t['putOIChange']; $t['volumeOIRatio']=$t['totalOI']>0?$t['totalVolume']/$t['totalOI']:0; $t['netGEX']=$t['callGEX']-$t['putGEX']; $t['netDEX']=$t['callDEX']+$t['putDEX']; $t['grossGEX']=$t['callGEX']+$t['putGEX']; $t['grossDEX']=abs($t['callDEX'])+abs($t['putDEX']); $strikes=array_values($by); usort($strikes, fn($a,$b)=>$a['strike']<=>$b['strike']); return ['strikes'=>$strikes,'totals'=>$t]; } function expirationOutlook(array $norm): array { $by=[]; foreach($norm['contracts'] as $c){ $e=$c['exp']; if(!isset($by[$e])) $by[$e]=['exp'=>$e,'callOI'=>0,'putOI'=>0,'callVol'=>0,'putVol'=>0,'callGEX'=>0,'putGEX'=>0,'callDEX'=>0,'putDEX'=>0,'callOIChange'=>0,'putOIChange'=>0,'targetCall'=>null,'targetPut'=>null,'targetCallForce'=>-1,'targetPutForce'=>-1]; $gex=$c['oi']*$c['gamma']*$norm['spot']*$norm['spot']*CONTRACT_SIZE*.01; $dex=$c['oi']*$c['delta']*$norm['spot']*CONTRACT_SIZE; $force=abs($gex)+abs($dex)/10+($c['volume']??0)*500+($c['oiChange']??0)*250; if($c['isCall']){ $by[$e]['callOI']+=$c['oi'];$by[$e]['callVol']+=$c['volume'];$by[$e]['callGEX']+=$gex;$by[$e]['callDEX']+=$dex;$by[$e]['callOIChange']+=$c['oiChange']; if($c['strike']>=$norm['spot'] && $force>$by[$e]['targetCallForce']){ $by[$e]['targetCall']=$c['strike'];$by[$e]['targetCallForce']=$force; } } else { $by[$e]['putOI']+=$c['oi'];$by[$e]['putVol']+=$c['volume'];$by[$e]['putGEX']+=$gex;$by[$e]['putDEX']+=$dex;$by[$e]['putOIChange']+=$c['oiChange']; if($c['strike']<=$norm['spot'] && $force>$by[$e]['targetPutForce']){ $by[$e]['targetPut']=$c['strike'];$by[$e]['targetPutForce']=$force; } } } ksort($by); $out=[]; foreach($by as $x){ $netGEX=$x['callGEX']-$x['putGEX']; $netDEX=$x['callDEX']+$x['putDEX']; $netOIChange=$x['callOIChange']-$x['putOIChange']; $callDom=$x['callGEX']+$x['callDEX']/10+$x['callVol']*500+$x['callOIChange']*250; $putDom=$x['putGEX']+abs($x['putDEX'])/10+$x['putVol']*500+$x['putOIChange']*250; $bias=$callDom>$putDom*1.15?'BULLISH':($putDom>$callDom*1.15?'BEARISH':'NEUTRAL'); $target=$bias==='BEARISH'?($x['targetPut']??$x['targetCall']):($x['targetCall']??$x['targetPut']); $out[]=['expiration'=>$x['exp'],'bias'=>$bias,'target'=>$target,'callTarget'=>$x['targetCall'],'putTarget'=>$x['targetPut'],'distanceToTargetPct'=>is_numeric($target)?($target-$norm['spot'])/$norm['spot']*100:null,'score'=>(int)round(clamp((abs($netGEX)/(abs($x['callGEX'])+abs($x['putGEX'])+1))*35+(abs($netDEX)/(abs($x['callDEX'])+abs($x['putDEX'])+1))*25+log10(1+$x['callVol']+$x['putVol'])*8+log10(1+abs($netOIChange))*6,0,100)),'netGEX'=>$netGEX,'netDEX'=>$netDEX,'netOIChange'=>$netOIChange,'totalVolume'=>$x['callVol']+$x['putVol'],'totalOI'=>$x['callOI']+$x['putOI']]; } return $out; } function nearestStep(string $symbol, float $spot): float { if($spot<50)return .5; if($spot<250)return 1; if($spot<1000)return 2.5; return 5; } function roundLevel(string $symbol, float $x): float { $s=nearestStep($symbol,$x); return round($x/$s)*$s; } function findLevels(array $norm, array $aggr): array { $spot=$norm['spot']; $callWall=null;$putWall=null;$magnet=null;$deltaWall=null;$maxCall=-1;$maxPut=-1;$maxAbs=-1;$maxDelta=-1; foreach($aggr['strikes'] as $r){ if($r['strike']>=$spot && $r['callGEX']>$maxCall){$maxCall=$r['callGEX'];$callWall=$r['strike'];} if($r['strike']<=$spot && $r['putGEX']>$maxPut){$maxPut=$r['putGEX'];$putWall=$r['strike'];} $abs=abs($r['netGEX'])+abs($r['netDEX'])/10; if($abs>$maxAbs){$maxAbs=$abs;$magnet=$r['strike'];} if(abs($r['netDEX'])>$maxDelta){$maxDelta=abs($r['netDEX']);$deltaWall=$r['strike'];} } $gammaFlip=findGammaFlip($norm['contracts'],$spot); return ['callWall'=>$callWall,'putWall'=>$putWall,'magnet'=>$magnet,'gammaFlip'=>$gammaFlip,'deltaWall'=>$deltaWall,'dsrp'=>$gammaFlip ?: ($magnet ?: roundLevel($norm['symbol'],$spot))]; } function scaleLog($v, float $divisor=1): float { return log10(1+max(0,abs((float)$v))/$divisor); } function computeCandidateScore(array $t): int { return (int)round(clamp(clamp(scaleLog($t['totalOI']??0,1000)*18,0,35)+clamp(scaleLog($t['totalOIChange']??0,100)*22,0,35)+clamp(scaleLog($t['totalVolume']??0,500)*15,0,20)+clamp(($t['volumeOIRatio']??0)*18,0,10),0,100)); } function computePositionBuilding(?array $prev, array $t): array { $now=round(microtime(true)*1000); $prevTs=$prev['createdAtMs']??0; $hours=$prevTs?max(($now-$prevTs)/3600000,0.05):0; $prevOI=$prev['metrics']['totalOI']??0; $prevVelocity=$prev['metrics']['oiVelocity']??0; $oiVelocity=$hours?($t['totalOI']-$prevOI)/$hours:0; $oiAcceleration=$hours?($oiVelocity-$prevVelocity)/$hours:0; $score=computeCandidateScore($t)*.45+clamp(scaleLog(abs($oiVelocity),100)*18,0,30)+clamp(scaleLog(abs($oiAcceleration),50)*18,0,25); return ['oiVelocity'=>$oiVelocity,'oiAcceleration'=>$oiAcceleration,'positionBuildingScore'=>(int)round(clamp($score,0,100))]; } function classifyRegime(array $norm, array $aggr, array $levels, ?array $prev): array { $t=$aggr['totals']; $distFlip=$levels['gammaFlip']?($norm['spot']-$levels['gammaFlip'])/$norm['spot']*100:0; $shortGamma=$t['netGEX']<0; $regime='RANGE / PINNING';$bias='NEUTRAL';$explanation='Dealers likely dampen movement unless a wall fails.'; if($shortGamma && $distFlip>0){$regime='SHORT GAMMA UPSIDE ACCELERATION';$bias='BULLISH ABOVE DSRP';$explanation='Negative GEX and spot above flip imply hedging can chase upside.';} elseif($shortGamma && $distFlip<0){$regime='SHORT GAMMA DOWNSIDE ACCELERATION';$bias='BEARISH BELOW DSRP';$explanation='Negative GEX and spot below flip imply hedging can chase downside.';} elseif(!$shortGamma && abs($distFlip)<.30){$regime='DEALER DEFENSE / PINNING';$bias='NEUTRAL TO RANGE';$explanation='Positive GEX near flip/magnet favors defense and mean reversion.';} elseif(!$shortGamma && $distFlip>0){$regime='LONG GAMMA ABOVE DEFENSE';$bias='NEUTRAL-BULLISH';} else {$regime='LONG GAMMA BELOW DEFENSE';$bias='NEUTRAL-BEARISH';} $hedgeNeed=-$t['netDEX']; $prevHedge=$prev['metrics']['dealerHedgeNeed']??0; $hedgeChange=$hedgeNeed-$prevHedge; $premiumNet=$t['callPremium']-$t['putPremium']; $pressureRaw=(abs($hedgeChange)/(max(1,$t['grossDEX']))*45)+(abs($t['netGEX'])/(max(1,$t['grossGEX']))*25)+(abs($premiumNet)/(max(1,$t['callPremium']+$t['putPremium']))*20)+($shortGamma?10:0); $pressureScore=(int)round(clamp($pressureRaw,0,100)); $heatScore=(int)round(clamp($pressureScore*.55+(abs($premiumNet)/max(1,$t['callPremium']+$t['putPremium']))*25+(count($norm['contracts'])>500?20:count($norm['contracts'])/25),0,100)); $candidateScore=computeCandidateScore($t); $building=computePositionBuilding($prev,$t); $opportunityScore=(int)round(clamp($heatScore*.25+$pressureScore*.25+$candidateScore*.20+$building['positionBuildingScore']*.25+(abs($t['volumeOIRatio']??0)*5),0,100)); return ['regime'=>$regime,'bias'=>$bias,'explanation'=>$explanation,'shortGamma'=>$shortGamma,'distFlip'=>$distFlip,'metrics'=>array_merge(['dealerHedgeNeed'=>$hedgeNeed,'hedgeChange'=>$hedgeChange,'pressureScore'=>$pressureScore,'heatScore'=>$heatScore,'candidateScore'=>$candidateScore,'opportunityScore'=>$opportunityScore,'netPremium'=>$premiumNet],$building)]; } function parseFirstTarget($txt): ?float { return preg_match('/[-+]?\d+(?:\.\d+)?/', (string)$txt, $m) ? (float)$m[0] : null; } function opportunityQuality($distancePct, $progressPct, $rr): string { if(!is_numeric($distancePct)) return 'UNKNOWN'; $d=abs((float)$distancePct); if($d>=2 && (!is_numeric($progressPct)||$progressPct<70) && $rr>=1.4)return'EXCELLENT'; if($d>=1 && (!is_numeric($progressPct)||$progressPct<80) && $rr>=1.0)return'GOOD'; if($d>=.5)return'LIMITED'; return 'EXHAUSTED / TOO CLOSE TO TARGET'; } function makeTradingPlan(array $norm, array $aggr, array $levels, array $regime, array $dailyContext=[]): array { $spot=$norm['spot'];$dsrp=$levels['dsrp']?:$spot;$step=nearestStep($norm['symbol'],$spot);$longTrigger=roundLevel($norm['symbol'],$dsrp+$step*.2);$shortTrigger=roundLevel($norm['symbol'],$dsrp-$step*.2);$upside=$levels['callWall']&&$levels['callWall']>$spot?fmt($levels['callWall']).' → '.fmt($levels['callWall']+$step):($levels['magnet']?fmt($levels['magnet']):fmt($spot+$step));$downside=$levels['putWall']&&$levels['putWall']<$spot?fmt($levels['putWall']).' → '.fmt($levels['putWall']-$step):($levels['magnet']?fmt($levels['magnet']):fmt($spot-$step));$best="WAIT for reaction around ".fmt($dsrp); if(str_contains($regime['bias'],'BULL'))$best='LONG above '.fmt($longTrigger).' toward '.$upside; if(str_contains($regime['bias'],'BEAR'))$best='SHORT below '.fmt($shortTrigger).' toward '.$downside; if(str_contains($regime['bias'],'RANGE')||str_contains($regime['bias'],'NEUTRAL'))$best='Do not chase. Buy defense near '.fmt($levels['putWall']?:$dsrp).' or sell rejection near '.fmt($levels['callWall']?:$dsrp).'.'; $target=parseFirstTarget(str_contains($regime['bias'],'BEAR')?$downside:$upside); $stop=str_contains($regime['bias'],'BEAR')?$longTrigger:$shortTrigger; $dist=$target?($target-$spot)/$spot*100:null; $risk=$stop?abs(($spot-$stop)/$spot*100):null; $reward=is_numeric($dist)?abs($dist):null; $rr=($reward && $risk)?$reward/$risk:null; $progress=($target && $target!=$dsrp)?clamp((($spot-$dsrp)/($target-$dsrp))*100,0,200):null; return ['date'=>nowNY()['date'],'macroContext'=>'LLM catalyst check is separate. Quant bias is based only on CBOE options structure and local Greeks.','keyOpeningLevel'=>fmt($dsrp-$step*.1).'–'.fmt($dsrp+$step*.1),'currentReference'=>fmt($spot),'mainBias'=>$regime['bias'],'dsrp'=>$dsrp,'upsideTarget'=>$upside,'bearishTarget'=>$downside,'openingTrade'=>'If '.$norm['symbol'].' opens/holds above '.fmt($longTrigger).', prefer LONG toward '.$upside.'. If it loses '.fmt($shortTrigger).', avoid long.','invalidIf'=>'Bullish plan invalid below '.fmt($shortTrigger).'. Bearish plan invalid above '.fmt($longTrigger).'.','bestTrade'=>$best,'finalDecision'=>'Best button-press trade: '.$best,'marketContext'=>array_merge($dailyContext,['currentPrice'=>$spot,'targetNumber'=>$target,'stopNumber'=>$stop,'distanceToTargetPct'=>$dist,'distanceToStretchPct'=>$target?(($target+(str_contains($regime['bias'],'BEAR')?-$step:$step)-$spot)/$spot*100):null,'potentialRiskPct'=>$risk,'potentialRewardPct'=>$reward,'riskReward'=>$rr,'progressToTargetPct'=>$progress,'opportunityQuality'=>opportunityQuality($dist,$progress,$rr?:0)]),'trades'=>[['name'=>'Opening Directional Trade','action'=>str_contains($regime['bias'],'BEAR')?'SHORT below '.fmt($shortTrigger):'LONG above '.fmt($longTrigger),'target'=>str_contains($regime['bias'],'BEAR')?$downside:$upside,'stop'=>str_contains($regime['bias'],'BEAR')?'Reclaim above '.fmt($longTrigger):'Loss below '.fmt($shortTrigger),'condition'=>'Use only after first reaction confirms the DSRP side.']]]; } function fetchYahooLatestPrice(string $symbol): ?array { try { $j=http_get_json('https://query1.finance.yahoo.com/v8/finance/chart/'.rawurlencode($symbol).'?range=1d&interval=1m&includePrePost=true',['User-Agent'=>'Mozilla/5.0','Accept'=>'application/json']); $r=$j['chart']['result'][0]??null; $q=$r['indicators']['quote'][0]??[]; $closes=array_values(array_filter($q['close']??[], 'is_numeric')); if(!$closes)return null; $highs=array_values(array_filter($q['high']??[], 'is_numeric')); $lows=array_values(array_filter($q['low']??[], 'is_numeric')); return ['price'=>(float)end($closes),'dayHigh'=>$highs?max($highs):null,'dayLow'=>$lows?min($lows):null,'source'=>'Yahoo 1m']; } catch(Throwable $e){ return null; } } function fetchYahooPrices(array $symbols): array { $out=[]; foreach(array_slice(array_filter(array_map('sanitizeSymbol',$symbols)),0,160) as $s){ $p=fetchYahooLatestPrice($s); if($p)$out[$s]=$p; } return $out; } function fetchYahooDailyContext(string $symbol): array { try { $j=http_get_json('https://query1.finance.yahoo.com/v8/finance/chart/'.rawurlencode($symbol).'?range=10d&interval=1d&includePrePost=false',['User-Agent'=>'Mozilla/5.0','Accept'=>'application/json']); $r=$j['chart']['result'][0]??null; $q=$r['indicators']['quote'][0]??[]; $ts=$r['timestamp']??[]; $rows=[]; foreach($ts as $i=>$t){ if(is_numeric($q['close'][$i]??null)) $rows[]=['open'=>$q['open'][$i]??null,'high'=>$q['high'][$i]??null,'low'=>$q['low'][$i]??null,'close'=>$q['close'][$i]??null,'volume'=>$q['volume'][$i]??null]; } $prev=count($rows)>=2?$rows[count($rows)-2]:($rows[0]??null); return $prev?['previousOpen'=>$prev['open'],'previousClose'=>$prev['close'],'previousHigh'=>$prev['high'],'previousLow'=>$prev['low'],'previousVolume'=>$prev['volume']]:[]; } catch(Throwable $e){return [];} } function fetchYahooChart(string $symbol, string $interval='1m'): array { $symbol=sanitizeMarketSymbol($symbol); $safe=in_array($interval,['1m','5m','15m','1h'],true)?$interval:'1m'; $j=http_get_json('https://query1.finance.yahoo.com/v8/finance/chart/'.rawurlencode($symbol).'?range=1d&interval='.rawurlencode($safe).'&includePrePost=true',['User-Agent'=>'Mozilla/5.0','Accept'=>'application/json']); $r=$j['chart']['result'][0]??null; if(!$r) throw new RuntimeException("No Yahoo chart result for {$symbol}"); $q=$r['indicators']['quote'][0]??[]; $out=[]; foreach(($r['timestamp']??[]) as $i=>$t){ if(is_numeric($q['close'][$i]??null)) $out[]=['time'=>$t,'open'=>$q['open'][$i]??null,'high'=>$q['high'][$i]??null,'low'=>$q['low'][$i]??null,'close'=>$q['close'][$i],'volume'=>$q['volume'][$i]??null]; } return $out; } function fetchYahooHeadlines(string $symbol): array { try { $rss=http_get_text('https://feeds.finance.yahoo.com/rss/2.0/headline?s='.rawurlencode($symbol).'®ion=US&lang=en-US',['User-Agent'=>'Mozilla/5.0','Accept'=>'application/rss+xml,text/xml']); preg_match_all('/[\s\S]*?<!\[CDATA\[([\s\S]*?)\]\]><\/title>[\s\S]*?<pubDate>([\s\S]*?)<\/pubDate>[\s\S]*?<\/item>/', $rss, $m, PREG_SET_ORDER); return array_slice(array_map(fn($x)=>['title'=>trim(preg_replace('/\s+/',' ',$x[1])),'pubDate'=>trim($x[2])], $m),0,8); } catch(Throwable $e){return [];} } function analyzeSymbol(string $symbol, string $targetMode='today', ?array $cboeOverride=null, ?string $expiryFilter=null): array { $cboe=$cboeOverride ?: getOptionsChain($symbol,false); if(empty($cboe['text'])) throw new RuntimeException($cboe['cacheWarning'] ?? 'No CBOE data available.'); $raw=json_decode($cboe['text'], true); if(!is_array($raw)) throw new RuntimeException('Invalid CBOE JSON.'); $norm=normalizeCboe($raw,sanitizeSymbol($symbol),$targetMode,$expiryFilter); $live=fetchYahooLatestPrice($symbol); if($live && !empty($live['price']) && abs($live['price']-$norm['spot'])/max(1,$norm['spot'])<.20){ $norm['cboeSpot']=$norm['spot']; $norm['spot']=$live['price']; $norm['livePriceSource']=$live['source']; $norm['dayHigh']=$live['dayHigh']; $norm['dayLow']=$live['dayLow']; } if(count($norm['contracts'])<10) throw new RuntimeException('Too few usable option contracts for '.$symbol.'.'); $prev=loadPreviousAnalysis($symbol); $aggr=aggregateByStrike($norm); $levels=findLevels($norm,$aggr); $regime=classifyRegime($norm,$aggr,$levels,$prev); $plan=makeTradingPlan($norm,$aggr,$levels,$regime,fetchYahooDailyContext($symbol)); $min=$norm['spot']*(1-.045);$max=$norm['spot']*(1+.045); $strikes=[]; foreach($aggr['strikes'] as $x){ if($x['strike']>=$min && $x['strike']<=$max){ $x['netPremium']=($x['callPremium']??0)-($x['putPremium']??0); $x['effectiveOI']=$x['oiTotal']; $strikes[]=$x; }} $today=nowNY()['date']; $expDte=[]; foreach($norm['allExpiries'] as $exp){ $dte=(int)round((strtotime($exp.' 16:00:00 America/New_York')-strtotime($today.' 16:00:00 America/New_York'))/86400); if($dte>=0 && $dte<=10) $expDte[]=['exp'=>$exp,'dte'=>$dte]; } $analysis=['symbol'=>sanitizeSymbol($symbol),'cacheWarning'=>$cboe['cacheWarning']??null,'provider'=>$norm['provider'],'livePriceSource'=>$norm['livePriceSource']??'CBOE delayed','cboeSpot'=>$norm['cboeSpot']??null,'dayHigh'=>$norm['dayHigh']??null,'dayLow'=>$norm['dayLow']??null,'timestampNY'=>nowNY()['date'].' '.nowNY()['time'].' NY','createdAtMs'=>round(microtime(true)*1000),'targetMode'=>$norm['targetMode'],'selectedExpiry'=>$expiryFilter,'expiriesWithDte'=>$expDte,'fromCache'=>!empty($cboe['fromCache']),'cboeSavedAtNY'=>$cboe['savedAtNY']??null,'spot'=>$norm['spot'],'expiries'=>$norm['expiries'],'allExpiries'=>$norm['allExpiries'],'expirationOutlook'=>expirationOutlook($norm),'contractCount'=>count($norm['contracts']),'levels'=>$levels,'regime'=>['regime'=>$regime['regime'],'bias'=>$regime['bias'],'explanation'=>$regime['explanation'],'shortGamma'=>$regime['shortGamma'],'distFlip'=>$regime['distFlip']],'metrics'=>array_merge($aggr['totals'],$regime['metrics']),'tradingPlan'=>$plan,'strikes'=>$strikes]; $analysis['localNetDEX']=array_sum(array_column($strikes,'netDEX')); $analysis['localNetGEX']=array_sum(array_column($strikes,'netGEX')); saveAnalysis($symbol,$analysis); return $analysis; } function scanSymbols(array $list, string $targetMode='today', bool $force=false): array { $max=(int)env_num('MAX_SCAN_SYMBOLS',40); $unique=array_values(array_unique(array_slice(array_filter(array_map('sanitizeSymbol',$list)),0,$max))); $out=[]; foreach($unique as $s){ try{$out[]=analyzeSymbol($s,$targetMode);} catch(Throwable $e){$out[]=['symbol'=>$s,'error'=>$e->getMessage(),'metrics'=>['heatScore'=>0,'pressureScore'=>0,'opportunityScore'=>0],'regime'=>['bias'=>'ERROR','regime'=>'ERROR']];} } usort($out, fn($a,$b)=>($b['metrics']['opportunityScore']??0)<=>($a['metrics']['opportunityScore']??0)); $out['batchInfo']=['createdAtNY'=>nowNY()['date'].' '.nowNY()['time'].' NY','fetched'=>null,'usedStale'=>null,'errors'=>count(array_filter($out,fn($x)=>isset($x['error']))),'cacheStatus'=>'PHP sequential scan']; return $out; } function boardSessionLogPath(string $date, string $symbol, ?string $expiry=null): string { global $SNAP_ROOT; ensureDir($SNAP_ROOT . DIRECTORY_SEPARATOR . strtoupper($symbol)); return $SNAP_ROOT . DIRECTORY_SEPARATOR . strtoupper($symbol) . DIRECTORY_SEPARATOR . 'board-session-' . $date . ($expiry?'-'.$expiry:'') . '.jsonl'; } function appendBoardSessionLog(string $date, string $symbol, array $entry, ?string $expiry=null): void { file_put_contents(boardSessionLogPath($date,$symbol,$expiry), json_encode($entry, JSON_UNESCAPED_SLASHES|JSON_UNESCAPED_UNICODE)."\n", FILE_APPEND); } function readBoardSessionLog(string $date, string $symbol, ?string $expiry=null): array { $p=boardSessionLogPath($date,$symbol,$expiry); if(!is_file($p))return[]; $rows=[]; foreach(file($p, FILE_IGNORE_NEW_LINES|FILE_SKIP_EMPTY_LINES) as $l){$j=json_decode($l,true); if(is_array($j))$rows[]=$j;} return $rows; } function computeRegimePersistence(string $date, string $symbol, string $label, ?string $expiry=null): int { $rows=array_reverse(readBoardSessionLog($date,$symbol,$expiry)); $c=1; foreach($rows as $r){ if(($r['regime']??'')===$label)$c++; else break; } return $c; } function freezeOpeningHypothesis(string $date, string $symbol, array $current, ?string $expiry=null): array { global $SNAP_ROOT; $dir=$SNAP_ROOT . DIRECTORY_SEPARATOR . strtoupper($symbol); ensureDir($dir); $p=$dir . DIRECTORY_SEPARATOR . 'opening-hypothesis-' . $date . ($expiry?'-'.$expiry:'') . '.json'; if(is_file($p)) return readJsonSafe($p, []); $f=['frozenAtNY'=>$current['timestampNY'],'spot'=>$current['spot'],'levels'=>$current['levels'],'regime'=>$current['regime'],'tradingPlan'=>$current['tradingPlan']]; writeJsonSafe($p,$f); return $f; } function buildOpeningBoardSnapshot(string $symbol, ?string $expiry=null): array { $sym=sanitizeSymbol($symbol); $today=nowNY()['date']; $current=analyzeSymbol($sym,'today',null,$expiry); $hyp=freezeOpeningHypothesis($today,$sym,$current,$expiry); $label=$current['regime']['regime']??'UNKNOWN'; $persistence=computeRegimePersistence($today,$sym,$label,$expiry); $spot=$current['spot']; $rows=$current['strikes']; $below=null;$above=null; foreach($rows as $r){ if($r['strike']<$spot && (!$below || $r['strike']>$below['strike']))$below=$r; if($r['strike']>$spot && (!$above || $r['strike']<$above['strike']))$above=$r; } $entry=['ts'=>round(microtime(true)*1000),'timeNY'=>$current['timestampNY'],'spot'=>$spot,'regime'=>$label,'bias'=>$current['regime']['bias']??null,'magnet'=>$current['levels']['magnet']??null,'callWall'=>$current['levels']['callWall']??null,'putWall'=>$current['levels']['putWall']??null,'gammaFlip'=>$current['levels']['gammaFlip']??null,'distFlipPct'=>$current['regime']['distFlip']??null,'persistence'=>$persistence,'nearestSupport'=>$below['strike']??null,'supportStatus'=>$below?(($below['netDEX']>0)?'Held':'Frágil'):null,'nearestResistance'=>$above['strike']??null,'resistanceStatus'=>$above?(($above['netDEX']<0)?'Held':'Frágil'):null,'netPremium'=>$current['metrics']['netPremium']??null,'netGEX'=>$current['metrics']['netGEX']??null,'netDEX'=>$current['metrics']['netDEX']??null,'totalOI'=>$current['metrics']['totalOI']??null,'totalOIChange'=>$current['metrics']['totalOIChange']??null,'positionBuildingScore'=>$current['metrics']['positionBuildingScore']??null]; appendBoardSessionLog($today,$sym,$entry,$expiry); return ['symbol'=>$sym,'date'=>$today,'historical'=>false,'current'=>$current,'openingHypothesis'=>$hyp,'gammaRamps'=>[],'regimePersistence'=>$persistence,'tableRow'=>$entry,'table'=>readBoardSessionLog($today,$sym,$expiry),'expiriesWithDte'=>$current['expiriesWithDte']??[],'selectedExpiry'=>$expiry]; } function llmCachePath(string $symbol): string { global $CACHE_ROOT; ensureDir($CACHE_ROOT); return $CACHE_ROOT . DIRECTORY_SEPARATOR . sanitizeSymbol($symbol) . '.json'; } function summarizeQuantForLLM(array $a): array { return ['symbol'=>$a['symbol'],'timestampNY'=>$a['timestampNY'],'spot'=>$a['spot'],'regime'=>$a['regime'],'levels'=>$a['levels'],'expirationOutlook'=>$a['expirationOutlook'],'metrics'=>['heatScore'=>$a['metrics']['heatScore']??null,'pressureScore'=>$a['metrics']['pressureScore']??null,'opportunityScore'=>$a['metrics']['opportunityScore']??null,'netGEX'=>$a['metrics']['netGEX']??null,'netDEX'=>$a['metrics']['netDEX']??null,'dealerHedgeNeed'=>$a['metrics']['dealerHedgeNeed']??null,'hedgeChange'=>$a['metrics']['hedgeChange']??null,'netPremium'=>$a['metrics']['netPremium']??null],'tradingPlan'=>$a['tradingPlan'],'strikeTable'=>array_map(fn($x)=>['strike'=>$x['strike'],'netGEX'=>$x['netGEX'],'netDEX'=>$x['netDEX'],'oiDelta'=>$x['netOIChange']??0,'volume'=>$x['volumeTotal']??0,'pressure'=>$x['pressure']??0], $a['strikes']??[])]; } function callOpenAI(array $messages): ?string { $key=getenv('OPENAI_API_KEY') ?: ''; if(!$key)return null; $j=http_post_json('https://api.openai.com/v1/chat/completions',['model'=>getenv('OPENAI_MODEL') ?: 'gpt-4.1-mini','temperature'=>0.2,'messages'=>$messages],['Authorization'=>'Bearer '.$key],45); return $j['choices'][0]['message']['content'] ?? ''; } function makeAITradeFromQuant(array $a): array { $bias=$a['regime']['bias']??''; $spot=(float)($a['spot']??0); $l=$a['levels']??[]; $mc=$a['tradingPlan']['marketContext']??[]; $target=$mc['targetNumber']??($l['callWall']??$spot); $stop=$mc['stopNumber']??($l['putWall']??($l['dsrp']??$spot)); $action='WAIT'; $entry=$spot; $reason='Wait for clean trigger.'; if(str_contains($bias,'BULL')){$action='LONG';$entry=max($l['dsrp']??$spot,$spot);$reason='Bullish dealer structure.';} elseif(str_contains($bias,'BEAR')){$action='SHORT';$entry=min($l['dsrp']??$spot,$spot);$reason='Bearish dealer structure.';} $m=$a['metrics']??[]; $conf=max(35,min(90,round(($m['opportunityScore']??0)*.40+($m['heatScore']??0)*.25+($m['pressureScore']??0)*.25+10))); return compact('action','entry','stop','target')+['confidence'=>$conf,'reason'=>$reason]; } function llmAnalysis(string $symbol, bool $force=false): array { $symbol=sanitizeSymbol($symbol); $latest=loadPreviousAnalysis($symbol) ?: analyzeSymbol($symbol); $cache=readJsonSafe(llmCachePath($symbol), null); $quantHash=json_encode(['levels'=>$latest['levels'],'regime'=>$latest['regime'],'metrics'=>['heat'=>$latest['metrics']['heatScore']??null,'pressure'=>$latest['metrics']['pressureScore']??null]], JSON_UNESCAPED_SLASHES); if($cache && !$force && (round(microtime(true)*1000)-($cache['createdAt']??0)<env_num('LLM_CACHE_MS',5*60*1000)) && ($cache['quantHash']??'')===$quantHash) return $cache+['cached'=>true]; $headlines=fetchYahooHeadlines($symbol); $content=null; try{$content=callOpenAI([['role'=>'system','content'=>'Você é um assistente institucional de day trade especializado em dealer positioning. Responda em português, objetivo.'],['role'=>'user','content'=>'Analyze '.$symbol.'. Quant data: '.json_encode(summarizeQuantForLLM($latest), JSON_UNESCAPED_UNICODE).'. Recent headlines: '.json_encode($headlines, JSON_UNESCAPED_UNICODE)]]);}catch(Throwable $e){} if(!$content){$h=$headlines?implode("\n",array_map(fn($x)=>'- '.$x['title'],array_slice($headlines,0,3))):'Sem headlines recentes disponíveis.'; $content="Catalyst Check\n{$h}\n\nLeitura automática sem LLM. Regime atual: ".($latest['regime']['bias']??'N/A').". Pressure ".($latest['metrics']['pressureScore']??'N/A')."/100. Melhor plano: ".($latest['tradingPlan']['bestTrade']??'N/A').'.';} $out=['symbol'=>$symbol,'createdAt'=>round(microtime(true)*1000),'createdAtNY'=>nowNY()['date'].' '.nowNY()['time'].' NY','quantHash'=>$quantHash,'answer'=>$content,'headlines'=>$headlines,'aiTrade'=>makeAITradeFromQuant($latest),'cached'=>false]; writeJsonSafe(llmCachePath($symbol),$out); return $out; } function llmScalpChat(array $payload): array { $symbol=sanitizeSymbol($payload['symbol']??'QQQ'); $quant=$payload['quant']??(loadPreviousAnalysis($symbol) ?: analyzeSymbol($symbol)); $answer='Sem OPENAI_API_KEY ativa ou falha na LLM. Resumo automático: '.($quant['regime']['bias']??'N/A').' · Pressure '.($quant['metrics']['pressureScore']??'N/A').'/100 · Spot '.fmt($quant['spot']??0).'.'; try{ $q=substr((string)($payload['question']??''),0,4000); $res=callOpenAI([['role'=>'system','content'=>'Você é um copiloto de scalp em português. Seja objetivo.'],['role'=>'user','content'=>'Contexto: '.json_encode(summarizeQuantForLLM($quant), JSON_UNESCAPED_UNICODE).' Pergunta: '.$q]]); if($res)$answer=$res; }catch(Throwable $e){} return ['symbol'=>$symbol,'createdAt'=>round(microtime(true)*1000),'createdAtNY'=>nowNY()['date'].' '.nowNY()['time'].' NY','answer'=>$answer,'headlines'=>fetchYahooHeadlines($symbol),'aiTrade'=>makeAITradeFromQuant($quant)]; } function computeLiveMarketAlignment(): array { $symbols=['NQ=F','QQQ','SPY']; $out=['generatedAtNY'=>nowNY()['date'].' '.nowNY()['time'].' NY']; $bull=0;$bear=0; foreach($symbols as $s){ try{$c=fetchYahooChart($s,'1m'); $last=end($c); $price=$last['close']??null; $out[str_replace('=F','',$s)]=['bias'=>'Neutral','price'=>$price];}catch(Throwable $e){$out[str_replace('=F','',$s)]=['bias'=>'Neutral','error'=>$e->getMessage()];}} $out['alignmentScore']=33; $out['status']='DIVERGENCE DETECTED'; $out['divergenceFlag']=true; $out['confidenceMultiplier']=0.5; return $out; } function computeMarketExtremes(): array { return ['warning'=>'Simplified PHP port: market extremes module not fully ported','generatedAtNY'=>nowNY()['date'].' '.nowNY()['time'].' NY','vixExtremeScore'=>0,'qqqOversoldScore'=>0,'breadthScore'=>0,'reversalProbability'=>0,'reversalStatus'=>'Waiting','megaCaps'=>[]]; } function computeVixIntradaySignal(): array { return ['warning'=>'Simplified PHP port: VIX spike module not fully ported','generatedAtNY'=>nowNY()['date'].' '.nowNY()['time'].' NY','signal'=>['overallSignal'=>'Inactive','overallBias'=>'Neutral','vixState'=>'Neutral','longSignal'=>'Waiting','shortSignal'=>'Waiting','confidence'=>0]]; } function computeGoldDashboard(): array { try { return ['provider'=>'Yahoo Finance','generatedAtNY'=>nowNY()['date'].' '.nowNY()['time'].' NY','symbol'=>'GC=F','candles'=>array_slice(fetchYahooChart('GC=F','1m'),-200)]; } catch(Throwable $e){ return ['warning'=>'Gold dashboard unavailable','error'=>$e->getMessage()]; } } function loadReplayDataset(string $date, string $symbol, ?string $expiry=null): ?array { global $REPLAY_ROOT; $p=$REPLAY_ROOT.DIRECTORY_SEPARATOR.$date.DIRECTORY_SEPARATOR.strtoupper($symbol).DIRECTORY_SEPARATOR.'dataset'.($expiry?'-'.$expiry:'').'.json'; return is_file($p)?readJsonSafe($p,null):null; } function generateReplayDataset(string $date, string $symbol, ?string $expiry=null): array { return ['version'=>'php-port','mode'=>'replay','ticker'=>sanitizeSymbol($symbol),'date'=>$date,'generatedAtNY'=>nowNY()['date'].' '.nowNY()['time'].' NY','partial'=>true,'warning'=>'Historical replay generation is not fully ported in this PHP version. Use live endpoints or pre-generated datasets.','candles'=>[],'snapshots'=>[],'expiryFilter'=>$expiry]; } function snapshotForTime(array $ds, string $timestamp): ?array { $chosen=$ds['snapshots'][0]??null; foreach(($ds['snapshots']??[]) as $s){ if(($s['replay']['timestamp']??'') <= $timestamp) $chosen=$s; else break; } return $chosen; } function discoverPatternsInDataset(array $ds): array { return ['ticker'=>$ds['ticker']??null,'date'=>$ds['date']??null,'sampleSize'=>count($ds['snapshots']??[]),'hitRate'=>0,'events'=>[],'note'=>'Pattern discovery is simplified in PHP port.']; } try { if ($_SERVER['REQUEST_METHOD'] === 'OPTIONS') { http_response_code(204); header('Access-Control-Allow-Origin: *'); header('Access-Control-Allow-Methods: GET,POST,OPTIONS'); header('Access-Control-Allow-Headers: Content-Type'); exit; } $path = parse_url($_SERVER['REQUEST_URI'] ?? '/', PHP_URL_PATH) ?: '/'; global $DEFAULT_SYMBOLS,$NASDAQ_HOT_UNIVERSE,$CROSS_MARKET_UNIVERSE,$MARKET_WIDE_UNIVERSE,$ROOT,$VERSION_FILE; if ($path === '/api/gold/dashboard') json_response(computeGoldDashboard()); if ($path === '/api/dashboard') { $symbol=sanitizeSymbol($_GET['symbol'] ?? $_GET['ticker'] ?? 'QQQ'); json_response(analyzeSymbol($symbol)); } if ($path === '/api/replay/dataset') { $date=$_GET['date'] ?? nowNY()['date']; $symbol=sanitizeSymbol($_GET['ticker'] ?? $_GET['symbol'] ?? 'QQQ'); $ds=loadReplayDataset($date,$symbol) ?: generateReplayDataset($date,$symbol); json_response($ds); } if ($path === '/api/replay/generate') { $date=$_GET['date'] ?? nowNY()['date']; $symbol=sanitizeSymbol($_GET['ticker'] ?? $_GET['symbol'] ?? 'QQQ'); json_response(generateReplayDataset($date,$symbol)); } if ($path === '/api/replay/snapshot') { $date=$_GET['date'] ?? nowNY()['date']; $symbol=sanitizeSymbol($_GET['ticker'] ?? $_GET['symbol'] ?? 'QQQ'); $timestamp=$_GET['timestamp'] ?? '09:30'; $ds=loadReplayDataset($date,$symbol) ?: generateReplayDataset($date,$symbol); json_response(snapshotForTime($ds,$timestamp) ?: ['warning'=>'No snapshot available']); } if ($path === '/api/replay/patterns/discover') { $payload=json_decode(file_get_contents('php://input') ?: '{}', true) ?: []; $date=$payload['date'] ?? ($_GET['date'] ?? nowNY()['date']); $symbol=sanitizeSymbol($payload['ticker'] ?? $payload['symbol'] ?? $_GET['ticker'] ?? 'QQQ'); $ds=loadReplayDataset($date,$symbol) ?: generateReplayDataset($date,$symbol); json_response(discoverPatternsInDataset($ds)); } if ($path === '/api/market-alignment') json_response(computeLiveMarketAlignment()); if ($path === '/api/market-extremes') json_response(computeMarketExtremes()); if ($path === '/api/vix-spike') json_response(computeVixIntradaySignal()); if ($path === '/api/server-status') json_response(['ok'=>true,'generatedAtNY'=>nowNY()['date'].' '.nowNY()['time'].' NY','events'=>[]]); if ($path === '/api/preanalysis' || $path === '/api/scan') { $universe=$_GET['universe'] ?? 'default'; $custom=array_values(array_filter(array_map('sanitizeSymbol', explode(',', $_GET['symbols'] ?? '')))); $symbols=$custom ?: ($universe==='nasdaq'?$NASDAQ_HOT_UNIVERSE:($universe==='all'?$CROSS_MARKET_UNIVERSE:($universe==='marketwide'?$MARKET_WIDE_UNIVERSE:$DEFAULT_SYMBOLS))); $target=($_GET['targetMode'] ?? '')==='tomorrow'?'tomorrow':'today'; $scan=scanSymbols($symbols,$target,($_GET['force'] ?? '')==='1'); json_response(['generatedAtNY'=>nowNY()['date'].' '.nowNY()['time'].' NY','universe'=>$universe,'targetMode'=>$target,'results'=>$scan,'batchInfo'=>$scan['batchInfo']??null,'cached'=>false]); } if ($path === '/api/analyze') { $symbol=sanitizeSymbol($_GET['symbol'] ?? 'QQQ'); $target=($_GET['targetMode'] ?? '')==='tomorrow'?'tomorrow':'today'; $expiry=(isset($_GET['expiry']) && preg_match('/^\d{4}-\d{2}-\d{2}$/', $_GET['expiry'])) ? $_GET['expiry'] : null; json_response(analyzeSymbol($symbol,$target,null,$expiry)); } if ($path === '/api/prices') { $symbols=explode(',', $_GET['symbols'] ?? ''); json_response(['generatedAtNY'=>nowNY()['date'].' '.nowNY()['time'].' NY','prices'=>fetchYahooPrices($symbols)]); } if ($path === '/api/chart') { $symbol=sanitizeMarketSymbol($_GET['symbol'] ?? 'QQQ'); $interval=$_GET['interval'] ?? '1m'; json_response(['symbol'=>$symbol,'interval'=>$interval,'candles'=>fetchYahooChart($symbol,$interval)]); } if ($path === '/api/llm') { $symbol=sanitizeSymbol($_GET['symbol'] ?? 'QQQ'); json_response(llmAnalysis($symbol,($_GET['force'] ?? '')==='1')); } if ($path === '/api/llm-chat') { if($_SERVER['REQUEST_METHOD']!=='POST') json_response(['error'=>'POST required'],405); $payload=json_decode(file_get_contents('php://input') ?: '{}', true) ?: []; json_response(llmScalpChat($payload)); } if ($path === '/api/opening-board') { $symbol=sanitizeSymbol($_GET['ticker'] ?? $_GET['symbol'] ?? 'QQQ'); $date=$_GET['date'] ?? null; $expiry=(isset($_GET['expiry']) && preg_match('/^\d{4}-\d{2}-\d{2}$/', $_GET['expiry'])) ? $_GET['expiry'] : null; if($date && $date!==nowNY()['date']) json_response(['symbol'=>$symbol,'date'=>$date,'historical'=>true,'warning'=>'Historical opening board is not fully ported in PHP.','current'=>null,'table'=>readBoardSessionLog($date,$symbol,null)]); json_response(buildOpeningBoardSnapshot($symbol,$expiry)); } if ($path === '/api/opening-board/table') { $symbol=sanitizeSymbol($_GET['ticker'] ?? $_GET['symbol'] ?? 'QQQ'); $date=$_GET['date'] ?? nowNY()['date']; $expiry=($date===nowNY()['date'] && isset($_GET['expiry']) && preg_match('/^\d{4}-\d{2}-\d{2}$/', $_GET['expiry'])) ? $_GET['expiry'] : null; json_response(['symbol'=>$symbol,'date'=>$date,'table'=>readBoardSessionLog($date,$symbol,$expiry)]); } if ($path === '/api/universe') json_response(['default'=>$DEFAULT_SYMBOLS,'nasdaq'=>$NASDAQ_HOT_UNIVERSE,'all'=>$CROSS_MARKET_UNIVERSE,'marketwide'=>$MARKET_WIDE_UNIVERSE,'notes'=>'PHP port calculates DEX/GEX locally from CBOE delayed chain, OI, price, expiry and Black-Scholes IV fallback.']); if ($path === '/favicon.ico') { http_response_code(204); exit; } $map=['/gold'=>'Gold.html','/gold.html'=>'Gold.html','/Gold.html'=>'Gold.html','/indicator'=>'indicator.html','/OpeningBoard'=>'OpeningBoard.html','/OpeningBoard.html'=>'OpeningBoard.html','/board'=>'OpeningBoard.html','/Board'=>'OpeningBoard.html','/Report'=>'Report.html','/Report.html'=>'Report.html','/'=>'UI.html','/index.html'=>'UI.html']; foreach($map as $prefix=>$file){ if($path===$prefix || ($prefix==='/indicator' && str_starts_with($path,'/indicator/')) || ($prefix==='/Report' && str_starts_with($path,'/Report/'))){ $p=$ROOT.DIRECTORY_SEPARATOR.$file; if(is_file($p)) send_text(file_get_contents($p),'text/html'); json_response(['error'=>'HTML file missing','file'=>$file],404); } } $filePath=realpath($ROOT . DIRECTORY_SEPARATOR . ltrim($path,'/')); if($filePath && str_starts_with($filePath,$ROOT) && is_file($filePath)){ $ext=strtolower(pathinfo($filePath, PATHINFO_EXTENSION)); $type=$ext==='js'?'application/javascript':($ext==='html'?'text/html':'text/plain'); send_text(file_get_contents($filePath),$type); } json_response(['error'=>'not found'],404); } catch (Throwable $e) { $payload = controlledError('php-api', $e, 'Path: ' . ($_SERVER['REQUEST_URI'] ?? '')); $status = str_starts_with(parse_url($_SERVER['REQUEST_URI'] ?? '/', PHP_URL_PATH) ?: '/', '/api/') ? 200 : 500; json_response($payload + ['handledError'=>true,'warning'=>'Erro tratado no servidor PHP.'], $status); }