⚠️ stocks.json not found'); } $json = file_get_contents($jsonFile); $data = json_decode($json, true); if (!is_array($data)) { die('
⚠️ Invalid JSON format
'); } // Filter stocks with pricetarget $targetStocks = array_filter($data, function($item) { return isset($item['pricetarget']) && is_numeric($item['pricetarget']) && $item['pricetarget'] > 0; }); // ---- FETCH LIVE QUOTE DATA FROM YOUR ROBINHOOD API ---- // Returns an array of everything useful for display + estimates, or null on failure. function fetchQuoteData($symbol) { // cURL needs an ABSOLUTE URL - a relative path can't be resolved by cURL. $scheme = (!empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off') ? 'https' : 'http'; $host = $_SERVER['HTTP_HOST'] ?? 'localhost'; $base = rtrim(dirname($_SERVER['SCRIPT_NAME'] ?? ''), '/'); $url = "$scheme://$host$base/robinhood_api.php?symbol=" . urlencode($symbol); $ch = curl_init(); curl_setopt($ch, CURLOPT_URL, $url); curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); curl_setopt($ch, CURLOPT_TIMEOUT, 8); curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, 5); curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true); curl_setopt($ch, CURLOPT_USERAGENT, 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36'); curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false); $response = curl_exec($ch); $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE); $curlErr = curl_error($ch); curl_close($ch); if ($curlErr) { error_log("[stocks_widget] fetchQuoteData($symbol) cURL error: $curlErr | URL: $url"); return null; } if ($httpCode !== 200 || !$response) { error_log("[stocks_widget] fetchQuoteData($symbol) HTTP $httpCode | URL: $url | body: " . substr((string)$response, 0, 300)); return null; } $data = json_decode($response, true); if (json_last_error() !== JSON_ERROR_NONE) { error_log("[stocks_widget] fetchQuoteData($symbol) JSON decode error: " . json_last_error_msg()); return null; } if (!$data || !isset($data['success']) || $data['success'] !== true) { error_log("[stocks_widget] fetchQuoteData($symbol) API returned success=false or malformed payload: " . substr($response, 0, 300)); return null; } $quote = $data['quote'] ?? []; $fundamentals = $data['fundamentals'] ?? []; $marketHours = $data['market_hours'] ?? []; $price = null; if (isset($quote['extended_hours_price']) && $quote['extended_hours_price'] > 0) { $price = $quote['extended_hours_price']; } elseif (isset($quote['last_price']) && $quote['last_price'] > 0) { $price = $quote['last_price']; } if ($price === null) { error_log("[stocks_widget] fetchQuoteData($symbol) no usable price field in quote: " . substr($response, 0, 300)); return null; } return [ 'price' => $price, 'previous_close' => $quote['previous_close'] ?? null, 'is_open' => $marketHours['is_open'] ?? null, 'high_52' => $fundamentals['high_52_weeks'] ?? null, 'low_52' => $fundamentals['low_52_weeks'] ?? null, ]; } // ---- FETCH ALL QUOTE DATA ---- $liveData = []; $apiWorking = false; foreach ($targetStocks as $stock) { $symbol = $stock['stock']; $quote = fetchQuoteData($symbol); if ($quote !== null) { $liveData[$symbol] = $quote; $apiWorking = true; } } // ---- PROCESS STOCKS ---- $processedStocks = []; foreach ($targetStocks as $stock) { $symbol = $stock['stock']; // Buy price from JSON (what you paid) $buyPrice = (float)$stock['price']; $priceTarget = (float)$stock['pricetarget']; // Get live quote from API $currentPrice = $buyPrice; // Default fallback $priceSource = 'cached'; $dayChangePct = null; $isMarketOpen = null; $high52 = null; $low52 = null; if (isset($liveData[$symbol])) { $q = $liveData[$symbol]; $currentPrice = $q['price']; $priceSource = 'live'; $isMarketOpen = $q['is_open']; $high52 = $q['high_52']; $low52 = $q['low_52']; if (!empty($q['previous_close']) && $q['previous_close'] > 0) { $dayChangePct = (($currentPrice - $q['previous_close']) / $q['previous_close']) * 100; } } // Calculate metrics $yieldPct = $buyPrice > 0 ? (($currentPrice - $buyPrice) / $buyPrice) * 100 : 0; $distanceToTarget = $priceTarget > 0 ? ($currentPrice / $priceTarget) * 100 : 0; $distanceToTarget = max(0, min(100, $distanceToTarget)); // Is the target realistic based on the stock's own 52-week range? // A target above the 52-week high hasn't been reached in a year - // worth flagging so the ETA doesn't look more confident than it should. $targetAboveHigh52 = ($high52 !== null && $priceTarget > $high52); // Days held $daysHeld = '—'; $daysHeldNum = 0; if (isset($stock['date'])) { try { $bought = new DateTime($stock['date']); $now = new DateTime(); $diff = $bought->diff($now); $daysHeldNum = $diff->days; $daysHeld = $daysHeldNum . 'd'; } catch (Exception $e) { $daysHeld = '—'; } } // ---- ETA TO TARGET ---- // Estimate based on the average daily price change since purchase, // projected forward at that same rate until the target is reached. // This is a rough linear estimate, not a forecast - stocks don't move linearly. $etaLabel = '—'; $etaClass = 'neutral'; if ($distanceToTarget >= 100) { $etaLabel = 'Target reached'; $etaClass = 'reached'; } elseif ($daysHeldNum > 0) { $dailyRate = ($currentPrice - $buyPrice) / $daysHeldNum; if ($dailyRate > 0) { $remaining = $priceTarget - $currentPrice; $daysNeeded = $remaining / $dailyRate; if ($daysNeeded <= 0) { $etaLabel = 'Target reached'; $etaClass = 'reached'; } elseif ($daysNeeded > 3650) { // Rate is positive but so slow it's not a meaningful estimate $etaLabel = 'Trend too slow to estimate'; $etaClass = 'stale'; } else { $etaDate = new DateTime(); $etaDate->modify('+' . (int)ceil($daysNeeded) . ' days'); $daysNeededRounded = (int)ceil($daysNeeded); if ($daysNeededRounded <= 60) { $etaLabel = '~' . $daysNeededRounded . 'd (' . $etaDate->format('d M') . ')'; } else { $months = round($daysNeededRounded / 30, 1); $etaLabel = '~' . $months . 'mo (' . $etaDate->format('d M Y') . ')'; } $etaClass = 'positive'; } } else { // Flat or declining since purchase - no positive trend to project forward $etaLabel = 'No upward trend'; $etaClass = 'negative'; } } else { // Bought today - not enough history for a rate $etaLabel = 'Too early to estimate'; $etaClass = 'stale'; } // Flag when the target sits above the 52-week high - the ETA above // assumes the recent trend continues, but the stock hasn't actually // traded at the target price in the last year, so treat the estimate // with extra skepticism in that case. if ($targetAboveHigh52 && $distanceToTarget < 100 && $etaClass === 'positive') { $etaLabel .= ' ⚠️ above 52w high'; } $processedStocks[] = [ 'symbol' => $symbol, 'depot' => $stock['depot'] ?? '—', 'bought_date' => $stock['date'] ?? 'N/A', 'days_held' => $daysHeld, 'buy_price' => number_format($buyPrice, 2), 'current_price' => number_format($currentPrice, 2), 'price_target' => number_format($priceTarget, 2), 'yield_pct' => number_format($yieldPct, 1), 'yield_class' => $yieldPct >= 0 ? 'green' : 'red', 'yield_sign' => $yieldPct >= 0 ? '+' : '', 'distance_pct' => number_format($distanceToTarget, 0), 'progress_class' => $distanceToTarget >= 100 ? 'over' : ($distanceToTarget >= 75 ? 'near' : ''), 'currency' => $stock['currency'] ?? '$', 'price_source' => $priceSource, 'eta_label' => $etaLabel, 'eta_class' => $etaClass, 'day_change_pct' => $dayChangePct !== null ? number_format($dayChangePct, 1) : null, 'day_change_class' => ($dayChangePct !== null && $dayChangePct >= 0) ? 'green' : 'red', 'day_change_sign' => ($dayChangePct !== null && $dayChangePct >= 0) ? '+' : '', 'is_market_open' => $isMarketOpen, 'high_52' => $high52 !== null ? number_format($high52, 2) : null, 'low_52' => $low52 !== null ? number_format($low52, 2) : null ]; } // Sort by closest to target usort($processedStocks, function($a, $b) { $aReached = (float)$a['distance_pct'] >= 100; $bReached = (float)$b['distance_pct'] >= 100; if ($aReached && !$bReached) return -1; if (!$aReached && $bReached) return 1; return (float)$b['distance_pct'] - (float)$a['distance_pct']; }); $count = count($processedStocks); $lastUpdate = date('H:i:s'); ?> Stock Targets Widget

🎯 Price Targets

updated
No stocks with price targets found
= 100; $isLive = $stock['price_source'] === 'live'; ?>
✅ TARGET
Bought Held Buy Current Yield % Today %
% /
ETA to target: