566 lines
23 KiB
Svelte
566 lines
23 KiB
Svelte
<script lang="ts">
|
|
import {
|
|
numberOfUnreadNotification,
|
|
displayCompanyName,
|
|
stockTicker,
|
|
analystEstimateComponent,
|
|
} from "$lib/store";
|
|
import { abbreviateNumber } from "$lib/utils";
|
|
|
|
export let data;
|
|
let index = 0;
|
|
let changeRevenue = 0;
|
|
let changeNetIncome = 0;
|
|
let changeEBITDA = 0;
|
|
let changeEPS = 0;
|
|
|
|
const price = data?.getStockQuote?.price?.toFixed(2) || 0;
|
|
|
|
const calculatePriceChange = (targetPrice) =>
|
|
targetPrice && price ? ((targetPrice / price - 1) * 100)?.toFixed(2) : 0;
|
|
|
|
const numOfAnalyst = data?.getAnalystRating?.numOfAnalyst || 0;
|
|
const avgPriceTarget = data?.getAnalystRating?.avgPriceTarget || 0;
|
|
const medianPriceTarget = data?.getAnalystRating?.medianPriceTarget || 0;
|
|
const lowPriceTarget = data?.getAnalystRating?.lowPriceTarget || 0;
|
|
const highPriceTarget = data?.getAnalystRating?.highPriceTarget || 0;
|
|
const consensusRating = data?.getAnalystRating?.consensusRating;
|
|
|
|
const lowChange = calculatePriceChange(lowPriceTarget);
|
|
const medianChange = calculatePriceChange(medianPriceTarget);
|
|
const avgChange = calculatePriceChange(avgPriceTarget);
|
|
const highChange = calculatePriceChange(highPriceTarget);
|
|
const rawAnalystList = data?.getAnalystRating?.recommendationList || [];
|
|
const recommendationList =
|
|
rawAnalystList?.length > 5 ? rawAnalystList?.slice(-6, -1) : rawAnalystList;
|
|
const categories = ["Strong Buy", "Buy", "Hold", "Sell", "Strong Sell"];
|
|
|
|
function findIndex(data) {
|
|
const currentYear = new Date().getFullYear();
|
|
|
|
// Find the index where the item's date is greater than the current year and revenue is null
|
|
const index = data?.findIndex(
|
|
(item) => item?.date > currentYear && item?.revenue === null,
|
|
);
|
|
|
|
// If index is found and there is at least one item in the data for the current year with non-null revenue
|
|
if (index !== -1) {
|
|
const hasNonNullRevenue = data?.some(
|
|
(item) => item?.date === currentYear && item?.revenue !== null,
|
|
);
|
|
|
|
// Add +1 to the index if the condition is met
|
|
return hasNonNullRevenue ? index + 1 : index;
|
|
}
|
|
|
|
return index; // Return the index or -1 if not found
|
|
}
|
|
|
|
function getTotalForDate(index) {
|
|
return categories.reduce(
|
|
(sum, cat) => sum + recommendationList[index][cat],
|
|
0,
|
|
);
|
|
}
|
|
|
|
const calculateChange = (current, previous) => {
|
|
if (previous !== undefined && previous !== 0) {
|
|
const change = (Math.abs(current) / Math.abs(previous) - 1) * 100;
|
|
// Preserve the direction of change (positive/negative)
|
|
const direction = current < 0 || previous < 0 ? -1 : 1;
|
|
return change * direction;
|
|
} else {
|
|
return null;
|
|
}
|
|
};
|
|
|
|
if (data?.getAnalystEstimate?.length !== 0) {
|
|
index = findIndex(data?.getAnalystEstimate);
|
|
|
|
// Calculate changes using the helper function
|
|
const estimatedRevenueAvg =
|
|
data?.getAnalystEstimate[index - 1]?.estimatedRevenueAvg;
|
|
const revenue = data?.getAnalystEstimate[index - 2]?.revenue;
|
|
const estimatedNetIncomeAvg =
|
|
data?.getAnalystEstimate[index - 1]?.estimatedNetIncomeAvg;
|
|
const netIncome = data?.getAnalystEstimate[index - 2]?.netIncome;
|
|
const estimatedEbitdaAvg =
|
|
data?.getAnalystEstimate[index - 1]?.estimatedEbitdaAvg;
|
|
const ebitda = data?.getAnalystEstimate[index - 2]?.ebitda;
|
|
const estimatedEpsAvg =
|
|
data?.getAnalystEstimate[index - 1]?.estimatedEpsAvg;
|
|
const eps = data?.getAnalystEstimate[index - 2]?.eps;
|
|
|
|
// Calculate percentage changes for each metric
|
|
changeRevenue = calculateChange(estimatedRevenueAvg, revenue);
|
|
changeNetIncome = calculateChange(estimatedNetIncomeAvg, netIncome);
|
|
changeEBITDA = calculateChange(estimatedEbitdaAvg, ebitda);
|
|
changeEPS = calculateChange(estimatedEpsAvg, eps);
|
|
}
|
|
</script>
|
|
|
|
<svelte:head>
|
|
<meta charset="utf-8" />
|
|
<meta name="viewport" content="width=device-width" />
|
|
<title>
|
|
{$numberOfUnreadNotification > 0 ? `(${$numberOfUnreadNotification})` : ""}
|
|
{$displayCompanyName} ({$stockTicker}) Forecast Overview · stocknear
|
|
</title>
|
|
<meta
|
|
name="description"
|
|
content={`A list of analyst ratings for Advanced Micro Devices (AMD) stock. See upgrades, downgrades, price targets and more from top Wall Street stock analysts.`}
|
|
/>
|
|
|
|
<!-- Other meta tags -->
|
|
<meta
|
|
property="og:title"
|
|
content={`${$displayCompanyName} (${$stockTicker}) Forecast Overview · stocknear`}
|
|
/>
|
|
<meta
|
|
property="og:description"
|
|
content={`A list of analyst ratings for Advanced Micro Devices (AMD) stock. See upgrades, downgrades, price targets and more from top Wall Street stock analysts.`}
|
|
/>
|
|
<meta property="og:type" content="website" />
|
|
<!-- Add more Open Graph meta tags as needed -->
|
|
|
|
<!-- Twitter specific meta tags -->
|
|
<meta name="twitter:card" content="summary_large_image" />
|
|
<meta
|
|
name="twitter:title"
|
|
content={`${$displayCompanyName} (${$stockTicker}) Forecast Overview · stocknear`}
|
|
/>
|
|
<meta
|
|
name="twitter:description"
|
|
content={`A list of analyst ratings for Advanced Micro Devices (AMD) stock. See upgrades, downgrades, price targets and more from top Wall Street stock analysts.`}
|
|
/>
|
|
<!-- Add more Twitter meta tags as needed -->
|
|
</svelte:head>
|
|
|
|
<section
|
|
class="w-full bg-[#09090B] overflow-hidden text-white h-full mb-40 sm:mb-0"
|
|
>
|
|
<div class="w-full flex h-full overflow-hidden">
|
|
<div
|
|
class="w-full relative flex justify-center items-center overflow-hidden"
|
|
>
|
|
<div class="sm:pl-6 sm:pr-7 sm:pt-5 w-full m-auto mt-2 sm:mt-0">
|
|
<h1 class="mb-px text-2xl font-bold bp:text-3xl sm:pl-1">
|
|
{$displayCompanyName} Forcast
|
|
</h1>
|
|
<div class="w-full mb-6 mt-3">
|
|
<div
|
|
class="rounded-sm border border-gray-600 p-0.5 xs:p-1 md:flex md:flex-col md:space-y-4 md:divide-y md:p-4 lg:flex-row lg:space-x-4 lg:space-y-0 lg:divide-x lg:divide-y-0 divide-gray-600"
|
|
>
|
|
<div
|
|
class="p-3 md:flex md:space-x-4 md:p-0 lg:block lg:max-w-[32%] lg:space-x-0"
|
|
>
|
|
<div>
|
|
<div class="flex items-baseline justify-between">
|
|
<h2 class="mb-1 text-xl font-bold">Stock Price Forecast</h2>
|
|
<span></span>
|
|
</div>
|
|
<p>
|
|
The {numOfAnalyst} analysts with 12-month price forecasts for {$stockTicker}
|
|
stock have an median target of {medianPriceTarget}, with a low
|
|
estimate of {lowPriceTarget}
|
|
and a high estimate of {highPriceTarget}. The median target
|
|
predicts an increase of {medianChange}% from the current stock
|
|
price of {price}.
|
|
</p>
|
|
</div>
|
|
<div>
|
|
<div class="h-[160px]">
|
|
<canvas
|
|
id="myChart"
|
|
style="display: block; box-sizing: border-box; height: 160px; width: 352px;"
|
|
width="529"
|
|
height="240"
|
|
></canvas>
|
|
</div>
|
|
<div class="-mt-2 text-center text-xl font-semibold">
|
|
Analyst Consensus: <span
|
|
class="font-bold {['Strong Buy', 'Buy']?.includes(
|
|
consensusRating,
|
|
)
|
|
? 'text-[#00FC50]'
|
|
: ['Strong Sell', 'Sell']?.includes(consensusRating)
|
|
? 'text-[#FF2F1F]'
|
|
: 'text-[#FF9E21]'}">{consensusRating}</span
|
|
>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="grow pt-2 md:pt-4 lg:pl-4 lg:pt-0">
|
|
<div class="h-[250px] xs:h-[275px]">
|
|
<canvas
|
|
id="myChart"
|
|
style="display: block; box-sizing: border-box; height: 275px; width: 728px;"
|
|
width="1092"
|
|
height="412"
|
|
></canvas>
|
|
</div>
|
|
<div
|
|
class="hide-scroll mb-1 mt-2 overflow-x-auto px-1.5 text-center md:mb-0 md:px-0 lg:mt-2"
|
|
>
|
|
<table
|
|
class="w-full text-right text-tiny text-white xs:text-sm sm:text-base"
|
|
>
|
|
<thead
|
|
><tr class="border-b border-gray-600 font-normal"
|
|
><th class="py-[3px] text-left font-semibold lg:py-0.5"
|
|
>Target</th
|
|
> <th class="font-semibold">Low</th>
|
|
<th class="font-semibold">Average</th>
|
|
<th class="font-semibold">Median</th>
|
|
<th class="font-semibold">High</th></tr
|
|
></thead
|
|
>
|
|
<tbody
|
|
><tr class="border-b border-gray-600 font-normal"
|
|
><td class="py-[3px] text-left lg:py-0.5">Price</td>
|
|
<td>${lowPriceTarget}</td>
|
|
<td>${avgPriceTarget}</td> <td>${medianPriceTarget}</td>
|
|
<td>${highPriceTarget}</td></tr
|
|
>
|
|
<tr
|
|
><td class="py-[3px] text-left lg:py-0.5">Change</td>
|
|
<td
|
|
class={lowChange > 0
|
|
? "before:content-['+'] text-[#00FC50]"
|
|
: "text-[#FF2F1F]"}>{lowChange}%</td
|
|
>
|
|
<td
|
|
class={avgChange > 0
|
|
? "before:content-['+'] text-[#00FC50]"
|
|
: "text-[#FF2F1F]"}>{avgChange}%</td
|
|
>
|
|
<td
|
|
class={medianChange > 0
|
|
? "before:content-['+'] text-[#00FC50]"
|
|
: "text-[#FF2F1F]"}>{lowChange}%</td
|
|
>
|
|
<td
|
|
class={highChange > 0
|
|
? "before:content-['+'] text-[#00FC50]"
|
|
: "text-[#FF2F1F]"}>{highChange}%</td
|
|
></tr
|
|
></tbody
|
|
>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div
|
|
class="w-full rounded-sm border border-gray-600 p-3 divide-gray-600 lg:flex lg:space-x-4 lg:divide-x"
|
|
>
|
|
<div
|
|
class="flex flex-col justify-between p-1 lg:max-w-[32%] text-white"
|
|
>
|
|
<div>
|
|
<h2 class="mb-1 text-xl font-bold">Analyst Ratings</h2>
|
|
<p>
|
|
According to {numOfAnalyst} stock analyst, the rating for GameStop
|
|
is "{consensusRating}". This means that the analyst believes
|
|
this stock is likely to lead to {[
|
|
"Strong Sell",
|
|
"Sell",
|
|
]?.includes(consensusRating)
|
|
? "lower"
|
|
: ["Strong Buy", "Buy"]?.includes(consensusRating)
|
|
? "higher"
|
|
: "similar"} returns than market as a whole.
|
|
</p>
|
|
</div>
|
|
</div>
|
|
<div class="grow pt-2 md:pt-4 lg:pl-4 lg:pt-0">
|
|
<div class="h-[250px] xs:h-[275px]">
|
|
<canvas
|
|
id="myChart"
|
|
style="display: block; box-sizing: border-box; height: 275px; width: 728px;"
|
|
width="1092"
|
|
height="412"
|
|
></canvas>
|
|
</div>
|
|
<div
|
|
class="hide-scroll mb-1 mt-2 overflow-x-auto px-1.5 text-center md:mb-0 md:px-0 lg:mt-2"
|
|
>
|
|
<table
|
|
class="w-full text-right text-tiny xs:text-sm md:text-smaller"
|
|
>
|
|
<thead
|
|
><tr class="border-b border-gray-600 font-normal"
|
|
><th
|
|
class="whitespace-nowrap px-1 py-[3px] text-left font-semibold"
|
|
>Rating</th
|
|
>
|
|
{#each recommendationList as item}
|
|
<th class="px-1 py-[3px] text-right font-semibold"
|
|
>{new Intl.DateTimeFormat("en", {
|
|
month: "short",
|
|
year: "2-digit",
|
|
}).format(new Date(item?.date))}</th
|
|
>
|
|
{/each}
|
|
</tr></thead
|
|
>
|
|
<tbody>
|
|
{#each categories as category}
|
|
<tr class="border-b border-gray-600 font-normal">
|
|
<td class="whitespace-nowrap px-1 py-[3px] text-left"
|
|
>{category}</td
|
|
>
|
|
{#each recommendationList as entry}
|
|
<td class="px-1 py-[3px] text-right"
|
|
>{entry[category]}</td
|
|
>
|
|
{/each}
|
|
</tr>
|
|
{/each}
|
|
<tr class="font-semibold"> </tr><tr class="font-semibold">
|
|
<td class="whitespace-nowrap px-1 py-[3px] text-left"
|
|
>Total</td
|
|
>
|
|
{#each recommendationList as _, i}
|
|
<td class="px-1 py-[3px] text-right">
|
|
{getTotalForDate(i)}
|
|
</td>
|
|
{/each}
|
|
</tr>
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<h2 class="mt-5 text-xl sm:text-2xl text-white font-bold mb-4">
|
|
Financial Forecast this Year
|
|
</h2>
|
|
{#if data?.getAnalystEstimate?.length !== 0}
|
|
<div
|
|
class="mb-4 grid grid-cols-2 grid-rows-1 divide-gray-600 rounded-lg border border-gray-600 shadow md:grid-cols-4 md:grid-rows-1 md:divide-x"
|
|
>
|
|
<div class="p-4 bp:p-5 sm:p-6">
|
|
<label
|
|
class="mr-1 cursor-pointer flex flex-row items-center text-white text-[1rem] font-semibold"
|
|
>
|
|
Revenue
|
|
</label>
|
|
<div
|
|
class="mt-1 flex flex-col items-baseline justify-start space-y-2 bp:space-y-0"
|
|
>
|
|
<div
|
|
class="flex items-baseline text-2xl font-semibold text-white"
|
|
>
|
|
{abbreviateNumber(
|
|
data?.getAnalystEstimate[index - 1]?.estimatedRevenueAvg,
|
|
)}
|
|
</div>
|
|
<div
|
|
class="inline-flex items-baseline rounded-full px-2.5 py-0.5 text-sm font-semibold md:mt-2 lg:mt-0 bg-[#FBCE3C] text-black"
|
|
>
|
|
<svg
|
|
class="-ml-1 mr-0.5 h-5 w-5 flex-shrink-0 self-center {changeRevenue >
|
|
0
|
|
? ''
|
|
: 'rotate-180'}"
|
|
fill="none"
|
|
viewBox="0 0 24 24"
|
|
stroke="currentColor"
|
|
style="max-width:40px"
|
|
aria-hidden="true"
|
|
><path
|
|
stroke-linecap="round"
|
|
stroke-linejoin="round"
|
|
stroke-width="2"
|
|
d="M7 11l5-5m0 0l5 5m-5-5v12"
|
|
></path></svg
|
|
>
|
|
{abbreviateNumber(changeRevenue?.toFixed(1))}%
|
|
</div>
|
|
</div>
|
|
<div
|
|
class="ml-0.5 mt-1.5 text-sm font-semibold text-white/60 lg:block"
|
|
>
|
|
from {abbreviateNumber(
|
|
data?.getAnalystEstimate[index - 2]?.revenue,
|
|
)}
|
|
</div>
|
|
</div>
|
|
<div
|
|
class="p-4 bp:p-5 sm:p-6 border-l border-contrast md:border-0"
|
|
>
|
|
<label
|
|
class="mr-1 cursor-pointer flex flex-row items-center text-white text-[1rem] font-semibold"
|
|
>
|
|
Net Income
|
|
</label>
|
|
<div
|
|
class="mt-1 flex flex-col items-baseline justify-start space-y-2 bp:space-y-0"
|
|
>
|
|
<div
|
|
class="flex items-baseline text-2xl font-semibold text-white"
|
|
>
|
|
{abbreviateNumber(
|
|
data?.getAnalystEstimate[index - 1]
|
|
?.estimatedNetIncomeAvg,
|
|
)}
|
|
</div>
|
|
<div
|
|
class="inline-flex items-baseline rounded-full px-2.5 py-0.5 text-sm font-semibold md:mt-2 lg:mt-0 bg-[#FBCE3C] text-black"
|
|
>
|
|
<svg
|
|
class="-ml-1 mr-0.5 h-5 w-5 flex-shrink-0 self-center {changeNetIncome >
|
|
0
|
|
? ''
|
|
: 'rotate-180'}"
|
|
fill="none"
|
|
viewBox="0 0 24 24"
|
|
stroke="currentColor"
|
|
style="max-width:40px"
|
|
aria-hidden="true"
|
|
><path
|
|
stroke-linecap="round"
|
|
stroke-linejoin="round"
|
|
stroke-width="2"
|
|
d="M7 11l5-5m0 0l5 5m-5-5v12"
|
|
></path></svg
|
|
>
|
|
{abbreviateNumber(changeNetIncome?.toFixed(1))}%
|
|
</div>
|
|
</div>
|
|
<div
|
|
class="ml-0.5 mt-1.5 text-sm font-semibold text-white/60 lg:block"
|
|
>
|
|
from {abbreviateNumber(
|
|
data?.getAnalystEstimate[index - 2]?.netIncome,
|
|
)}
|
|
</div>
|
|
</div>
|
|
<div
|
|
class="p-4 bp:p-5 sm:p-6 border-t border-contrast md:border-0"
|
|
>
|
|
<label
|
|
class="mr-1 cursor-pointer flex flex-row items-center text-white text-[1rem] font-semibold"
|
|
>
|
|
EBITDA
|
|
</label>
|
|
<div
|
|
class="mt-1 flex flex-col items-baseline justify-start space-y-2 bp:space-y-0"
|
|
>
|
|
<div
|
|
class="flex items-baseline text-2xl font-semibold text-white"
|
|
>
|
|
{abbreviateNumber(
|
|
data?.getAnalystEstimate[index - 1]?.estimatedEbitdaAvg,
|
|
)}
|
|
</div>
|
|
<div
|
|
class="inline-flex items-baseline rounded-full px-2.5 py-0.5 text-sm font-semibold md:mt-2 lg:mt-0 bg-[#FBCE3C] text-black"
|
|
>
|
|
<svg
|
|
class="-ml-1 mr-0.5 h-5 w-5 flex-shrink-0 self-center {changeEBITDA >
|
|
0
|
|
? ''
|
|
: 'rotate-180'}"
|
|
fill="none"
|
|
viewBox="0 0 24 24"
|
|
stroke="currentColor"
|
|
style="max-width:40px"
|
|
aria-hidden="true"
|
|
><path
|
|
stroke-linecap="round"
|
|
stroke-linejoin="round"
|
|
stroke-width="2"
|
|
d="M7 11l5-5m0 0l5 5m-5-5v12"
|
|
></path></svg
|
|
>
|
|
{abbreviateNumber(changeEBITDA?.toFixed(2))}%
|
|
</div>
|
|
</div>
|
|
<div
|
|
class="ml-0.5 mt-1.5 text-sm font-semibold text-white/60 lg:block"
|
|
>
|
|
from {abbreviateNumber(
|
|
data?.getAnalystEstimate[index - 2]?.ebitda,
|
|
)}
|
|
</div>
|
|
</div>
|
|
<div
|
|
class="p-4 bp:p-5 sm:p-6 border-t border-contrast md:border-0 border-l border-contrast md:border-0"
|
|
>
|
|
<label
|
|
class="mr-1 cursor-pointer flex flex-row items-center text-white text-[1rem] font-semibold"
|
|
>
|
|
EPS
|
|
</label>
|
|
<div
|
|
class="mt-1 flex flex-col items-baseline justify-start space-y-2 bp:space-y-0"
|
|
>
|
|
<div
|
|
class="flex items-baseline text-2xl font-semibold text-white"
|
|
>
|
|
{data?.getAnalystEstimate[index - 1]?.estimatedEpsAvg}
|
|
</div>
|
|
<div
|
|
class="inline-flex items-baseline rounded-full px-2.5 py-0.5 text-sm font-semibold md:mt-2 lg:mt-0 bg-[#FBCE3C] text-black"
|
|
>
|
|
<svg
|
|
class="-ml-1 mr-0.5 h-5 w-5 flex-shrink-0 self-center {changeEPS >
|
|
0
|
|
? ''
|
|
: 'rotate-180'}"
|
|
fill="none"
|
|
viewBox="0 0 24 24"
|
|
stroke="currentColor"
|
|
style="max-width:40px"
|
|
aria-hidden="true"
|
|
><path
|
|
stroke-linecap="round"
|
|
stroke-linejoin="round"
|
|
stroke-width="2"
|
|
d="M7 11l5-5m0 0l5 5m-5-5v12"
|
|
></path></svg
|
|
>
|
|
{abbreviateNumber(changeEPS?.toFixed(1))}%
|
|
</div>
|
|
</div>
|
|
<div
|
|
class="ml-0.5 mt-1.5 text-sm font-semibold text-white/60 lg:block"
|
|
>
|
|
from {data?.getAnalystEstimate[index - 2]?.eps}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div
|
|
class="w-full m-auto sm:pb-6 sm:pt-6 {!$analystEstimateComponent
|
|
? 'hidden'
|
|
: ''}"
|
|
>
|
|
{#await import("$lib/components/AnalystEstimate.svelte") then { default: Comp }}
|
|
<svelte:component this={Comp} {data} />
|
|
{/await}
|
|
</div>
|
|
{:else}
|
|
<div
|
|
class="text-white p-3 sm:p-5 mb-10 rounded-lg sm:flex sm:flex-row sm:items-center border border-slate-800 text-sm sm:text-[1rem]"
|
|
>
|
|
<svg
|
|
class="w-6 h-6 flex-shrink-0 inline-block sm:mr-2"
|
|
xmlns="http://www.w3.org/2000/svg"
|
|
viewBox="0 0 256 256"
|
|
><path
|
|
fill="#a474f6"
|
|
d="M128 24a104 104 0 1 0 104 104A104.11 104.11 0 0 0 128 24m-4 48a12 12 0 1 1-12 12a12 12 0 0 1 12-12m12 112a16 16 0 0 1-16-16v-40a8 8 0 0 1 0-16a16 16 0 0 1 16 16v40a8 8 0 0 1 0 16"
|
|
/></svg
|
|
>
|
|
No analyst forecast available for {$displayCompanyName}.
|
|
</div>
|
|
{/if}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</section>
|