$url, CURLOPT_RETURNTRANSFER => true, CURLOPT_FOLLOWLOCATION => true, CURLOPT_TIMEOUT => 15, CURLOPT_SSL_VERIFYPEER => false, CURLOPT_SSL_VERIFYHOST => false, CURLOPT_USERAGENT => $userAgent, ]); $raw = curl_exec($ch); $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE); curl_close($ch); if ($httpCode !== 200 || !$raw) { return null; } $data = json_decode($raw, true); $result = $data['chart']['result'][0] ?? null; if (!$result) { return null; } $meta = $result['meta'] ?? []; $price = $meta['regularMarketPrice'] ?? null; if ($price === null) { $price = $result['indicators']['quote'][0]['close'][0] ?? null; } if ($price === null) { return null; } $prevClose = $meta['chartPreviousClose'] ?? $meta['previousClose'] ?? null; $history = []; if ($withHistory) { $timestamps = $result['timestamp'] ?? []; $quote = $result['indicators']['quote'][0] ?? []; $highs = $quote['high'] ?? []; $lows = $quote['low'] ?? []; $closes = $quote['close'] ?? []; foreach ($timestamps as $i => $ts) { if (!isset($closes[$i], $highs[$i], $lows[$i]) || $closes[$i] === null) { continue; } $history[] = [ 'date' => date('Y-m-d', $ts), 'high' => (float) $highs[$i], 'low' => (float) $lows[$i], 'close' => (float) $closes[$i], ]; } $history = array_slice($history, -$historyDays); } return [ 'price' => (float) $price, 'prevClose' => $prevClose !== null ? (float) $prevClose : null, 'currency' => strtoupper($meta['currency'] ?? 'USD'), 'history' => $history, ]; } function getFxRateToUsd(string $currency, string $userAgent, array &$fxCache): ?float { $currency = strtoupper($currency); if ($currency === 'USD') { return 1.0; } if (isset($fxCache[$currency])) { return $fxCache[$currency]; } $quote = fetchYahooQuote($currency . 'USD=X', $userAgent); $rate = $quote['price'] ?? null; $fxCache[$currency] = $rate; return $rate; } function computeRecentStats(array $history): ?array { if (count($history) < 3) { return null; } $lows = array_column($history, 'low'); $highs = array_column($history, 'high'); $closes = array_column($history, 'close'); $rangePcts = []; $dailyChanges = []; foreach ($history as $i => $bar) { if ($bar['close'] > 0) { $rangePcts[] = (($bar['high'] - $bar['low']) / $bar['close']) * 100; } if ($i > 0 && $history[$i-1]['close'] > 0) { $dailyChanges[] = (($bar['close'] - $history[$i-1]['close']) / $history[$i-1]['close']) * 100; } } return [ 'days' => count($history), 'low' => min($lows), 'high' => max($highs), 'avgClose' => array_sum($closes) / count($closes), 'avgRangePct' => count($rangePcts) ? array_sum($rangePcts) / count($rangePcts) : 0.0, 'dailyChanges' => $dailyChanges, 'lastClose' => end($closes), ]; } /** * Classify a stock based on price, volatility, and performance */ function classifyStock(float $price, float $avgDailyMove, float $stdDev, float $gainPct, float $avgRangePct): array { $classification = []; $emoji = ''; $quip = ''; $color = ''; // Determine price category if ($price < 5) { $priceCategory = 'penny'; $priceLabel = 'Penny Stock'; $priceEmoji = '🪙'; } elseif ($price < 20) { $priceCategory = 'low'; $priceLabel = 'Low-Priced'; $priceEmoji = '💵'; } elseif ($price < 50) { $priceCategory = 'mid'; $priceLabel = 'Mid-Cap'; $priceEmoji = '📊'; } else { $priceCategory = 'high'; $priceLabel = 'Institutional'; $priceEmoji = '🏛️'; } // Determine volatility category if ($avgDailyMove > 10 || $stdDev > 8) { $volCategory = 'high'; $volLabel = 'Very Volatile'; $volEmoji = '⚡'; } elseif ($avgDailyMove > 5 || $stdDev > 4) { $volCategory = 'medium'; $volLabel = 'Moderate Volatility'; $volEmoji = '📈'; } else { $volCategory = 'low'; $volLabel = 'Low Volatility'; $volEmoji = '🐢'; } // Determine performance category if ($gainPct > 50) { $perfCategory = 'star'; $perfLabel = 'Star Performer'; $perfEmoji = '⭐'; } elseif ($gainPct > 20) { $perfCategory = 'gainer'; $perfLabel = 'Solid Gainer'; $perfEmoji = '📈'; } elseif ($gainPct > 5) { $perfCategory = 'steady'; $perfLabel = 'Steady Growth'; $perfEmoji = '🌱'; } elseif ($gainPct > -5) { $perfCategory = 'flat'; $perfLabel = 'Flat'; $perfEmoji = '➡️'; } elseif ($gainPct > -20) { $perfCategory = 'dip'; $perfLabel = 'In a Dip'; $perfEmoji = '📉'; } else { $perfCategory = 'crash'; $perfLabel = 'Heavy Loss'; $perfEmoji = '🔥'; } // Combine for final classification if ($priceCategory == 'penny' && $volCategory == 'high') { $classification['type'] = 'penny_volatile'; $classification['label'] = 'High-Risk Penny Stock'; $emoji = '🎰'; $quip = 'High risk, high reward — trade with caution'; $color = '#f44336'; } elseif ($priceCategory == 'penny') { $classification['type'] = 'penny'; $classification['label'] = 'Penny Stock'; $emoji = '🪙'; $quip = 'Cheap but unpredictable — manage risk tightly'; $color = '#ff9800'; } elseif ($priceCategory == 'high' && $volCategory == 'low') { $classification['type'] = 'keeper'; $classification['label'] = 'Institutional Keeper'; $emoji = '🏛️'; $quip = 'Solid, stable, hold for the long term'; $color = '#4caf50'; } elseif ($priceCategory == 'high' && $volCategory == 'medium') { $classification['type'] = 'growth'; $classification['label'] = 'Quality Growth'; $emoji = '🚀'; $quip = 'Growing with institutional backing — hold through volatility'; $color = '#8bc34a'; } elseif ($volCategory == 'high' && $perfCategory == 'star') { $classification['type'] = 'rocket'; $classification['label'] = 'Rocket Ship 🚀'; $emoji = '🚀'; $quip = 'Massive gains but extremely volatile — lock in profits'; $color = '#ff6f00'; } elseif ($volCategory == 'high' && $perfCategory == 'crash') { $classification['type'] = 'risky'; $classification['label'] = 'High-Risk Recovery'; $emoji = '🎲'; $quip = 'Selling off hard — only for high-risk tolerance'; $color = '#d32f2f'; } elseif ($volCategory == 'medium' && $perfCategory == 'gainer') { $classification['type'] = 'momentum'; $classification['label'] = 'Momentum Play'; $emoji = '⚡'; $quip = 'Good momentum with moderate risk — let it ride with a stop'; $color = '#ffa726'; } elseif ($volCategory == 'low' && $perfCategory == 'steady') { $classification['type'] = 'dividend'; $classification['label'] = 'Steady Eddi'; $emoji = '🐢'; $quip = 'Slow and steady — set it and forget it'; $color = '#66bb6a'; } elseif ($perfCategory == 'dip' && $volCategory == 'high') { $classification['type'] = 'bounce'; $classification['label'] = 'Bounce Candidate'; $emoji = '🏀'; $quip = 'High volatility dip — could bounce back strongly'; $color = '#ff9800'; } elseif ($perfCategory == 'dip') { $classification['type'] = 'value'; $classification['label'] = 'Value Play'; $emoji = '💎'; $quip = 'Temporarily down but fundamentally sound — consider averaging down'; $color = '#4caf50'; } else { $classification['type'] = 'mixed'; $classification['label'] = 'Mixed Signals'; $emoji = '🤔'; $quip = 'No clear pattern — wait for confirmation'; $color = '#9e9e9e'; } $classification['emoji'] = $emoji; $classification['quip'] = $quip; $classification['color'] = $color; $classification['priceLabel'] = $priceLabel; $classification['volLabel'] = $volLabel; $classification['perfLabel'] = $perfLabel; return $classification; } function generatePredictions(float $currentPrice, array $history, float $fxRate): array { $dataPoints = count($history); if ($dataPoints < 3) { return [ 'best' => $currentPrice * 1.05, 'worst' => $currentPrice * 0.95, 'median' => $currentPrice, 'bestPct' => 5.0, 'worstPct' => -5.0, 'medianPct' => 0.0, 'confidence' => 'low', 'confidenceScore' => 20, 'trend' => 'flat', 'reasoning' => 'Not enough historical data (need at least 3 days) for reliable predictions.', 'dataPoints' => $dataPoints, 'isInstitutional' => false ]; } $changes = []; for ($i = 1; $i < count($history); $i++) { if ($history[$i-1]['close'] > 0) { $changes[] = (($history[$i]['close'] - $history[$i-1]['close']) / $history[$i-1]['close']) * 100; } } if (count($changes) < 2) { return [ 'best' => $currentPrice * 1.05, 'worst' => $currentPrice * 0.95, 'median' => $currentPrice, 'bestPct' => 5.0, 'worstPct' => -5.0, 'medianPct' => 0.0, 'confidence' => 'low', 'confidenceScore' => 15, 'trend' => 'flat', 'reasoning' => 'Only ' . count($changes) . ' daily changes available.', 'dataPoints' => $dataPoints, 'isInstitutional' => false ]; } $sortedChanges = $changes; sort($sortedChanges); $count = count($sortedChanges); $overallAvg = array_sum($sortedChanges) / $count; $stdDev = 0; foreach ($sortedChanges as $change) { $stdDev += pow($change - $overallAvg, 2); } $stdDev = sqrt($stdDev / $count); $isInstitutional = ($currentPrice > 20 && $stdDev < 8); $recentDays = $isInstitutional ? 10 : 5; $recentDays = min($recentDays, $count); $recentAvg = 0; for ($i = $count - $recentDays; $i < $count; $i++) { $recentAvg += $sortedChanges[$i]; } $recentAvg = $recentAvg / $recentDays; $trendWeight = $isInstitutional ? 0.6 : 0.7; $trendFactor = ($recentAvg * $trendWeight) + ($overallAvg * (1 - $trendWeight)); $confidenceScore = 50; if ($stdDev < 2) { $confidenceScore += 25; } elseif ($stdDev < 4) { $confidenceScore += 15; } elseif ($stdDev < 8) { $confidenceScore += 5; } else { $confidenceScore -= 10; } $trendStrength = abs($trendFactor); if ($trendStrength > 5) { $confidenceScore += 15; } elseif ($trendStrength > 2) { $confidenceScore += 8; } else { $confidenceScore -= 5; } $trendDiff = abs($recentAvg - $overallAvg); if ($trendDiff < 1) { $confidenceScore += 10; } elseif ($trendDiff < 3) { $confidenceScore += 5; } else { $confidenceScore -= 10; } if ($count >= 40) { $confidenceScore += 20; } elseif ($count >= 30) { $confidenceScore += 15; } elseif ($count >= 20) { $confidenceScore += 10; } elseif ($count >= 10) { $confidenceScore += 5; } else { $confidenceScore -= 5; } $range = max($sortedChanges) - min($sortedChanges); if ($range > 20) { $confidenceScore -= 15; } elseif ($range > 15) { $confidenceScore -= 8; } elseif ($range > 10) { $confidenceScore -= 3; } else { $confidenceScore += 5; } $avgMove = array_sum(array_map('abs', $sortedChanges)) / $count; if ($avgMove > 10) { $confidenceScore -= 10; } elseif ($avgMove > 6) { $confidenceScore -= 5; } elseif ($avgMove < 3) { $confidenceScore += 5; } if ($isInstitutional && $stdDev < 6) { $confidenceScore += 10; } $reversals = 0; for ($i = 1; $i < count($sortedChanges); $i++) { if (($sortedChanges[$i] > 0 && $sortedChanges[$i-1] < 0) || ($sortedChanges[$i] < 0 && $sortedChanges[$i-1] > 0)) { $reversals++; } } $reversalRate = $reversals / (count($sortedChanges) - 1); if ($reversalRate > 0.5) { $confidenceScore -= 10; } elseif ($reversalRate > 0.3) { $confidenceScore -= 5; } else { $confidenceScore += 5; } $confidenceScore = max(0, min(100, $confidenceScore)); if ($confidenceScore >= 70) { $confidence = 'high'; } elseif ($confidenceScore >= 45) { $confidence = 'medium'; } else { $confidence = 'low'; } $maxDailyMovePct = $isInstitutional ? 12.0 : 20.0; $bestPct = min($maxDailyMovePct, max(0, $sortedChanges[floor($count * 0.85)] + ($trendFactor * 0.15))); if ($trendFactor > 1) { $bestPct = max($bestPct, 1.5); } $worstPct = max(-$maxDailyMovePct, min(0, $sortedChanges[floor($count * 0.15)] + ($trendFactor * 0.15))); if ($trendFactor < -1) { $worstPct = min($worstPct, -1.5); } $medianPct = max(-$maxDailyMovePct * 0.5, min($maxDailyMovePct * 0.5, $trendFactor)); $bestPct = max($bestPct, $medianPct + 0.5); $worstPct = min($worstPct, $medianPct - 0.5); $trend = 'flat'; if ($trendFactor > 0.5) $trend = 'up'; elseif ($trendFactor < -0.5) $trend = 'down'; $reasoningParts = []; if ($confidenceScore >= 70) { $reasoningParts[] = "High confidence (" . round($confidenceScore) . "%)"; } elseif ($confidenceScore >= 45) { $reasoningParts[] = "Moderate confidence (" . round($confidenceScore) . "%)"; } else { $reasoningParts[] = "Low confidence (" . round($confidenceScore) . "%)"; } if ($count >= 40) { $reasoningParts[] = round($count) . " days of data (excellent)"; } elseif ($count >= 30) { $reasoningParts[] = round($count) . " days of data (very good)"; } elseif ($count >= 20) { $reasoningParts[] = round($count) . " days of data (good)"; } elseif ($count >= 10) { $reasoningParts[] = round($count) . " days of data (decent)"; } else { $reasoningParts[] = "only " . round($count) . " data points"; } if ($isInstitutional) { $reasoningParts[] = "institutional stock with stable pattern"; } if ($stdDev < 2) { $reasoningParts[] = "very consistent moves (std dev " . number_format($stdDev, 1) . "%)"; } elseif ($stdDev < 4) { $reasoningParts[] = "fairly consistent (std dev " . number_format($stdDev, 1) . "%)"; } elseif ($stdDev < 8) { $reasoningParts[] = "moderate volatility (std dev " . number_format($stdDev, 1) . "%)"; } else { $reasoningParts[] = "highly volatile (std dev " . number_format($stdDev, 1) . "%)"; } if ($trendStrength > 5) { $reasoningParts[] = "strong " . $trend . "ward trend (" . number_format($trendFactor, 1) . "% avg)"; } elseif ($trendStrength > 2) { $reasoningParts[] = "moderate " . $trend . "ward trend"; } else { $reasoningParts[] = "no clear trend direction"; } $lastChange = end($changes); if (abs($lastChange) > 5) { $reasoningParts[] = "recent momentum: " . ($lastChange > 0 ? '+' : '') . number_format($lastChange, 1) . "%"; } if ($reversalRate > 0.5) { $reasoningParts[] = "many reversals (" . round($reversalRate * 100) . "% of days change direction)"; } elseif ($reversalRate < 0.2) { $reasoningParts[] = "consistent direction (few reversals)"; } $reasoning = implode(" • ", $reasoningParts) . "."; return [ 'best' => $currentPrice * (1 + $bestPct / 100), 'worst' => $currentPrice * (1 + $worstPct / 100), 'median' => $currentPrice * (1 + $medianPct / 100), 'bestPct' => $bestPct, 'worstPct' => $worstPct, 'medianPct' => $medianPct, 'confidence' => $confidence, 'confidenceScore' => $confidenceScore, 'trend' => $trend, 'trendStrength' => $trendStrength, 'stdDev' => $stdDev, 'reasoning' => $reasoning, 'avgDailyMove' => $avgMove ?? 0, 'dataPoints' => $count, 'reversalRate' => $reversalRate, 'isInstitutional' => $isInstitutional ]; } function generateActionableAdvice(array $row, array $predictions, array $classification): string { $gainPct = $row['gainPct'] ?? 0; $gainUsd = $row['gainUsd'] ?? 0; $currentPrice = $row['currentPriceUsd'] ?? 0; $entry = $row['entry'] ?? 0; $ticker = $row['ticker'] ?? 'Unknown'; $volatility = $predictions['stdDev'] ?? 0; $trend = $predictions['trend'] ?? 'flat'; $confScore = $predictions['confidenceScore'] ?? 0; $dataPoints = $predictions['dataPoints'] ?? 0; $avgMove = $predictions['avgDailyMove'] ?? 0; $advice = []; // Start with classification emoji and type $advice[] = $classification['emoji'] . " " . $classification['label'] . ": " . $classification['quip']; // Current situation if ($gainPct > 0) { $advice[] = "📈 " . number_format($gainPct, 0) . "% gain (+\$" . number_format($gainUsd, 2) . "/share) from \$" . number_format($entry, 2); } else { $advice[] = "📉 " . number_format(abs($gainPct), 0) . "% loss (-\$" . number_format(abs($gainUsd), 2) . "/share) from \$" . number_format($entry, 2); } // Volatility assessment if ($avgMove > 10) { $advice[] = "⚡ Very volatile — expect large swings"; } elseif ($avgMove > 5) { $advice[] = "📊 Moderately volatile"; } else { $advice[] = "🐢 Low volatility — stable"; } // Trend assessment if ($trend == 'up' && $gainPct > 0) { $advice[] = "📈 Strong momentum — trend is your friend"; } elseif ($trend == 'up' && $gainPct < 0) { $advice[] = "🔄 Recovering — upward trend forming"; } elseif ($trend == 'down' && $gainPct > 0) { $advice[] = "⚠️ Potential reversal — uptrend losing steam"; } elseif ($trend == 'down' && $gainPct < 0) { $advice[] = "📉 Downward trend — protect capital"; } else { $advice[] = "➡️ No clear trend — range-bound"; } // Prediction $bestPct = $predictions['bestPct'] ?? 0; $worstPct = $predictions['worstPct'] ?? 0; $advice[] = "🔮 Tomorrow: -" . number_format(abs($worstPct), 0) . "% to +" . number_format($bestPct, 0) . "% (median: " . number_format($predictions['medianPct'] ?? 0, 1) . "%)"; // Specific action based on classification $type = $classification['type'] ?? 'mixed'; switch ($type) { case 'penny_volatile': case 'rocket': $advice[] = "🎯 ACTION: Take partial profits now, set tight SAFE stop at \$" . number_format($row['safeStopUsd'] ?? 0, 2); break; case 'keeper': case 'dividend': $advice[] = "🎯 ACTION: Hold long-term — use wide SWING stop at \$" . number_format($row['swingStopUsd'] ?? 0, 2) . " to avoid getting shaken out"; break; case 'growth': case 'momentum': $advice[] = "🎯 ACTION: Let it ride with SWING stop at \$" . number_format($row['swingStopUsd'] ?? 0, 2) . " — or SAFE at \$" . number_format($row['safeStopUsd'] ?? 0, 2) . " to protect gains"; break; case 'bounce': case 'value': $advice[] = "🎯 ACTION: Consider averaging down — use SWING stop at \$" . number_format($row['swingStopUsd'] ?? 0, 2) . " for protection"; break; case 'risky': $advice[] = "🎯 ACTION: High risk — cut losses or use tight SAFE stop at \$" . number_format($row['safeStopUsd'] ?? 0, 2); break; default: $advice[] = "🎯 ACTION: Use SAFE stop at \$" . number_format($row['safeStopUsd'] ?? 0, 2) . " or SWING at \$" . number_format($row['swingStopUsd'] ?? 0, 2); } // Confidence context if ($confScore >= 70) { $advice[] = "✅ " . round($confScore) . "% confidence — " . $dataPoints . " days of consistent data"; } elseif ($confScore >= 45) { $advice[] = "📈 " . round($confScore) . "% confidence — " . $dataPoints . " days shows patterns"; } else { $advice[] = "⚠️ " . round($confScore) . "% confidence — highly unpredictable"; } return implode(" | ", $advice); } function buildAdviceWithTwoStops(float $shares, float $entry, float $currentUsd, float $fxRate, ?float $dayChangePct, ?array $stats, float $minTrimProfitUsd): array { $gainPct = $entry > 0 ? (($currentUsd - $entry) / $entry) * 100 : 0.0; $gainUsd = $currentUsd - $entry; if ($stats === null) { $swingStop = $currentUsd * 0.85; $safeStop = $entry + ($gainUsd * 0.5); if ($gainPct <= 0) { $safeStop = $currentUsd * 0.90; } $note = 'Not enough recent trading history — using generic levels.'; return [ 'action' => "Hold all " . rtrim(rtrim(number_format($shares, 2), '0'), '.') . " shares", 'swingStopUsd' => $swingStop, 'safeStopUsd' => $safeStop, 'swingStopPct' => 15.0, 'safeStopPct' => (($currentUsd - $safeStop) / $currentUsd) * 100, 'sharesTrim' => 0, 'sharesHold' => $shares, 'note' => $note, ]; } $lowUsd = $stats['low'] * $fxRate; $avgRangePct = $stats['avgRangePct']; $days = $stats['days']; if ($avgRangePct < 2) { $volLabel = 'low'; $swingBufferPct = 8; } elseif ($avgRangePct <= 5) { $volLabel = 'moderate'; $swingBufferPct = 12; } else { $volLabel = 'high'; $swingBufferPct = 18; } $swingStopUsd = $currentUsd * (1 - $swingBufferPct / 100); $swingStopUsd = max($swingStopUsd, $lowUsd * 0.95); $swingStopPct = (($currentUsd - $swingStopUsd) / $currentUsd) * 100; if ($gainPct >= 100) { $profitProtectPct = 0.70; } elseif ($gainPct >= 50) { $profitProtectPct = 0.60; } elseif ($gainPct >= 20) { $profitProtectPct = 0.50; } elseif ($gainPct >= 10) { $profitProtectPct = 0.40; } elseif ($gainPct > 0) { $profitProtectPct = 0.30; } else { $safeStopUsd = $swingStopUsd; $safeStopPct = $swingStopPct; $profitProtectPct = 0; } if ($gainPct > 0) { $safeStopUsd = $entry + ($gainUsd * $profitProtectPct); $supportBasedStop = $lowUsd * 0.98; if ($supportBasedStop > $safeStopUsd && $supportBasedStop > $entry) { $safeStopUsd = $supportBasedStop; } $safeStopUsd = max($safeStopUsd, $entry); $safeStopUsd = min($safeStopUsd, $currentUsd * 0.995); $protectedGainPct = (($safeStopUsd - $entry) / $gainUsd) * 100; if ($gainUsd > 0) { $protectedGainPct = min(100, max(0, $protectedGainPct)); } else { $protectedGainPct = 0; } } else { $safeStopUsd = $swingStopUsd; $protectedGainPct = 0; } $safeStopPct = (($currentUsd - $safeStopUsd) / $currentUsd) * 100; if ($gainPct >= 100) { $protectFrac = 0.5; } elseif ($gainPct >= 50) { $protectFrac = 0.35; } elseif ($gainPct >= 20) { $protectFrac = 0.2; } else { $protectFrac = 0.0; } $sharesTrim = $protectFrac > 0 ? (float) round($shares * $protectFrac) : 0.0; $sharesHold = $shares - $sharesTrim; $fmt = fn($n) => rtrim(rtrim(number_format($n, 2), '0'), '.'); $tooSmallToTrim = false; if ($sharesTrim > 0) { $profitCheck = $sharesTrim * ($currentUsd - $entry); if ($profitCheck < $minTrimProfitUsd) { $tooSmallToTrim = true; $sharesTrim = 0.0; $sharesHold = $shares; } } $gainDesc = ""; if ($gainPct > 0 && $gainUsd > 0) { $gainDesc = " 🔒 " . number_format($protectedGainPct, 0) . "% of $" . number_format($gainUsd, 2) . " gain protected"; } if ($sharesTrim > 0) { $proceedsUsd = $sharesTrim * $currentUsd; $profitUsd = $sharesTrim * ($currentUsd - $entry); $action = "SELL {$fmt($sharesTrim)} of {$fmt($shares)} shares now " . "(~$" . number_format($proceedsUsd, 0) . " proceeds, ~$" . number_format($profitUsd, 0) . " profit); " . "hold remaining {$fmt($sharesHold)} with SAFE stop at $" . number_format($safeStopUsd, 2) . " (" . number_format($safeStopPct, 1) . "% below current, " . number_format($protectedGainPct, 0) . "% of gain protected)" . " or SWING stop at $" . number_format($swingStopUsd, 2) . " (" . number_format($swingStopPct, 1) . "% below)"; } else { $action = "Hold all {$fmt($shares)} shares. SAFE stop: $" . number_format($safeStopUsd, 2) . " (" . number_format($safeStopPct, 1) . "% below current)" . $gainDesc . " — SWING stop: $" . number_format($swingStopUsd, 2) . " (" . number_format($swingStopPct, 1) . "% below) to let it breathe through volatility"; } $note = "Last {$days}d: support $" . number_format($lowUsd, 2) . ", avg range " . number_format($avgRangePct, 1) . "%/day ({$volLabel} volatility)."; if ($gainPct > 0) { $note .= " SAFE stop locks in " . number_format($protectedGainPct, 0) . "% of your $" . number_format($gainUsd, 2) . " profit."; } $note .= " SWING stop is wider to avoid normal pullbacks."; if ($tooSmallToTrim) { $note = "Position too small to justify partial exit — trimming would realize < $" . number_format($minTrimProfitUsd, 0) . " in profit. " . $note; } if ($dayChangePct !== null && $dayChangePct <= -8) { $note = '⚠️ SHARP DROP TODAY (' . number_format($dayChangePct, 1) . '%). ' . $note; } if ($gainPct > 50) { $note .= " With " . number_format($gainPct, 0) . "% gain, SAFE stop is recommended to lock in profits."; } elseif ($gainPct > 20) { $note .= " With " . number_format($gainPct, 0) . "% gain, consider SAFE stop to protect your profits."; } elseif ($gainPct < 0) { $note .= " Currently down " . number_format(abs($gainPct), 0) . "% — SWING stop may be safer to avoid whipsaw."; } return [ 'action' => $action, 'swingStopUsd' => $swingStopUsd, 'safeStopUsd' => $safeStopUsd, 'swingStopPct' => $swingStopPct, 'safeStopPct' => $safeStopPct, 'protectedGainPct' => $protectedGainPct, 'sharesTrim' => $sharesTrim, 'sharesHold' => $sharesHold, 'note' => $note, ]; } // ---------- Load positions ---------- $positions = []; $loadError = null; if (!file_exists($jsonFile)) { $loadError = "stocks.json not found at {$jsonFile}"; } else { $raw = file_get_contents($jsonFile); $decoded = json_decode($raw, true); if (json_last_error() !== JSON_ERROR_NONE) { $loadError = 'stocks.json is not valid JSON: ' . json_last_error_msg(); } else { $items = $decoded['positions'] ?? $decoded; foreach ($items as $item) { if (!isset($item['nrbght'], $item['stock'], $item['price']) || $item['nrbght'] <= 0) { continue; } $positions[] = [ 'ticker' => $item['stock'], 'shares' => (float) $item['nrbght'], 'entry' => (float) $item['price'], 'depot' => $item['depot'] ?? '', 'isin' => $item['isin'] ?? '', 'date' => $item['date'] ?? '', ]; } } } // ---------- Fetch quotes + build rows ---------- $rows = []; $totalCost = 0.0; $totalValue = 0.0; $fxCache = []; foreach ($positions as $pos) { $quote = fetchYahooQuote($pos['ticker'], $userAgent, true, $historyDays); $cost = $pos['shares'] * $pos['entry']; $totalCost += $cost; if ($quote === null) { $rows[] = ['currentPriceUsd' => null, 'nativeCurrency' => 'N/A', 'fxRate' => null, 'dayChangePct' => null, 'value' => null, 'gainUsd' => null, 'gainPct' => null, 'action' => '—', 'swingStopUsd' => null, 'safeStopUsd' => null, 'swingStopPct' => null, 'safeStopPct' => null, 'protectedGainPct' => null, 'predictions' => null, 'classification' => null, 'advice' => 'Price fetch failed', 'flag' => 'Price fetch failed'] + $pos; continue; } $nativePrice = $quote['price']; $nativeCurrency = $quote['currency']; $fxRate = getFxRateToUsd($nativeCurrency, $userAgent, $fxCache); if ($fxRate === null) { $rows[] = ['currentPriceUsd' => null, 'nativeCurrency' => $nativeCurrency, 'fxRate' => null, 'dayChangePct' => null, 'value' => null, 'gainUsd' => null, 'gainPct' => null, 'action' => '—', 'swingStopUsd' => null, 'safeStopUsd' => null, 'swingStopPct' => null, 'safeStopPct' => null, 'protectedGainPct' => null, 'predictions' => null, 'classification' => null, 'advice' => 'FX rate unavailable', 'flag' => "FX rate {$nativeCurrency}->USD unavailable"] + $pos; continue; } $currentUsd = round($nativePrice * $fxRate, 4); $value = $pos['shares'] * $currentUsd; $totalValue += $value; $gainUsd = $value - $cost; $gainPct = $cost > 0 ? ($gainUsd / $cost) * 100 : 0; $dayChangePct = null; if ($quote['prevClose']) { $dayChangePct = (($nativePrice - $quote['prevClose']) / $quote['prevClose']) * 100; } $stats = computeRecentStats($quote['history']); $advice = buildAdviceWithTwoStops($pos['shares'], $pos['entry'], $currentUsd, $fxRate, $dayChangePct, $stats, $minTrimProfitUsd); $predictions = generatePredictions($currentUsd, $quote['history'], $fxRate); // Classify the stock $avgMove = $predictions['avgDailyMove'] ?? 5; $stdDev = $predictions['stdDev'] ?? 5; $avgRangePct = $stats['avgRangePct'] ?? 5; $classification = classifyStock($currentUsd, $avgMove, $stdDev, $gainPct, $avgRangePct); // Generate actionable advice $rowData = [ 'ticker' => $pos['ticker'], 'entry' => $pos['entry'], 'currentPriceUsd' => $currentUsd, 'gainPct' => $gainPct, 'gainUsd' => $gainUsd, 'safeStopUsd' => $advice['safeStopUsd'], 'safeStopPct' => $advice['safeStopPct'], 'swingStopUsd' => $advice['swingStopUsd'], 'protectedGainPct' => $advice['protectedGainPct'] ?? 0 ]; $actionableAdvice = generateActionableAdvice($rowData, $predictions, $classification); $rows[] = [ 'currentPriceUsd' => $currentUsd, 'nativeCurrency' => $nativeCurrency, 'fxRate' => $fxRate, 'dayChangePct' => $dayChangePct, 'value' => $value, 'gainUsd' => $gainUsd, 'gainPct' => $gainPct, 'action' => $advice['action'], 'swingStopUsd' => $advice['swingStopUsd'], 'safeStopUsd' => $advice['safeStopUsd'], 'swingStopPct' => $advice['swingStopPct'], 'safeStopPct' => $advice['safeStopPct'], 'protectedGainPct'=> $advice['protectedGainPct'] ?? 0, 'predictions' => $predictions, 'classification' => $classification, 'advice' => $actionableAdvice, 'flag' => $advice['note'], ] + $pos; } usort($rows, function($a, $b) { if ($a['gainPct'] === null && $b['gainPct'] === null) return 0; if ($a['gainPct'] === null) return 1; if ($b['gainPct'] === null) return -1; return $b['gainPct'] <=> $a['gainPct']; }); $totalGainUsd = $totalValue - $totalCost; $totalGainPct = $totalCost > 0 ? ($totalGainUsd / $totalCost) * 100 : 0; ?>
= htmlspecialchars($loadError) ?>
| # | Ticker | Depot | Shares | Entry (USD) | Current (USD) | Gain % | Classification | Next Day Predictions | Best Course of Action | Stops |
|---|---|---|---|---|---|---|---|---|---|---|
| = $rank++ ?> | = htmlspecialchars($r['ticker']) ?> | = htmlspecialchars($r['depot']) ?> | = number_format($r['shares'], 2) ?> | = number_format($r['entry'], 2) ?> | = $r['currentPriceUsd'] !== null ? number_format($r['currentPriceUsd'], 2) : '—' ?> |
= $gainPct !== null ? number_format($gainPct, 2) . '%' : '—' ?>
0 && $gainPct > 0): ?>
🔒 = number_format($protectedPct, 0) ?>%
|
= $class['emoji'] ?> = $class['label'] ?> = $class['quip'] ?> — |
▲
$= number_format($pred['best'], 2) ?>
■
$= number_format($pred['median'], 2) ?>
▼
$= number_format($pred['worst'], 2) ?>
'📊 High', 'medium' => '📈 Mod', 'low' => '📉 Low'];
$confClass = 'confidence-' . ($pred['confidence'] ?? 'low');
?>
= $confText[$pred['confidence'] ?? 'low'] ?? 'Low' ?> (= round($confScore) ?>%)
• = $pred['trend'] === 'up' ? '📈' : ($pred['trend'] === 'down' ? '📉' : '➡️') ?>
= $pred['trend'] ?>
= $dataPoints ?> days
|
= htmlspecialchars($advice) ?> |
🛡️ SAFE: $= number_format($r['safeStopUsd'], 2) ?> (= number_format($r['safeStopPct'], 1) ?>%)
0): ?>
— = number_format($protectedPct, 0) ?>%
—
🔄 SWING: $= number_format($r['swingStopUsd'], 2) ?> (= number_format($r['swingStopPct'], 1) ?>%) |