1013 lines
40 KiB
Svelte
1013 lines
40 KiB
Svelte
<script lang="ts">
|
|
import { displayCompanyName, stockTicker, screenWidth } from "$lib/store";
|
|
import { removeCompanyStrings } from "$lib/utils";
|
|
import Infobox from "$lib/components/Infobox.svelte";
|
|
import highcharts from "$lib/highcharts.ts";
|
|
import { mode } from "mode-watcher";
|
|
|
|
import SEO from "$lib/components/SEO.svelte";
|
|
|
|
export let data;
|
|
|
|
const isPro = ["Pro", "Plus"].includes(data?.user?.tier);
|
|
|
|
const formatDateToQuarter = (timestamp) => {
|
|
const date = new Date(timestamp);
|
|
const year = date.getFullYear().toString().slice(-2); // Get last two digits of the year
|
|
const quarter = Math.floor(date.getMonth() / 3) + 1; // Determine quarter (Q1-Q4)
|
|
return `Q${quarter} '${year}`;
|
|
};
|
|
|
|
const calculatePriceChange = (targetPrice) =>
|
|
targetPrice && price ? ((targetPrice / price - 1) * 100)?.toFixed(2) : 0;
|
|
|
|
function prepareDataset() {
|
|
price = data?.getStockQuote?.price?.toFixed(2) || 0;
|
|
|
|
avgPriceTarget = data?.getPriceAnalysis?.avgPriceTarget || 0;
|
|
medianPriceTarget = data?.getPriceAnalysis?.medianPriceTarget || 0;
|
|
lowPriceTarget = data?.getPriceAnalysis?.lowPriceTarget || 0;
|
|
highPriceTarget = data?.getPriceAnalysis?.highPriceTarget || 0;
|
|
|
|
lowChange = calculatePriceChange(lowPriceTarget);
|
|
medianChange = calculatePriceChange(medianPriceTarget);
|
|
avgChange = calculatePriceChange(avgPriceTarget);
|
|
highChange = calculatePriceChange(highPriceTarget);
|
|
|
|
// Assume data.getHistoricalPrice contains objects with a "time" field (e.g. "2015-01-02")
|
|
historicalData = data?.getHistoricalPrice || [];
|
|
|
|
backtestList = data?.getAIScore?.backtest || [];
|
|
|
|
// Append the latest historical date (using "time") if available. Note that this entry may not include a score.
|
|
if (historicalData && historicalData?.length) {
|
|
const latest = historicalData?.at(-1);
|
|
backtestList?.push({ date: latest.time });
|
|
|
|
const seenDates = new Set();
|
|
backtestList = backtestList?.filter((item) => {
|
|
// Check if the date is already seen
|
|
if (seenDates?.has(item?.date)) {
|
|
// If yes, skip this duplicate (delete the last one)
|
|
return false;
|
|
}
|
|
// Otherwise, record the date and keep the item
|
|
seenDates?.add(item?.date);
|
|
return true;
|
|
});
|
|
}
|
|
|
|
processedData = backtestList?.map((item) => {
|
|
const dateStr = item.date;
|
|
const targetTime = new Date(dateStr).getTime();
|
|
let closestPoint = historicalData[0];
|
|
let minDiff = Infinity;
|
|
|
|
historicalData.forEach((point) => {
|
|
const pointTime = new Date(point.time).getTime();
|
|
const diff = Math.abs(pointTime - targetTime);
|
|
if (diff < minDiff) {
|
|
minDiff = diff;
|
|
closestPoint = point;
|
|
}
|
|
});
|
|
|
|
// Base data point with x as the target date timestamp and y as the close price from the closest historical point
|
|
const dataPoint = { x: targetTime, y: closestPoint.close };
|
|
|
|
// If a score is provided, add marker configuration based on its value.
|
|
if (item.hasOwnProperty("score")) {
|
|
let markerColor, markerSymbol;
|
|
if (item.score > 6) {
|
|
// Bullish: green marker with an upward triangle
|
|
markerColor = "#007050";
|
|
markerSymbol = "circle";
|
|
} else if (item.score < 5) {
|
|
// Bearish: red marker with a downward triangle
|
|
markerColor = "#007050";
|
|
markerSymbol = "circle";
|
|
} else {
|
|
// Neutral (score exactly 5): yellow marker with a circle
|
|
markerColor = "#007050";
|
|
markerSymbol = "circle";
|
|
}
|
|
|
|
dataPoint.marker = {
|
|
symbol: markerSymbol,
|
|
radius: 4,
|
|
fillColor: markerColor,
|
|
lineWidth: 2,
|
|
lineColor: $mode === "light" ? "black" : "white",
|
|
};
|
|
|
|
dataPoint.dataLabels = {
|
|
enabled: true,
|
|
format: String(item.score),
|
|
style: {
|
|
color: $mode === "light" ? "black" : "white",
|
|
fontWeight: "bold",
|
|
fontSize: "14px",
|
|
},
|
|
y: -10,
|
|
};
|
|
}
|
|
|
|
return dataPoint;
|
|
});
|
|
|
|
tableDates = processedData
|
|
?.slice(0, -1)
|
|
?.map((item) => formatDateToQuarter(item?.x));
|
|
|
|
tableScore = processedData
|
|
?.slice(0, -1)
|
|
?.map((item) => item?.dataLabels?.format);
|
|
|
|
// Compute percentage change
|
|
tableQuarterChange = processedData
|
|
?.slice(0, -1)
|
|
?.map((item, index, arr) => {
|
|
const prevY = arr[index - 1]?.y; // Get the previous value
|
|
if (prevY == null || item.y == null) return null; // Handle missing values
|
|
const change = ((item.y - prevY) / prevY) * 100; // Calculate percentage change
|
|
return {
|
|
quarter: tableDates[index],
|
|
change: Number(change?.toFixed(2)), // Format to 2 decimal places
|
|
};
|
|
})
|
|
?.filter(Boolean); // Remove null values
|
|
|
|
// Compute Average Return
|
|
returns = processedData
|
|
?.slice(1) // Skip the first value since there's no previous value for it
|
|
?.map((item, index) => {
|
|
const prevY = processedData[index]?.y;
|
|
if (prevY == null || item.y == null) return null;
|
|
const returnPercentage = ((item.y - prevY) / prevY) * 100;
|
|
return returnPercentage;
|
|
})
|
|
.filter(Boolean); // Remove null values
|
|
|
|
avgReturn =
|
|
returns?.reduce((sum, returnPercentage) => sum + returnPercentage, 0) /
|
|
returns?.length;
|
|
}
|
|
|
|
let price;
|
|
|
|
let avgPriceTarget;
|
|
let medianPriceTarget;
|
|
let lowPriceTarget;
|
|
let highPriceTarget;
|
|
|
|
let lowChange;
|
|
let medianChange;
|
|
let avgChange;
|
|
let highChange;
|
|
|
|
let historicalData;
|
|
|
|
let backtestList;
|
|
|
|
// For each backtest entry, find the historical price with the closest available time.
|
|
// Then, if a score exists, attach a marker and data label based on the score.
|
|
let processedData = [];
|
|
|
|
let tableDates;
|
|
|
|
let tableScore;
|
|
|
|
// Compute percentage change
|
|
let tableQuarterChange;
|
|
|
|
// Compute Average Return
|
|
let returns;
|
|
|
|
let avgReturn;
|
|
|
|
function getAIScorePlot() {
|
|
const solidData = processedData.slice(0, -1);
|
|
const lastTwoPoints = processedData.slice(-2); // Extract the last two points
|
|
|
|
// Highcharts options for plotting the data with markers.
|
|
const options = {
|
|
chart: {
|
|
backgroundColor: $mode === "light" ? "#fff" : "#09090B",
|
|
height: 360,
|
|
animation: false,
|
|
},
|
|
title: {
|
|
text: `<h3 class="mt-3 mb-1 text-[1rem] sm:text-lg">Historical AI Score Performance</h3>`,
|
|
style: {
|
|
color: $mode === "light" ? "black" : "white",
|
|
// Using inline CSS for margin-top and margin-bottom
|
|
},
|
|
useHTML: true, // Enable HTML to apply custom class styling
|
|
},
|
|
xAxis: {
|
|
gridLineWidth: 1,
|
|
gridLineColor: $mode === "light" ? "#e5e7eb" : "#111827",
|
|
type: "datetime",
|
|
labels: {
|
|
style: {
|
|
color: $mode === "light" ? "#545454" : "white",
|
|
},
|
|
formatter: function () {
|
|
const date = new Date(this.value);
|
|
return date.toLocaleDateString("en-US", {
|
|
month: "short",
|
|
year: "numeric",
|
|
});
|
|
},
|
|
},
|
|
},
|
|
tooltip: {
|
|
enabled: false,
|
|
},
|
|
yAxis: {
|
|
title: {
|
|
text: "",
|
|
},
|
|
labels: {
|
|
style: {
|
|
color: $mode === "light" ? "#545454" : "white",
|
|
},
|
|
formatter: function () {
|
|
return `$${this.value.toFixed(0)}`;
|
|
},
|
|
},
|
|
gridLineWidth: 1,
|
|
gridLineColor: $mode === "light" ? "#e5e7eb" : "#111827",
|
|
},
|
|
series: [
|
|
{
|
|
name: "Close Price",
|
|
data: solidData,
|
|
color: $mode === "light" ? "#007050" : "#fff",
|
|
animation: false,
|
|
marker: {
|
|
enabled: true,
|
|
},
|
|
lineWidth: 3,
|
|
},
|
|
{
|
|
name: "Projected Price",
|
|
data: lastTwoPoints, // Include the second-last and last point
|
|
color: $mode === "light" ? "#007050" : "#fff",
|
|
animation: false,
|
|
marker: {
|
|
enabled: false,
|
|
},
|
|
lineWidth: 2.5,
|
|
dashStyle: "Dash", // Dashed line for the last part
|
|
},
|
|
],
|
|
|
|
plotOptions: {
|
|
series: {
|
|
enableMouseTracking: false,
|
|
states: {
|
|
hover: {
|
|
enabled: false,
|
|
},
|
|
},
|
|
marker: {
|
|
states: {
|
|
hover: {
|
|
enabled: false,
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
legend: {
|
|
enabled: false,
|
|
},
|
|
credits: {
|
|
enabled: false,
|
|
},
|
|
};
|
|
|
|
return options;
|
|
}
|
|
|
|
function getPriceForecastChart() {
|
|
const historicalData = data?.getAnalystSummary?.pastPriceList || [];
|
|
const forecastTargets = {
|
|
low: lowPriceTarget,
|
|
avg: avgPriceTarget,
|
|
high: highPriceTarget,
|
|
};
|
|
|
|
// Process historical data
|
|
const processedHistorical = historicalData?.map((point) => [
|
|
new Date(point?.date).getTime(),
|
|
point?.close,
|
|
]);
|
|
|
|
const currentDate = new Date();
|
|
const forecastDate = new Date(
|
|
currentDate.getFullYear() + 1,
|
|
currentDate.getMonth(),
|
|
currentDate.getDate(),
|
|
);
|
|
const forecastTimestamp = forecastDate.getTime();
|
|
|
|
// Get the last historical data point
|
|
const lastHistoricalDate = new Date(
|
|
historicalData[historicalData.length - 1]?.date,
|
|
).getTime();
|
|
const lastHistoricalClose =
|
|
historicalData[historicalData.length - 1]?.close;
|
|
|
|
// Create forecast points
|
|
const forecastHigh = [
|
|
[lastHistoricalDate, lastHistoricalClose],
|
|
[forecastTimestamp, forecastTargets.high],
|
|
];
|
|
|
|
const forecastAvg = [
|
|
[lastHistoricalDate, lastHistoricalClose],
|
|
[forecastTimestamp, forecastTargets.avg],
|
|
];
|
|
|
|
const forecastLow = [
|
|
[lastHistoricalDate, lastHistoricalClose],
|
|
[forecastTimestamp, forecastTargets.low],
|
|
];
|
|
|
|
const options = {
|
|
tooltip: {
|
|
enabled: false,
|
|
},
|
|
plotOptions: {
|
|
series: {
|
|
enableMouseTracking: false,
|
|
states: {
|
|
hover: {
|
|
enabled: false,
|
|
},
|
|
},
|
|
marker: {
|
|
states: {
|
|
hover: {
|
|
enabled: false,
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
chart: {
|
|
backgroundColor: $mode === "light" ? "#fff" : "#09090B",
|
|
plotBackgroundColor: $mode === "light" ? "#fff" : "#09090B",
|
|
height: 360,
|
|
animation: false,
|
|
},
|
|
title: {
|
|
text: `<div class="grid grid-cols-2 w-[200px] sm:w-[500px] -mb-3.5 text-xs font-[501] text-gray-600 dark:text-gray-400">
|
|
<h3 class="text-left">${$screenWidth && $screenWidth < 640 ? "Past Year" : "Past 12 Months"}</h3>
|
|
<h3 class="text-right">${$screenWidth && $screenWidth < 640 ? "Next Year" : "12 Month Forecast"}</h3>
|
|
</div>`,
|
|
style: {
|
|
color: $mode === "light" ? "black" : "white",
|
|
width: "100%",
|
|
},
|
|
verticalAlign: "top",
|
|
useHTML: true,
|
|
},
|
|
xAxis: {
|
|
gridLineWidth: 1,
|
|
gridLineColor: $mode === "light" ? "#e5e7eb" : "#111827",
|
|
type: "datetime",
|
|
endOnTick: false,
|
|
labels: {
|
|
style: {
|
|
color: $mode === "light" ? "#545454" : "white",
|
|
},
|
|
formatter: function () {
|
|
const date = new Date(this.value);
|
|
return date.toLocaleDateString("en-US", {
|
|
month: "short",
|
|
year: "numeric",
|
|
});
|
|
},
|
|
},
|
|
},
|
|
yAxis: {
|
|
title: {
|
|
text: "",
|
|
},
|
|
labels: {
|
|
style: {
|
|
color: $mode === "light" ? "#545454" : "white",
|
|
},
|
|
formatter: function () {
|
|
return `$${this.value.toFixed(0)}`;
|
|
},
|
|
},
|
|
gridLineWidth: 1,
|
|
gridLineColor: $mode === "light" ? "#e5e7eb" : "#111827",
|
|
},
|
|
|
|
series: [
|
|
{
|
|
animation: false,
|
|
name: "Historical",
|
|
data: processedHistorical,
|
|
color: $mode === "light" ? "#007050" : "#fff",
|
|
marker: {
|
|
enabled: true,
|
|
symbol: "circle",
|
|
radius: 4,
|
|
},
|
|
lineWidth: 3,
|
|
},
|
|
{
|
|
animation: false,
|
|
name: "High",
|
|
data: forecastHigh,
|
|
color: "#31B800",
|
|
dashStyle: "Dash",
|
|
marker: {
|
|
enabled: false,
|
|
},
|
|
lineWidth: 2.5,
|
|
},
|
|
{
|
|
animation: false,
|
|
name: "Average",
|
|
data: forecastAvg,
|
|
color: $mode === "light" ? "#007050" : "#fff",
|
|
dashStyle: "Dash",
|
|
marker: {
|
|
enabled: false,
|
|
},
|
|
lineWidth: 2.5,
|
|
},
|
|
{
|
|
animation: false,
|
|
name: "Low",
|
|
data: forecastLow,
|
|
color: "#D9220E",
|
|
dashStyle: "Dash",
|
|
marker: {
|
|
enabled: false,
|
|
},
|
|
lineWidth: 2.5,
|
|
},
|
|
],
|
|
|
|
legend: {
|
|
enabled: false,
|
|
},
|
|
credits: {
|
|
enabled: false,
|
|
},
|
|
};
|
|
return options;
|
|
}
|
|
|
|
let config = null;
|
|
let configScore = null;
|
|
|
|
$: {
|
|
if ($stockTicker || $mode) {
|
|
prepareDataset();
|
|
configScore = getAIScorePlot() || null;
|
|
config = getPriceForecastChart() || null;
|
|
}
|
|
}
|
|
</script>
|
|
|
|
<SEO
|
|
title={`${$displayCompanyName} (${$stockTicker}) Stock Forecast & Analyst Ratings | AI Insights`}
|
|
description={`Discover our AI-powered forecast for ${$displayCompanyName} (${$stockTicker}). Get in-depth analyst ratings, price targets, upgrades, and downgrades from top Wall Street experts. Stay ahead of market trends and make smarter investment decisions.`}
|
|
/>
|
|
|
|
<section class="w-full overflow-hidden h-full">
|
|
<div class="w-full flex justify-center w-full sm-auto h-full overflow-hidden">
|
|
<div
|
|
class="w-full relative flex justify-center items-center overflow-hidden"
|
|
>
|
|
<main class="w-full">
|
|
<div class="sm:pl-7 sm:pb-7 sm:pt-7 m-auto mt-2 sm:mt-0">
|
|
{#if data?.getAIScore?.backtest?.length > 0}
|
|
<div class="">
|
|
<h1 class="text-xl sm:text-2xl font-bold">
|
|
{removeCompanyStrings($displayCompanyName)} AI Score Forecast
|
|
</h1>
|
|
</div>
|
|
<div class="w-full mb-10 mt-3">
|
|
<div
|
|
class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-4 mb-4 mt-3"
|
|
>
|
|
<div
|
|
class="shadow-md bg-gray-100 dark:bg-gray-800/30 rounded-lg p-4"
|
|
>
|
|
<div class="text-sm sm:text-[1rem] mb-2 flex items-center">
|
|
<span>Score Accuracy</span>
|
|
</div>
|
|
<div class="flex items-baseline">
|
|
<span class="text-xl font-bold"
|
|
>{data?.getAIScore?.accuracy
|
|
? data?.getAIScore?.accuracy + "%"
|
|
: "n/a"}</span
|
|
>
|
|
</div>
|
|
</div>
|
|
|
|
<div
|
|
class="shadow-md bg-gray-100 dark:bg-gray-800/30 rounded-lg p-4"
|
|
>
|
|
<div class="text-sm sm:text-[1rem] mb-2 flex items-center">
|
|
<span>Latest Forecast</span>
|
|
</div>
|
|
<div class="flex items-baseline">
|
|
<span class="text-xl font-bold">
|
|
{#if isPro}
|
|
{[10, 9, 8, 7]?.includes(data?.getAIScore?.score)
|
|
? "Bullish"
|
|
: [6, 5, 4]?.includes(data?.getAIScore?.score)
|
|
? "Hold"
|
|
: "Bearish"}
|
|
{:else}
|
|
<a
|
|
href="/pricing"
|
|
class="sm:hover:text-blue-700 dark:sm:hover:text-blue-400"
|
|
>Pro <svg
|
|
class="w-5 h-5 mb-1 inline-block"
|
|
xmlns="http://www.w3.org/2000/svg"
|
|
viewBox="0 0 24 24"
|
|
>
|
|
<path
|
|
fill="currentColor"
|
|
d="M17 9V7c0-2.8-2.2-5-5-5S7 4.2 7 7v2c-1.7 0-3 1.3-3 3v7c0 1.7 1.3 3 3 3h10c1.7 0 3-1.3 3-3v-7c0-1.7-1.3-3-3-3M9 7c0-1.7 1.3-3 3-3s3 1.3 3 3v2H9z"
|
|
/>
|
|
</svg></a
|
|
>
|
|
{/if}
|
|
</span>
|
|
</div>
|
|
</div>
|
|
|
|
<div
|
|
class="shadow-md bg-gray-100 dark:bg-gray-800/30 rounded-lg p-4"
|
|
>
|
|
<div class="text-sm sm:text-[1rem] mb-2 flex items-center">
|
|
<span>Avg Return</span>
|
|
</div>
|
|
<div class="flex items-baseline">
|
|
{#if isPro}
|
|
<span
|
|
class="text-xl font-bold {avgReturn >= 0
|
|
? "before:content-['+'] text-green-800 dark:text-[#00FC50]"
|
|
: 'text-red-800 dark:text-[#FF2F1F]'}"
|
|
>{avgReturn?.toFixed(2)}%</span
|
|
>
|
|
{:else}
|
|
<a
|
|
href="/pricing"
|
|
class="sm:hover:text-blue-700 dark:sm:hover:text-blue-400 text-xl font-bold"
|
|
>
|
|
Pro <svg
|
|
class="w-5 h-5 mb-1 inline-block"
|
|
xmlns="http://www.w3.org/2000/svg"
|
|
viewBox="0 0 24 24"
|
|
>
|
|
<path
|
|
fill="currentColor"
|
|
d="M17 9V7c0-2.8-2.2-5-5-5S7 4.2 7 7v2c-1.7 0-3 1.3-3 3v7c0 1.7 1.3 3 3 3h10c1.7 0 3-1.3 3-3v-7c0-1.7-1.3-3-3-3M9 7c0-1.7 1.3-3 3-3s3 1.3 3 3v2H9z"
|
|
/>
|
|
</svg></a
|
|
>
|
|
{/if}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div>
|
|
<div class="grow">
|
|
<div class="relative">
|
|
<!-- Apply the blur class to the chart -->
|
|
<div
|
|
class="{!isPro
|
|
? 'blur-[3px]'
|
|
: ''} mt-5 shadow-sm sm:mt-0 sm:border sm:border-gray-300 dark:border-gray-800 rounded"
|
|
use:highcharts={configScore}
|
|
></div>
|
|
<!-- Overlay with "Upgrade to Pro" -->
|
|
{#if !["Pro", "Plus"]?.includes(data?.user?.tier)}
|
|
<div
|
|
class="font-bold text-lg sm:text-xl absolute top-0 bottom-0 left-0 right-0 flex items-center justify-center text-muted dark:text-white"
|
|
>
|
|
<a
|
|
href="/pricing"
|
|
class="sm:hover:text-blue-700 dark:sm:hover:text-white dark:text-white flex flex-row items-center"
|
|
>
|
|
<span>Upgrade to Pro</span>
|
|
<svg
|
|
class="ml-1 w-5 h-5 sm:w-6 sm:h-6 inline-block"
|
|
xmlns="http://www.w3.org/2000/svg"
|
|
viewBox="0 0 24 24"
|
|
><path
|
|
fill="currentColor"
|
|
d="M17 9V7c0-2.8-2.2-5-5-5S7 4.2 7 7v2c-1.7 0-3 1.3-3 3v7c0 1.7 1.3 3 3 3h10c1.7 0 3-1.3 3-3v-7c0-1.7-1.3-3-3-3M9 7c0-1.7 1.3-3 3-3s3 1.3 3 3v2H9z"
|
|
/></svg
|
|
>
|
|
</a>
|
|
</div>
|
|
{/if}
|
|
</div>
|
|
|
|
<div
|
|
class="no-scrollbar mb-1 mt-2 overflow-x-auto px-1.5 text-center md:mb-0 md:px-0 lg:mt-5"
|
|
>
|
|
<table
|
|
class="table table-sm table-compact w-full text-right text-tiny xs:text-sm"
|
|
>
|
|
<thead>
|
|
<tr
|
|
class="border-b border-gray-300 dark:border-gray-600 font-normal text-sm sm:text-[1rem] whitespace-nowrap"
|
|
>
|
|
<th
|
|
class="py-[3px] text-left font-semibold lg:py-0.5 text-muted dark:text-white"
|
|
>
|
|
Date
|
|
</th>
|
|
{#each tableDates as item}
|
|
<th
|
|
class="py-[3px] text-left font-semibold lg:py-0.5 text-muted dark:text-white"
|
|
>
|
|
{item}
|
|
</th>
|
|
{/each}
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
<!-- Score Row -->
|
|
<tr
|
|
class="border-b border-gray-300 dark:border-gray-600 font-normal text-sm sm:text-[1rem] whitespace-nowrap"
|
|
>
|
|
<td class="py-[3px] text-left lg:py-0.5 text-[1rem]"
|
|
>Score</td
|
|
>
|
|
{#each tableScore as val, index}
|
|
<td
|
|
class="text-right whitespace-nowrap text-[1rem]"
|
|
>
|
|
{#if index < 5 || isPro}
|
|
{val}
|
|
{[10, 9, 8, 7].includes(Number(val))
|
|
? "(Bullish)"
|
|
: [6, 5, 4].includes(Number(val))
|
|
? "(Hold)"
|
|
: "(Sell)"}
|
|
{:else}
|
|
<a
|
|
href="/pricing"
|
|
class="sm:hover:text-blue-700 dark:sm:hover:text-blue-400"
|
|
>
|
|
Pro
|
|
<svg
|
|
class="w-4 h-4 mb-1 inline-block"
|
|
xmlns="http://www.w3.org/2000/svg"
|
|
viewBox="0 0 24 24"
|
|
>
|
|
<path
|
|
fill="currentColor"
|
|
d="M17 9V7c0-2.8-2.2-5-5-5S7 4.2 7 7v2c-1.7 0-3 1.3-3 3v7c0 1.7 1.3 3 3 3h10c1.7 0 3-1.3 3-3v-7c0-1.7-1.3-3-3-3M9 7c0-1.7 1.3-3 3-3s3 1.3 3 3v2H9z"
|
|
/>
|
|
</svg>
|
|
</a>
|
|
{/if}
|
|
</td>
|
|
{/each}
|
|
</tr>
|
|
|
|
<!-- QoQ Change Row -->
|
|
<tr
|
|
class="font-normal text-sm sm:text-[1rem] whitespace-nowrap"
|
|
>
|
|
<td class="py-[3px] text-left lg:py-0.5 text-[1rem]"
|
|
>QoQ Change</td
|
|
>
|
|
{#each tableQuarterChange as item, index}
|
|
<td class="text-[1rem]">
|
|
{#if index < 5 || isPro}
|
|
<span
|
|
class={item?.change > 0
|
|
? "before:content-['+'] text-green-800 dark:text-[#00FC50]"
|
|
: "text-red-800 dark:text-[#FF2F1F]"}
|
|
>
|
|
{item?.change}%
|
|
</span>
|
|
{:else}
|
|
<a
|
|
href="/pricing"
|
|
class="sm:hover:text-blue-700 dark:sm:hover:text-blue-400"
|
|
>
|
|
Pro
|
|
<svg
|
|
class="w-4 h-4 mb-1 inline-block"
|
|
xmlns="http://www.w3.org/2000/svg"
|
|
viewBox="0 0 24 24"
|
|
>
|
|
<path
|
|
fill="currentColor"
|
|
d="M17 9V7c0-2.8-2.2-5-5-5S7 4.2 7 7v2c-1.7 0-3 1.3-3 3v7c0 1.7 1.3 3 3 3h10c1.7 0 3-1.3 3-3v-7c0-1.7-1.3-3-3-3M9 7c0-1.7 1.3-3 3-3s3 1.3 3 3v2H9z"
|
|
/>
|
|
</svg>
|
|
</a>
|
|
{/if}
|
|
</td>
|
|
{/each}
|
|
</tr>
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
|
|
<p class="mt-4">
|
|
Following the AI Score for {removeCompanyStrings(
|
|
$displayCompanyName,
|
|
)} the model shows that the average return would be
|
|
<span
|
|
class="font-semibold {avgReturn >= 0
|
|
? "before:content-['+'] text-green-800 dark:text-[#00FC50]"
|
|
: 'text-red-800 dark:text-[#FF2F1F]'}"
|
|
>{avgReturn?.toFixed(2)}%</span
|
|
> based on the backtesting results.
|
|
</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
{/if}
|
|
|
|
{#if Object?.keys(data?.getPriceAnalysis)?.length > 0}
|
|
<div class="">
|
|
<h1 class="text-xl sm:text-2xl font-bold">
|
|
{removeCompanyStrings($displayCompanyName)} Trend Forecast
|
|
</h1>
|
|
</div>
|
|
<div class="w-full mb-6 mt-3">
|
|
{#if !["Pro", "Plus"]?.includes(data?.user?.tier)}
|
|
<Infobox
|
|
text={`Using our AI model trained on historical seasonal data, we generated a 12-month forecast for ${removeCompanyStrings($displayCompanyName)}. The model predicts a ... Unlock content with
|
|
<a
|
|
class="inline-block ml-0.5 text-blue-700 sm:hover:text-muted dark:text-blue-400 dark:sm:hover:text-white"
|
|
href="/pricing"
|
|
>Pro Subscription <svg
|
|
class="w-4 h-4 mb-1 inline-block"
|
|
xmlns="http://www.w3.org/2000/svg"
|
|
viewBox="0 0 24 24"
|
|
><path
|
|
fill="currentColor"
|
|
d="M17 9V7c0-2.8-2.2-5-5-5S7 4.2 7 7v2c-1.7 0-3 1.3-3 3v7c0 1.7 1.3 3 3 3h10c1.7 0 3-1.3 3-3v-7c0-1.7-1.3-3-3-3M9 7c0-1.7 1.3-3 3-3s3 1.3 3 3v2H9z"
|
|
/></svg
|
|
></a
|
|
>`}
|
|
/>
|
|
{:else}
|
|
<Infobox
|
|
text={`Using our AI model trained on historical seasonal data, we generated a 12-month forecast for ${removeCompanyStrings($displayCompanyName)}. The model predicts a median target price of ${medianPriceTarget}, ranging from ${lowPriceTarget} to ${highPriceTarget}, indicating a ${medianChange > 0 ? "potential increase" : "potential decrease"} of ${medianChange}% from the current price of ${price}.`}
|
|
/>
|
|
{/if}
|
|
|
|
<div>
|
|
<div class="grow pt-5">
|
|
<div class="relative">
|
|
<!-- Apply the blur class to the chart -->
|
|
<div
|
|
class="{!isPro
|
|
? 'blur-[3px]'
|
|
: ''} mt-5 shadow-sm sm:mt-0 sm:border sm:border-gray-300 dark:border-gray-800 rounded"
|
|
use:highcharts={config}
|
|
></div>
|
|
<!-- Overlay with "Upgrade to Pro" -->
|
|
{#if !["Pro", "Plus"]?.includes(data?.user?.tier)}
|
|
<div
|
|
class="font-bold text-lg sm:text-xl absolute top-0 bottom-0 left-0 right-0 flex items-center justify-center text-muted dark:text-white"
|
|
>
|
|
<a
|
|
href="/pricing"
|
|
class="sm:hover:text-blue-700 dark:sm:hover:text-white dark:text-white flex flex-row items-center"
|
|
>
|
|
<span>Upgrade to Pro</span>
|
|
<svg
|
|
class="ml-1 w-5 h-5 sm:w-6 sm:h-6 inline-block"
|
|
xmlns="http://www.w3.org/2000/svg"
|
|
viewBox="0 0 24 24"
|
|
><path
|
|
fill="currentColor"
|
|
d="M17 9V7c0-2.8-2.2-5-5-5S7 4.2 7 7v2c-1.7 0-3 1.3-3 3v7c0 1.7 1.3 3 3 3h10c1.7 0 3-1.3 3-3v-7c0-1.7-1.3-3-3-3M9 7c0-1.7 1.3-3 3-3s3 1.3 3 3v2H9z"
|
|
/></svg
|
|
>
|
|
</a>
|
|
</div>
|
|
{/if}
|
|
</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-5"
|
|
>
|
|
<table class="w-full text-right text-tiny xs:text-sm">
|
|
<thead
|
|
><tr
|
|
class="border-b border-gray-300 dark:border-gray-600 font-normal text-sm sm:text-[1rem]"
|
|
><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-300 dark:border-gray-600 font-normal text-sm sm:text-[1rem]"
|
|
>
|
|
<td class="py-[3px] text-left lg:py-0.5">Price</td>
|
|
{#if !["Pro", "Plus"]?.includes(data?.user?.tier)}
|
|
<td class="whitespace-nowrap">
|
|
<a
|
|
href="/pricing"
|
|
class="sm:hover:text-blue-700 dark:sm:hover:text-blue-400"
|
|
>Pro <svg
|
|
class="w-4 h-4 mb-1 inline-block"
|
|
xmlns="http://www.w3.org/2000/svg"
|
|
viewBox="0 0 24 24"
|
|
>
|
|
<path
|
|
fill="currentColor"
|
|
d="M17 9V7c0-2.8-2.2-5-5-5S7 4.2 7 7v2c-1.7 0-3 1.3-3 3v7c0 1.7 1.3 3 3 3h10c1.7 0 3-1.3 3-3v-7c0-1.7-1.3-3-3-3M9 7c0-1.7 1.3-3 3-3s3 1.3 3 3v2H9z"
|
|
/>
|
|
</svg></a
|
|
>
|
|
</td>
|
|
<td class="whitespace-nowrap">
|
|
<a
|
|
href="/pricing"
|
|
class="sm:hover:text-blue-700 dark:sm:hover:text-blue-400"
|
|
>Pro <svg
|
|
class="w-4 h-4 mb-1 inline-block"
|
|
xmlns="http://www.w3.org/2000/svg"
|
|
viewBox="0 0 24 24"
|
|
>
|
|
<path
|
|
fill="currentColor"
|
|
d="M17 9V7c0-2.8-2.2-5-5-5S7 4.2 7 7v2c-1.7 0-3 1.3-3 3v7c0 1.7 1.3 3 3 3h10c1.7 0 3-1.3 3-3v-7c0-1.7-1.3-3-3-3M9 7c0-1.7 1.3-3 3-3s3 1.3 3 3v2H9z"
|
|
/>
|
|
</svg></a
|
|
>
|
|
</td>
|
|
<td class="whitespace-nowrap">
|
|
<a
|
|
href="/pricing"
|
|
class="sm:hover:text-blue-700 dark:sm:hover:text-blue-400"
|
|
>Pro <svg
|
|
class="w-4 h-4 mb-1 inline-block"
|
|
xmlns="http://www.w3.org/2000/svg"
|
|
viewBox="0 0 24 24"
|
|
>
|
|
<path
|
|
fill="currentColor"
|
|
d="M17 9V7c0-2.8-2.2-5-5-5S7 4.2 7 7v2c-1.7 0-3 1.3-3 3v7c0 1.7 1.3 3 3 3h10c1.7 0 3-1.3 3-3v-7c0-1.7-1.3-3-3-3M9 7c0-1.7 1.3-3 3-3s3 1.3 3 3v2H9z"
|
|
/>
|
|
</svg></a
|
|
>
|
|
</td>
|
|
<td class="whitespace-nowrap">
|
|
<a
|
|
href="/pricing"
|
|
class="sm:hover:text-blue-700 dark:sm:hover:text-blue-400"
|
|
>Pro <svg
|
|
class="w-4 h-4 mb-1 inline-block"
|
|
xmlns="http://www.w3.org/2000/svg"
|
|
viewBox="0 0 24 24"
|
|
>
|
|
<path
|
|
fill="currentColor"
|
|
d="M17 9V7c0-2.8-2.2-5-5-5S7 4.2 7 7v2c-1.7 0-3 1.3-3 3v7c0 1.7 1.3 3 3 3h10c1.7 0 3-1.3 3-3v-7c0-1.7-1.3-3-3-3M9 7c0-1.7 1.3-3 3-3s3 1.3 3 3v2H9z"
|
|
/>
|
|
</svg></a
|
|
>
|
|
</td>
|
|
{:else}
|
|
<td>${lowPriceTarget}</td>
|
|
<td>${avgPriceTarget}</td>
|
|
<td>${medianPriceTarget}</td>
|
|
<td>${highPriceTarget}</td>
|
|
{/if}
|
|
</tr>
|
|
|
|
<tr class="text-sm sm:text-[1rem]">
|
|
<td class="py-[3px] text-left lg:py-0.5">Change</td>
|
|
{#if !["Pro", "Plus"]?.includes(data?.user?.tier)}
|
|
<td class="whitespace-nowrap">
|
|
<a
|
|
href="/pricing"
|
|
class="sm:hover:text-blue-700 dark:sm:hover:text-blue-400"
|
|
>
|
|
Pro
|
|
<svg
|
|
class="w-4 h-4 mb-1 inline-block"
|
|
xmlns="http://www.w3.org/2000/svg"
|
|
viewBox="0 0 24 24"
|
|
>
|
|
<path
|
|
fill="currentColor"
|
|
d="M17 9V7c0-2.8-2.2-5-5-5S7 4.2 7 7v2c-1.7 0-3 1.3-3 3v7c0 1.7 1.3 3 3 3h10c1.7 0 3-1.3 3-3v-7c0-1.7-1.3-3-3-3M9 7c0-1.7 1.3-3 3-3s3 1.3 3 3v2H9z"
|
|
/>
|
|
</svg>
|
|
</a>
|
|
</td>
|
|
<td class="whitespace-nowrap"
|
|
><a
|
|
href="/pricing"
|
|
class="sm:hover:text-blue-700 dark:sm:hover:text-blue-400"
|
|
>Pro <svg
|
|
class="w-4 h-4 mb-1 inline-block"
|
|
xmlns="http://www.w3.org/2000/svg"
|
|
viewBox="0 0 24 24"
|
|
>
|
|
<path
|
|
fill="currentColor"
|
|
d="M17 9V7c0-2.8-2.2-5-5-5S7 4.2 7 7v2c-1.7 0-3 1.3-3 3v7c0 1.7 1.3 3 3 3h10c1.7 0 3-1.3 3-3v-7c0-1.7-1.3-3-3-3M9 7c0-1.7 1.3-3 3-3s3 1.3 3 3v2H9z"
|
|
/>
|
|
</svg></a
|
|
></td
|
|
>
|
|
<td class="whitespace-nowrap"
|
|
><a
|
|
href="/pricing"
|
|
class="sm:hover:text-blue-700 dark:sm:hover:text-blue-400"
|
|
>Pro <svg
|
|
class="w-4 h-4 mb-1 inline-block"
|
|
xmlns="http://www.w3.org/2000/svg"
|
|
viewBox="0 0 24 24"
|
|
>
|
|
<path
|
|
fill="currentColor"
|
|
d="M17 9V7c0-2.8-2.2-5-5-5S7 4.2 7 7v2c-1.7 0-3 1.3-3 3v7c0 1.7 1.3 3 3 3h10c1.7 0 3-1.3 3-3v-7c0-1.7-1.3-3-3-3M9 7c0-1.7 1.3-3 3-3s3 1.3 3 3v2H9z"
|
|
/>
|
|
</svg></a
|
|
></td
|
|
>
|
|
<td class="whitespace-nowrap"
|
|
><a
|
|
href="/pricing"
|
|
class="sm:hover:text-blue-700 dark:sm:hover:text-blue-400"
|
|
>Pro <svg
|
|
class="w-4 h-4 mb-1 inline-block"
|
|
xmlns="http://www.w3.org/2000/svg"
|
|
viewBox="0 0 24 24"
|
|
>
|
|
<path
|
|
fill="currentColor"
|
|
d="M17 9V7c0-2.8-2.2-5-5-5S7 4.2 7 7v2c-1.7 0-3 1.3-3 3v7c0 1.7 1.3 3 3 3h10c1.7 0 3-1.3 3-3v-7c0-1.7-1.3-3-3-3M9 7c0-1.7 1.3-3 3-3s3 1.3 3 3v2H9z"
|
|
/>
|
|
</svg></a
|
|
></td
|
|
>
|
|
{:else}
|
|
<td
|
|
class={lowChange > 0
|
|
? "before:content-['+'] text-green-800 dark:text-[#00FC50]"
|
|
: "text-red-800 dark:text-[#FF2F1F]"}
|
|
>{lowChange}%</td
|
|
>
|
|
<td
|
|
class={avgChange > 0
|
|
? "before:content-['+'] text-green-800 dark:text-[#00FC50]"
|
|
: "text-red-800 dark:text-[#FF2F1F]"}
|
|
>{avgChange}%</td
|
|
>
|
|
<td
|
|
class={medianChange > 0
|
|
? "before:content-['+'] text-green-800 dark:text-[#00FC50]"
|
|
: "text-red-800 dark:text-[#FF2F1F]"}
|
|
>{medianChange}%</td
|
|
>
|
|
<td
|
|
class={highChange > 0
|
|
? "before:content-['+'] text-green-800 dark:text-[#00FC50]"
|
|
: "text-red-800 dark:text-[#FF2F1F]"}
|
|
>{highChange}%</td
|
|
>
|
|
{/if}
|
|
</tr>
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
{:else}
|
|
<Infobox text="No AI Price Forecast available right now" />
|
|
{/if}
|
|
</div>
|
|
</main>
|
|
</div>
|
|
</div>
|
|
</section>
|