adding market heatmap
This commit is contained in:
parent
126c40f806
commit
902b734302
45
package-lock.json
generated
45
package-lock.json
generated
@ -40,6 +40,7 @@
|
|||||||
"file-saver": "^2.0.5",
|
"file-saver": "^2.0.5",
|
||||||
"flowbite-svelte": "^0.46.15",
|
"flowbite-svelte": "^0.46.15",
|
||||||
"got": "^14.4.2",
|
"got": "^14.4.2",
|
||||||
|
"html2canvas": "^1.4.1",
|
||||||
"jsonwebtoken": "^9.0.2",
|
"jsonwebtoken": "^9.0.2",
|
||||||
"katex": "^0.16.11",
|
"katex": "^0.16.11",
|
||||||
"layercake": "^8.4.0-beta.1",
|
"layercake": "^8.4.0-beta.1",
|
||||||
@ -3732,6 +3733,16 @@
|
|||||||
"integrity": "sha512-X1xgQhkZ9n94WDwntqst5D/FKkmiU0GlJSFZSV3kLvyJ1WC5VeyoXDOuleUD+SIuH9C7W05is++0Woh0CGfKjQ==",
|
"integrity": "sha512-X1xgQhkZ9n94WDwntqst5D/FKkmiU0GlJSFZSV3kLvyJ1WC5VeyoXDOuleUD+SIuH9C7W05is++0Woh0CGfKjQ==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/css-line-break": {
|
||||||
|
"version": "2.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/css-line-break/-/css-line-break-2.1.0.tgz",
|
||||||
|
"integrity": "sha512-FHcKFCZcAha3LwfVBhCQbW2nCNbkZXn7KVUJcsT5/P8YmfsVja0FMPJr0B903j/E69HUphKiV9iQArX8SDYA4w==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"utrie": "^1.0.2"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/css-loader": {
|
"node_modules/css-loader": {
|
||||||
"version": "7.1.2",
|
"version": "7.1.2",
|
||||||
"resolved": "https://registry.npmjs.org/css-loader/-/css-loader-7.1.2.tgz",
|
"resolved": "https://registry.npmjs.org/css-loader/-/css-loader-7.1.2.tgz",
|
||||||
@ -5580,6 +5591,20 @@
|
|||||||
"integrity": "sha512-08iL2VyCRbkQKBySkSh6m8zMUa3sADAxGVWs3Z1aPcUkTJeK0ETG4Fc27tEmQBGUAXZjIsXOZqBvacuVNSC/fQ==",
|
"integrity": "sha512-08iL2VyCRbkQKBySkSh6m8zMUa3sADAxGVWs3Z1aPcUkTJeK0ETG4Fc27tEmQBGUAXZjIsXOZqBvacuVNSC/fQ==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/html2canvas": {
|
||||||
|
"version": "1.4.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/html2canvas/-/html2canvas-1.4.1.tgz",
|
||||||
|
"integrity": "sha512-fPU6BHNpsyIhr8yyMpTLLxAbkaK8ArIBcmZIRiBLiDhjeqvXolaEmDGmELFuX9I4xDcaKKcJl+TKZLqruBbmWA==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"css-line-break": "^2.1.0",
|
||||||
|
"text-segmentation": "^1.0.3"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=8.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/http-cache-semantics": {
|
"node_modules/http-cache-semantics": {
|
||||||
"version": "4.1.1",
|
"version": "4.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.1.1.tgz",
|
||||||
@ -9307,6 +9332,16 @@
|
|||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true
|
"peer": true
|
||||||
},
|
},
|
||||||
|
"node_modules/text-segmentation": {
|
||||||
|
"version": "1.0.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/text-segmentation/-/text-segmentation-1.0.3.tgz",
|
||||||
|
"integrity": "sha512-iOiPUo/BGnZ6+54OsWxZidGCsdU8YbE4PSpdPinp7DeMtUJNJBoJ/ouUSTJjHkh1KntHaltHl/gDs2FC4i5+Nw==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"utrie": "^1.0.2"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/thenify": {
|
"node_modules/thenify": {
|
||||||
"version": "3.3.1",
|
"version": "3.3.1",
|
||||||
"resolved": "https://registry.npmjs.org/thenify/-/thenify-3.3.1.tgz",
|
"resolved": "https://registry.npmjs.org/thenify/-/thenify-3.3.1.tgz",
|
||||||
@ -9628,6 +9663,16 @@
|
|||||||
"integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==",
|
"integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/utrie": {
|
||||||
|
"version": "1.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/utrie/-/utrie-1.0.2.tgz",
|
||||||
|
"integrity": "sha512-1MLa5ouZiOmQzUbjbu9VmjLzn1QLXBhwpUa7kdLUQK+KQ5KA9I1vk5U4YHe/X2Ch7PYnJfWuWT+VbuxbGwljhw==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"base64-arraybuffer": "^1.0.2"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/uuid": {
|
"node_modules/uuid": {
|
||||||
"version": "10.0.0",
|
"version": "10.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/uuid/-/uuid-10.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/uuid/-/uuid-10.0.0.tgz",
|
||||||
|
|||||||
@ -42,6 +42,7 @@
|
|||||||
"file-saver": "^2.0.5",
|
"file-saver": "^2.0.5",
|
||||||
"flowbite-svelte": "^0.46.15",
|
"flowbite-svelte": "^0.46.15",
|
||||||
"got": "^14.4.2",
|
"got": "^14.4.2",
|
||||||
|
"html2canvas": "^1.4.1",
|
||||||
"jsonwebtoken": "^9.0.2",
|
"jsonwebtoken": "^9.0.2",
|
||||||
"katex": "^0.16.11",
|
"katex": "^0.16.11",
|
||||||
"layercake": "^8.4.0-beta.1",
|
"layercake": "^8.4.0-beta.1",
|
||||||
|
|||||||
@ -409,19 +409,19 @@
|
|||||||
>Market Mover</a
|
>Market Mover</a
|
||||||
>
|
>
|
||||||
</Button>
|
</Button>
|
||||||
<!--
|
|
||||||
<Button
|
<Button
|
||||||
builders={[builder]}
|
builders={[builder]}
|
||||||
type="submit"
|
type="submit"
|
||||||
class="w-full bg-[#141417] hover:bg-[#141417]"
|
class="w-full bg-[#141417] hover:bg-[#141417]"
|
||||||
>
|
>
|
||||||
<a
|
<a
|
||||||
href="/heatmaps"
|
href="/heatmap"
|
||||||
class="text-start w-full text-[1rem] text-white ml-4 mt-4"
|
class="text-start w-full text-[1rem] text-white ml-4 mt-4"
|
||||||
>Heatmaps</a
|
>Market Heatmap</a
|
||||||
>
|
>
|
||||||
</Button>
|
</Button>
|
||||||
-->
|
|
||||||
<Button
|
<Button
|
||||||
builders={[builder]}
|
builders={[builder]}
|
||||||
type="submit"
|
type="submit"
|
||||||
@ -969,12 +969,13 @@
|
|||||||
class="text-[1rem] text-white ml-4 mt-4"
|
class="text-[1rem] text-white ml-4 mt-4"
|
||||||
>Market Mover</a
|
>Market Mover</a
|
||||||
>
|
>
|
||||||
<!--
|
|
||||||
<a
|
<a
|
||||||
href="/heatmaps"
|
href="/heatmap"
|
||||||
class="text-[1rem] text-white ml-4 mt-4">Heatmaps</a
|
class="text-[1rem] text-white ml-4 mt-4"
|
||||||
|
>Market Heatmap</a
|
||||||
>
|
>
|
||||||
-->
|
|
||||||
<a
|
<a
|
||||||
href="/list"
|
href="/list"
|
||||||
class="text-[1rem] text-white ml-4 mt-4"
|
class="text-[1rem] text-white ml-4 mt-4"
|
||||||
|
|||||||
24
src/routes/api/heatmap/+server.ts
Normal file
24
src/routes/api/heatmap/+server.ts
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
import type { RequestHandler } from "./$types";
|
||||||
|
|
||||||
|
export const POST: RequestHandler = async ({ request, locals }) => {
|
||||||
|
const data = await request.json();
|
||||||
|
const { apiURL, apiKey } = locals;
|
||||||
|
|
||||||
|
const postData = { params: data?.params };
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
const response = await fetch(apiURL + "/heatmap", {
|
||||||
|
method: "POST",
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
"X-API-KEY": apiKey,
|
||||||
|
},
|
||||||
|
body: JSON.stringify(postData),
|
||||||
|
});
|
||||||
|
|
||||||
|
const output = await response.text();
|
||||||
|
return new Response(JSON.stringify(output));
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
@ -1,21 +0,0 @@
|
|||||||
export const load = async ({ locals }) => {
|
|
||||||
const getData = async () => {
|
|
||||||
const { apiURL, apiKey } = locals;
|
|
||||||
|
|
||||||
const response = await fetch(apiURL + "/heatmap", {
|
|
||||||
method: "GET",
|
|
||||||
headers: {
|
|
||||||
"Content-Type": "text/html",
|
|
||||||
"X-API-KEY": apiKey,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const output = await response.text();
|
|
||||||
return output;
|
|
||||||
};
|
|
||||||
|
|
||||||
// Make sure to return a promise
|
|
||||||
return {
|
|
||||||
getData: await getData(),
|
|
||||||
};
|
|
||||||
};
|
|
||||||
@ -1,47 +1,223 @@
|
|||||||
<script>
|
<script lang="ts">
|
||||||
import Plot from "svelte-plotly.js";
|
import * as DropdownMenu from "$lib/components/shadcn/dropdown-menu/index.js";
|
||||||
|
import { Button } from "$lib/components/shadcn/button/index.js";
|
||||||
|
import SEO from "$lib/components/SEO.svelte";
|
||||||
|
|
||||||
// Treemap data configuration
|
export let data;
|
||||||
const data = [
|
let rawData = data?.getData;
|
||||||
{
|
let iframe: HTMLIFrameElement;
|
||||||
type: "treemap",
|
let iframeLoaded = false;
|
||||||
labels: [
|
let selectedFormat: "png" | "jpeg" | "svg" = "png";
|
||||||
"Eve",
|
let selectedTimePeriod = "1D";
|
||||||
"Cain",
|
let isLoaded = false;
|
||||||
"Seth",
|
|
||||||
"Enos",
|
async function getHeatMap() {
|
||||||
"Noam",
|
const postData = { params: selectedTimePeriod };
|
||||||
"Abel",
|
const response = await fetch("/api/heatmap", {
|
||||||
"Awan",
|
method: "POST",
|
||||||
"Enoch",
|
headers: {
|
||||||
"Azura",
|
"Content-Type": "application/json",
|
||||||
],
|
|
||||||
parents: ["", "Eve", "Eve", "Seth", "Seth", "Eve", "Eve", "Awan", "Eve"],
|
|
||||||
textinfo: "label+parent",
|
|
||||||
marker: { line: { width: 2 } },
|
|
||||||
},
|
},
|
||||||
];
|
body: JSON.stringify(postData),
|
||||||
|
});
|
||||||
|
|
||||||
// Layout configuration with black background
|
rawData = await response.json();
|
||||||
const layout = {
|
}
|
||||||
title: "Family Tree Treemap",
|
|
||||||
margin: { l: 0, r: 0, b: 20, t: 40 },
|
async function downloadPlot(item) {
|
||||||
width: 800,
|
if (item === "PNG") {
|
||||||
height: 600,
|
selectedFormat = "png";
|
||||||
paper_bgcolor: "#09090b", // Sets the outer background to black
|
} else if (item === "JPG") {
|
||||||
font: { color: "#FFFFFF" }, // Optional: Change text color to white for better contrast
|
selectedFormat = "jpeg";
|
||||||
|
} else {
|
||||||
|
selectedFormat = "svg";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!iframe || !iframeLoaded) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const iframeWindow = iframe.contentWindow;
|
||||||
|
if (!iframeWindow) return;
|
||||||
|
|
||||||
|
const Plotly = (iframeWindow as any).Plotly;
|
||||||
|
if (!Plotly) throw new Error("Plotly not found in iframe");
|
||||||
|
|
||||||
|
const plotDiv =
|
||||||
|
iframe.contentDocument?.querySelector(".plotly-graph-div");
|
||||||
|
if (!plotDiv) throw new Error("Plotly div not found");
|
||||||
|
|
||||||
|
// Configure image options based on format
|
||||||
|
const options = {
|
||||||
|
format: selectedFormat,
|
||||||
|
width: 1200,
|
||||||
|
height: 800,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Get image data URL
|
||||||
|
const imageData = await Plotly.toImage(plotDiv, options);
|
||||||
|
|
||||||
|
// Create download link
|
||||||
|
const link = document.createElement("a");
|
||||||
|
link.href = imageData;
|
||||||
|
link.download = `sp500-heatmap-${selectedTimePeriod}`;
|
||||||
|
document.body.appendChild(link);
|
||||||
|
link.click();
|
||||||
|
document.body.removeChild(link);
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Download failed:", error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$: {
|
||||||
|
if (selectedTimePeriod && typeof window !== "undefined") {
|
||||||
|
isLoaded = false;
|
||||||
|
getHeatMap();
|
||||||
|
isLoaded = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="w-full min-h-screen">
|
<SEO
|
||||||
<div class="treemap-container">
|
title="S&P 500 Stock Market Heatmap"
|
||||||
<Plot {data} {layout} />
|
description="A stock market heatmap showing the performance of the individual stocks, sectors and industries in the S&P500."
|
||||||
</div>
|
/>
|
||||||
</div>
|
|
||||||
|
|
||||||
<style>
|
<section
|
||||||
.treemap-container {
|
class="w-full max-w-3xl sm:max-w-[1400px] overflow-hidden min-h-screen pb-20 pt-5 px-4 lg:px-3"
|
||||||
margin: 1rem;
|
>
|
||||||
padding: 1rem;
|
<div class="text-sm sm:text-[1rem] breadcrumbs">
|
||||||
}
|
<ul>
|
||||||
</style>
|
<li><a href="/" class="text-gray-300">Home</a></li>
|
||||||
|
<li class="text-gray-300">Heatmap</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="w-full overflow-hidden m-auto mt-5">
|
||||||
|
<div class="sm:p-0 flex justify-center w-full m-auto overflow-hidden">
|
||||||
|
<div
|
||||||
|
class="relative flex justify-center items-start overflow-hidden w-full"
|
||||||
|
>
|
||||||
|
<main class="w-full">
|
||||||
|
<div class="border-b-[2px] flex justify-between items-center gap-4">
|
||||||
|
<h1 class="mb-1 text-white text-2xl sm:text-3xl font-bold">
|
||||||
|
S&P 500 - {selectedTimePeriod} Performance
|
||||||
|
</h1>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex flex-row items-center w-fit">
|
||||||
|
<div
|
||||||
|
class="grid grid-cols-2 sm:grid-cols-3 gap-y-3 sm:gap-y-0 gap-x-2.5 lg:grid-cols-3 w-full mt-5"
|
||||||
|
>
|
||||||
|
<DropdownMenu.Root>
|
||||||
|
<DropdownMenu.Trigger asChild let:builder>
|
||||||
|
<Button
|
||||||
|
builders={[builder]}
|
||||||
|
class="border-gray-600 border bg-default sm:hover:bg-primary ease-out flex flex-row justify-between items-center px-3 py-2 text-white rounded-md truncate"
|
||||||
|
>
|
||||||
|
<span class="truncate text-white">Time Period</span>
|
||||||
|
<svg
|
||||||
|
class="-mr-1 ml-1 h-5 w-5 xs:ml-2 inline-block"
|
||||||
|
viewBox="0 0 20 20"
|
||||||
|
fill="currentColor"
|
||||||
|
style="max-width:40px"
|
||||||
|
aria-hidden="true"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
fill-rule="evenodd"
|
||||||
|
d="M5.293 7.293a1 1 0 011.414 0L10 10.586l3.293-3.293a1 1 0 111.414 1.414l-4 4a1 1 0 01-1.414 0l-4-4a1 1 0 010-1.414z"
|
||||||
|
clip-rule="evenodd"
|
||||||
|
></path>
|
||||||
|
</svg>
|
||||||
|
</Button>
|
||||||
|
</DropdownMenu.Trigger>
|
||||||
|
<DropdownMenu.Content
|
||||||
|
class="w-auto h-fit max-h-72 overflow-y-auto scroller"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="relative sticky z-40 focus:outline-none -top-1"
|
||||||
|
tabindex="0"
|
||||||
|
role="menu"
|
||||||
|
style=""
|
||||||
|
></div>
|
||||||
|
<DropdownMenu.Group>
|
||||||
|
{#each ["1D", "1W", "1M", "3M", "6M", "1Y", "3Y"] as item}
|
||||||
|
<DropdownMenu.Item class="sm:hover:bg-primary">
|
||||||
|
<div class="flex items-center">
|
||||||
|
<button
|
||||||
|
on:click={() => (selectedTimePeriod = item)}
|
||||||
|
class="cursor-pointer text-white"
|
||||||
|
>
|
||||||
|
<span class="mr-8">{item}</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</DropdownMenu.Item>
|
||||||
|
{/each}
|
||||||
|
</DropdownMenu.Group>
|
||||||
|
</DropdownMenu.Content>
|
||||||
|
</DropdownMenu.Root>
|
||||||
|
|
||||||
|
<DropdownMenu.Root>
|
||||||
|
<DropdownMenu.Trigger asChild let:builder>
|
||||||
|
<Button
|
||||||
|
builders={[builder]}
|
||||||
|
class="border-gray-600 border bg-default sm:hover:bg-primary ease-out flex flex-row justify-between items-center px-3 py-2 text-white rounded-md truncate"
|
||||||
|
>
|
||||||
|
<span class="truncate text-white">Download</span>
|
||||||
|
<svg
|
||||||
|
class="-mr-1 ml-1 h-5 w-5 xs:ml-2 inline-block"
|
||||||
|
viewBox="0 0 20 20"
|
||||||
|
fill="currentColor"
|
||||||
|
style="max-width:40px"
|
||||||
|
aria-hidden="true"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
fill-rule="evenodd"
|
||||||
|
d="M5.293 7.293a1 1 0 011.414 0L10 10.586l3.293-3.293a1 1 0 111.414 1.414l-4 4a1 1 0 01-1.414 0l-4-4a1 1 0 010-1.414z"
|
||||||
|
clip-rule="evenodd"
|
||||||
|
></path>
|
||||||
|
</svg>
|
||||||
|
</Button>
|
||||||
|
</DropdownMenu.Trigger>
|
||||||
|
<DropdownMenu.Content
|
||||||
|
class="w-auto h-fit max-h-72 overflow-y-auto scroller"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="relative sticky z-40 focus:outline-none -top-1"
|
||||||
|
tabindex="0"
|
||||||
|
role="menu"
|
||||||
|
style=""
|
||||||
|
></div>
|
||||||
|
<DropdownMenu.Group>
|
||||||
|
{#each ["PNG", "JPG", "SVG"] as item}
|
||||||
|
<DropdownMenu.Item class="sm:hover:bg-primary">
|
||||||
|
<div class="flex items-center">
|
||||||
|
<button
|
||||||
|
on:click={() => downloadPlot(item)}
|
||||||
|
disabled={!iframeLoaded}
|
||||||
|
class="cursor-pointer text-white"
|
||||||
|
>
|
||||||
|
<span class="mr-8">Download {item}</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</DropdownMenu.Item>
|
||||||
|
{/each}
|
||||||
|
</DropdownMenu.Group>
|
||||||
|
</DropdownMenu.Content>
|
||||||
|
</DropdownMenu.Root>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="w-full min-h-screen bg-[#09090B] overflow-hidden">
|
||||||
|
{#if rawData}
|
||||||
|
<iframe
|
||||||
|
bind:this={iframe}
|
||||||
|
srcdoc={rawData}
|
||||||
|
class="w-full h-screen border-none"
|
||||||
|
on:load={() => (iframeLoaded = true)}
|
||||||
|
/>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|||||||
@ -65,6 +65,7 @@ const pages = [
|
|||||||
{ title: "/politicians/flow-data" },
|
{ title: "/politicians/flow-data" },
|
||||||
{ title: "/analysts" },
|
{ title: "/analysts" },
|
||||||
{ title: "/analysts/top-stocks" },
|
{ title: "/analysts/top-stocks" },
|
||||||
|
{ title: "/heatmap" },
|
||||||
];
|
];
|
||||||
|
|
||||||
const website = "https://stocknear.com";
|
const website = "https://stocknear.com";
|
||||||
|
|||||||
@ -124,6 +124,10 @@
|
|||||||
title: "Stock Screener",
|
title: "Stock Screener",
|
||||||
link: "/stock-screener",
|
link: "/stock-screener",
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
title: "Market Heatmap",
|
||||||
|
link: "/heatmap",
|
||||||
|
},
|
||||||
{
|
{
|
||||||
title: "Stock Lists",
|
title: "Stock Lists",
|
||||||
link: "/list",
|
link: "/list",
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user