From 57fa0224bf0329e13dbb6b47d7a63700092160cb Mon Sep 17 00:00:00 2001 From: MuslemRahimi Date: Thu, 13 Mar 2025 02:23:15 +0100 Subject: [PATCH] ui fix --- src/routes/pricing/+page.svelte | 2 +- .../[tickerID]/forecast/ai/+layout.server.ts | 67 ++++ .../[tickerID]/forecast/ai/+layout.svelte | 58 +-- .../[tickerID]/forecast/ai/+page.server.ts | 201 ---------- .../[tickerID]/forecast/ai/+page.svelte | 356 ++++++++++-------- 5 files changed, 307 insertions(+), 377 deletions(-) create mode 100644 src/routes/stocks/[tickerID]/forecast/ai/+layout.server.ts delete mode 100644 src/routes/stocks/[tickerID]/forecast/ai/+page.server.ts diff --git a/src/routes/pricing/+page.svelte b/src/routes/pricing/+page.svelte index f00894ac..2fe0fdef 100644 --- a/src/routes/pricing/+page.svelte +++ b/src/routes/pricing/+page.svelte @@ -1120,7 +1120,7 @@ - + Full Market Access ❌ diff --git a/src/routes/stocks/[tickerID]/forecast/ai/+layout.server.ts b/src/routes/stocks/[tickerID]/forecast/ai/+layout.server.ts new file mode 100644 index 00000000..18bf8ee8 --- /dev/null +++ b/src/routes/stocks/[tickerID]/forecast/ai/+layout.server.ts @@ -0,0 +1,67 @@ +import { error, fail, redirect } from "@sveltejs/kit"; +import { validateData } from "$lib/utils"; +import { loginUserSchema, registerUserSchema } from "$lib/schemas"; + +export const load = async ({ locals, params }) => { + const { apiURL, apiKey } = locals; + const postData = { + ticker: params.tickerID, + }; + + + const getPriceAnalysis = async () => { + const response = await fetch(apiURL + "/price-analysis", { + method: "POST", + headers: { + "Content-Type": "application/json", + "X-API-KEY": apiKey, + }, + body: JSON.stringify(postData), + }); + + const output = await response.json(); + return output; + }; + + const getAIScore = async () => { + const response = await fetch(apiURL + "/ai-score", { + method: "POST", + headers: { + "Content-Type": "application/json", + "X-API-KEY": apiKey, + }, + body: JSON.stringify(postData), + }); + + const output = await response.json(); + return output; + }; + + const getHistoricalPrice = async () => { + const postData = { ticker: params.tickerID, timePeriod: "max" }; + const response = await fetch(apiURL + "/historical-price", { + method: "POST", + headers: { + "Content-Type": "application/json", + "X-API-KEY": apiKey, + }, + body: JSON.stringify(postData), + }); + + const output = await response.json(); + + return output; + }; + + + // Make sure to return a promise + return { + getPriceAnalysis: await getPriceAnalysis(), + getHistoricalPrice: await getHistoricalPrice(), + getAIScore: await getAIScore(), + }; +}; + + + + diff --git a/src/routes/stocks/[tickerID]/forecast/ai/+layout.svelte b/src/routes/stocks/[tickerID]/forecast/ai/+layout.svelte index 350c5487..73541414 100644 --- a/src/routes/stocks/[tickerID]/forecast/ai/+layout.svelte +++ b/src/routes/stocks/[tickerID]/forecast/ai/+layout.svelte @@ -33,17 +33,20 @@ {/if} -
-

AI Score Definition

-
- Revenue, also called sales, is the amount of money a company - receives from its business activities, such as sales of products - or services. Revenue does not take any expenses into account and - is therefore different from profits. -
- -
+ + {/if} -
-

- AI Trend Forecast Definition -

-
- Revenue, also called sales, is the amount of money a company - receives from its business activities, such as sales of products - or services. Revenue does not take any expenses into account and - is therefore different from profits. -
- -
+ + {/if} diff --git a/src/routes/stocks/[tickerID]/forecast/ai/+page.server.ts b/src/routes/stocks/[tickerID]/forecast/ai/+page.server.ts deleted file mode 100644 index 1c538d39..00000000 --- a/src/routes/stocks/[tickerID]/forecast/ai/+page.server.ts +++ /dev/null @@ -1,201 +0,0 @@ -import { error, fail, redirect } from "@sveltejs/kit"; -import { validateData } from "$lib/utils"; -import { loginUserSchema, registerUserSchema } from "$lib/schemas"; - -export const load = async ({ locals, params }) => { - const { apiURL, apiKey } = locals; - const postData = { - ticker: params.tickerID, - }; - - - const getPriceAnalysis = async () => { - const response = await fetch(apiURL + "/price-analysis", { - method: "POST", - headers: { - "Content-Type": "application/json", - "X-API-KEY": apiKey, - }, - body: JSON.stringify(postData), - }); - - const output = await response.json(); - return output; - }; - - const getHistoricalPrice = async () => { - const postData = { ticker: params.tickerID, timePeriod: "max" }; - const response = await fetch(apiURL + "/historical-price", { - method: "POST", - headers: { - "Content-Type": "application/json", - "X-API-KEY": apiKey, - }, - body: JSON.stringify(postData), - }); - - const output = await response.json(); - - return output; - }; - - - // Make sure to return a promise - return { - getPriceAnalysis: await getPriceAnalysis(), - getHistoricalPrice: await getHistoricalPrice(), - }; -}; - - - - -export const actions = { - login: async ({ url, request, locals }) => { - - const path = url?.href?.replace("/oauth2","") - - const { formData, errors } = await validateData( - await request.formData(), - loginUserSchema, - ); - - if (errors) { - return fail(400, { - data: formData, - errors: errors.fieldErrors, - }); - } - - try { - await locals.pb - .collection("users") - .authWithPassword(formData.email, formData.password); - - /* - if (!locals.pb?.authStore?.model?.verified) { - locals.pb.authStore.clear(); - return { - notVerified: true, - }; - } - */ - } catch (err) { - console.log("Error: ", err); - error(err.status, err.message); - } - - redirect(302, path); - }, - - register: async ({ url, locals, request }) => { - const path = url?.href?.replace("/oauth2","") - - const { formData, errors } = await validateData( - await request.formData(), - registerUserSchema, - ); - - if (errors) { - return fail(400, { - data: formData, - errors: errors.fieldErrors, - }); - } - - try { - let newUser = await locals.pb.collection("users").create(formData); - /* -await locals.pb?.collection('users').update( - newUser?.id, { - 'freeTrial' : true, - 'tier': 'Pro', //Give new users a free trial for the Pro Subscription - }); -*/ - await locals.pb.collection("users")?.requestVerification(formData.email); - } catch (err) { - console.log("Error: ", err); - error(err.status, err.message); - } - - try { - await locals.pb - .collection("users") - .authWithPassword(formData.email, formData.password); - } catch (err) { - console.log("Error: ", err); - error(err.status, err.message); - } - - redirect(303, path); - }, - - oauth2: async ({ url, locals, request, cookies }) => { - - const path = url?.href?.replace("/oauth2","") - const authMethods = (await locals?.pb - ?.collection("users") - ?.listAuthMethods())?.oauth2; - - - const data = await request?.formData(); - const providerSelected = data?.get("provider"); - - if (!authMethods) { - return { - authProviderRedirect: "", - authProviderState: "", - }; - } - const redirectURL = `${url.origin}/oauth`; - - const targetItem = authMethods?.providers?.findIndex( - (item) => item?.name === providerSelected, - ); - //console.log("==================") - //console.log(authMethods.authProviders) - //console.log('target item is: ', targetItem) - - const provider = authMethods.providers[targetItem]; - const authProviderRedirect = `${provider.authUrl}${redirectURL}`; - const state = provider.state; - const verifier = provider.codeVerifier; - - - - cookies.set("state", state, { - httpOnly: true, - sameSite: "lax", - secure: true, - path: "/", - maxAge: 60 * 60, - }); - - cookies.set("verifier", verifier, { - httpOnly: true, - sameSite: "lax", - secure: true, - path: "/", - maxAge: 60 * 60, - }); - - cookies.set("provider", providerSelected, { - httpOnly: true, - sameSite: "lax", - secure: true, - path: "/", - maxAge: 60 * 60, - }); - - cookies.set("path", path, { - httpOnly: true, - sameSite: "lax", - secure: true, - path: "/", - maxAge: 60, - }); - - redirect(302, authProviderRedirect); - }, - -}; \ No newline at end of file diff --git a/src/routes/stocks/[tickerID]/forecast/ai/+page.svelte b/src/routes/stocks/[tickerID]/forecast/ai/+page.svelte index a8c7e8df..9521edde 100644 --- a/src/routes/stocks/[tickerID]/forecast/ai/+page.svelte +++ b/src/routes/stocks/[tickerID]/forecast/ai/+page.svelte @@ -9,6 +9,13 @@ export let data; + 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 price = data?.getStockQuote?.price?.toFixed(2) || 0; const calculatePriceChange = (targetPrice) => @@ -24,104 +31,115 @@ const avgChange = calculatePriceChange(avgPriceTarget); const highChange = calculatePriceChange(highPriceTarget); - let consensusRating; + // Assume data.getHistoricalPrice contains objects with a "time" field (e.g. "2015-01-02") + const historicalData = data?.getHistoricalPrice || []; - if (avgChange < -20) { - consensusRating = "Strong Sell"; - } else if (avgChange < -10) { - consensusRating = "Sell"; - } else if (avgChange <= 15) { - consensusRating = "Hold"; - } else if (avgChange >= 35) { - consensusRating = "Strong Buy"; - } else if (avgChange >= 20) { - consensusRating = "Buy"; - } else { - consensusRating = "Hold"; + const 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 }); } - function getAIScorePlot() { - // Assume data.getHistoricalPrice contains objects with a "time" field (e.g. "2015-01-02") - const historicalData = data?.getHistoricalPrice || []; + // 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. + const processedData = backtestList?.map((item) => { + const dateStr = item.date; + const targetTime = new Date(dateStr).getTime(); + let closestPoint = historicalData[0]; + let minDiff = Infinity; - // Pre-defined backtest dates and scores - const backtestList = [ - { date: "2023-03-31", yTest: 1, yPred: 1, score: 9 }, - { date: "2023-06-30", yTest: 0, yPred: 1, score: 9 }, - { date: "2023-09-30", yTest: 0, yPred: 1, score: 9 }, - { date: "2023-12-31", yTest: 0, yPred: 1, score: 7 }, - { date: "2024-03-31", yTest: 1, yPred: 1, score: 9 }, - { date: "2024-06-30", yTest: 1, yPred: 1, score: 9 }, - { date: "2024-09-30", yTest: 1, yPred: 1, score: 9 }, - { date: "2024-12-31", yTest: 0, yPred: 1, score: 9 }, - ]; - - // 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 }); - } - - // 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. - const 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 = "#2ecc71"; - markerSymbol = "triangle-up"; - } else if (item.score < 5) { - // Bearish: red marker with a downward triangle - markerColor = "#e74c3c"; - markerSymbol = "triangle-down"; - } else { - // Neutral (score exactly 5): yellow marker with a circle - markerColor = "#f1c40f"; - 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, - }; + historicalData.forEach((point) => { + const pointTime = new Date(point.time).getTime(); + const diff = Math.abs(pointTime - targetTime); + if (diff < minDiff) { + minDiff = diff; + closestPoint = point; } - - return dataPoint; }); + // 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 = "#2ecc71"; + markerSymbol = "triangle-up"; + } else if (item.score < 5) { + // Bearish: red marker with a downward triangle + markerColor = "#e74c3c"; + markerSymbol = "triangle-down"; + } else { + // Neutral (score exactly 5): yellow marker with a circle + markerColor = "#f1c40f"; + 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; + }); + + const tableDates = processedData + ?.slice(0, -1) + ?.map((item) => formatDateToQuarter(item.x)); + + const tableScore = processedData + ?.slice(0, -1) + ?.map((item) => item?.dataLabels?.format); + + // Compute percentage change + const 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 + const 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 + + const avgReturn = + returns?.reduce((sum, returnPercentage) => sum + returnPercentage, 0) / + returns?.length; + + function getAIScorePlot() { const solidData = processedData.slice(0, -1); const lastTwoPoints = processedData.slice(-2); // Extract the last two points @@ -423,19 +441,61 @@ >
- {#if Object?.keys(data?.getPriceAnalysis)?.length > 0} + {#if data?.getAIScore?.backtest?.length > 0}

- {removeCompanyStrings($displayCompanyName)} AI Score + {removeCompanyStrings($displayCompanyName)} AI Score Forecast

- +
+
+
+ Score Accuracy +
+
+ {data?.getAIScore?.accuracy + ? data?.getAIScore?.accuracy + "%" + : "n/a"} +
+
+ +
+
+ Latest Forecast +
+
+ Bullish +
+
+ +
+
+ Avg Return +
+
+ {avgReturn?.toFixed(2)}% +
+
+
-
+
- +
- - - + {#each tableDates as item} + + {/each} + - - - - - + {#each tableScore as val} + + {/each} + + - - - - {item?.change}% + {/each} +
Date Q1 '23Q2 '23Q3 '23Q4 '23
{item}
Score8 (Bullish)9 (Bullish)8 (Bullish)7 (Hold)
Score + {val} + {[10, 9, 8, 7]?.includes(Number(val)) + ? "(Bullish)" + : [6, 5, 4]?.includes(Number(val)) + ? "(Hold)" + : "(Sell)"} +
QoQ Change 0 - ? "before:content-['+'] text-green-600 dark:text-[#00FC50]" - : "text-red-600 dark:text-[#FF2F1F]"} - >{lowChange}% 0 - ? "before:content-['+'] text-green-600 dark:text-[#00FC50]" - : "text-red-600 dark:text-[#FF2F1F]"} - >{avgChange}% 0 - ? "before:content-['+'] text-green-600 dark:text-[#00FC50]" - : "text-red-600 dark:text-[#FF2F1F]"} - >{medianChange}% 0 - ? "before:content-['+'] text-green-600 dark:text-[#00FC50]" - : "text-red-600 dark:text-[#FF2F1F]"} - >{highChange}%
@@ -502,9 +563,13 @@

Following the AI Score for {removeCompanyStrings( $displayCompanyName, - )} the model shows that the total return would be - +22.2%, with a maximum drawdown of - -12% based on the backtesting results. + )} the model shows that the average return would be + {avgReturn?.toFixed(2)}% based on the backtesting results.

@@ -519,14 +584,7 @@
0 ? "an increase" : "a decrease" - } of ${medianChange}% compared to the current price - of ${price}.`} + 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}.`} />