add options watchlist

This commit is contained in:
MuslemRahimi 2024-09-23 21:37:36 +02:00
parent b042d880b3
commit 330aaacd16
7 changed files with 496 additions and 7 deletions

View File

@ -0,0 +1,37 @@
import type { RequestHandler } from "./$types";
import { serialize } from "object-to-formdata";
export const POST: RequestHandler = async ({ request, locals }) => {
const data = await request.json();
const { pb } = locals;
let output;
try {
const watchList = await pb.collection("optionsWatchlist").getOne(data?.id);
if (watchList?.optionsId?.includes(data?.itemId)) {
// Remove ticker from the watchlist.
const newTickerList = watchList?.optionsId.filter(
(item) => item !== data?.itemId
);
output = await pb
.collection("optionsWatchlist")
.update(data?.id, { optionsId: newTickerList });
} else {
// Add ticker to the watchlist.
const newTickerList = [...watchList?.optionsId, data?.itemId];
output = await pb
.collection("optionsWatchlist")
.update(data?.id, { optionsId: newTickerList });
}
} catch (e) {
output = await pb.collection("optionsWatchlist").create(
serialize({
user: locals?.user?.id,
optionsId: JSON.stringify([data?.itemId]),
})
);
}
return new Response(JSON.stringify(output?.id));
};

View File

@ -1,5 +1,5 @@
export const load = async ({ locals, cookies }) => {
const { apiURL, apiKey } = locals;
const { apiURL, apiKey, pb, user } = locals;
const getOptionsFlowFeed = async () => {
// make the POST request to the endpoint
@ -29,9 +29,28 @@ export const load = async ({ locals, cookies }) => {
return output;
};
const getOptionsWatchlist = async () => {
let output;
try {
output = (
await pb?.collection("optionsWatchlist").getFullList({
filter: `user="${user?.id}"`,
})
)?.at(0);
if (output === undefined) {
output = { optionsId: [] };
}
} catch (e) {
//console.log(e)
output = { optionsId: [] };
}
return output;
};
// Make sure to return a promise
return {
getOptionsFlowFeed: await getOptionsFlowFeed(),
getPredefinedCookieRuleOfList: await getPredefinedCookieRuleOfList(),
getOptionsWatchlist: await getOptionsWatchlist(),
};
};

View File

@ -21,6 +21,8 @@
export let data;
let optionsWatchlist = data?.getOptionsWatchlist;
let ruleOfList = data?.getPredefinedCookieRuleOfList || [];
let displayRules = [];
@ -731,6 +733,52 @@ function debounce(fn, delay) {
const debouncedHandleInput = debounce(handleInput, 300);
async function addToWatchlist(itemId) {
if(data?.user?.tier === 'Pro') {
try {
const postData = {
'itemId': itemId,
'id': optionsWatchlist?.id
};
if (optionsWatchlist?.optionsId?.includes(itemId)) {
// Remove ticker from the watchlist.
optionsWatchlist.optionsId = optionsWatchlist?.optionsId.filter(
(item) => item !== itemId);
} else {
// Add ticker to the watchlist.
optionsWatchlist.optionsId = [...optionsWatchlist?.optionsId, itemId];
}
const response = await fetch('/api/update-options-watchlist', {
method: 'POST',
headers: {
"Content-Type": "application/json"
},
body: JSON.stringify(postData),
});
optionsWatchlist.id = await response.json();
console.log(optionsWatchlist)
if (!response.ok) {
throw new Error("Network response was not ok");
}
} catch (error) {
console.error("An error occurred:", error);
// Handle the error appropriately (e.g., show an error message to the user)
}
} else {
toast.error('Only for Pro Members', {
style: 'border-radius: 200px; background: #333; color: #fff;'
});
}
}
$: {
if(searchTerm)
{
@ -1221,6 +1269,7 @@ $: {
<!-- Table headers -->
<div class="td bg-[#161618] text-slate-300 font-bold text-xs text-start uppercase">Time</div>
<div class="td bg-[#161618] font-bold text-slate-300 text-xs text-start uppercase">Symbol</div>
<div class="td bg-[#161618] text-slate-300 font-bold text-xs text-start uppercase">Save</div>
<div class="td bg-[#161618] text-slate-300 font-bold text-xs text-start uppercase">Expiry</div>
<div class="td bg-[#161618] text-slate-300 font-bold text-xs text-start uppercase">DTE</div>
<div class="td bg-[#161618] text-slate-300 font-bold text-xs text-start uppercase">Strike</div>
@ -1251,6 +1300,10 @@ $: {
{displayedData[index]?.ticker}
</a>
<div on:click|stopPropagation ={() => addToWatchlist(displayedData[index]?.id)} style="justify-content: center;" class="td {optionsWatchlist.optionsId?.includes(displayedData[index]?.id) ? 'text-[#FBCE3C]' : 'text-white'}">
<svg class="w-5 h-5 inline-block cursor-pointer flex-shrink-0" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><path fill="currentColor" d="M3.612 15.443c-.386.198-.824-.149-.746-.592l.83-4.73L.173 6.765c-.329-.314-.158-.888.283-.95l4.898-.696L7.538.792c.197-.39.73-.39.927 0l2.184 4.327l4.898.696c.441.062.612.636.282.95l-3.522 3.356l.83 4.73c.078.443-.36.79-.746.592L8 13.187l-4.389 2.256z"/></svg>
</div>
<div style="justify-content: center;" class="td text-sm sm:text-[1rem] text-white text-start">
{reformatDate(displayedData[index]?.date_expiration)}
</div>

View File

@ -0,0 +1,37 @@
export const load = async ({ locals }) => {
const getOptionsWatchlist = async () => {
const { apiKey, apiURL, pb, user } = locals;
try {
// Fetch the watchlist for the user from the database
const watchList = (
await pb.collection("optionsWatchlist").getFullList({
filter: `user="${user?.id}"`,
})
)?.at(0);
if (watchList !== undefined && watchList?.optionsId?.length !== 0) {
const postData = { optionsIdList: watchList?.optionsId };
const response = await fetch(apiURL + "/get-options-watchlist", {
method: "POST",
headers: {
"Content-Type": "application/json",
"X-API-KEY": apiKey,
},
body: JSON.stringify(postData),
});
const output = await response.json();
return output;
} else {
return [];
}
} catch (e) {
console.error("Error fetching options watchlist or Benzinga data:", e);
return [];
}
};
return {
getOptionsWatchlist: await getOptionsWatchlist(),
};
};

View File

@ -0,0 +1,342 @@
<script lang='ts'>
import { numberOfUnreadNotification } from '$lib/store';
import {screenWidth } from '$lib/store';
//import MiniPlot from '$lib/components/MiniPlot.svelte';
import { abbreviateNumber } from '$lib/utils';
import { goto } from '$app/navigation';
import ArrowLogo from "lucide-svelte/icons/move-up-right";
let cloudFrontUrl = import.meta.env.VITE_IMAGE_URL;
export let data;
let isLoaded = false;
let optionsWatchlist = data?.getOptionsWatchlist
function formatDate(dateStr) {
// Parse the input date string (YYYY-mm-dd)
var date = new Date(dateStr);
// Get month, day, and year
var month = date.getMonth() + 1; // Month starts from 0
var day = date.getDate();
var year = date.getFullYear();
// Extract the last two digits of the year
var shortYear = year.toString().slice(-2);
// Add leading zeros if necessary
month = (month < 10 ? "0" : "") + month;
day = (day < 10 ? "0" : "") + day;
var formattedDate = month + "/" + day + "/" + year;
return formattedDate;
}
function formatTime(timeString) {
// Split the time string into components
const [hours, minutes, seconds] = timeString.split(':').map(Number);
// Determine AM or PM
const period = hours >= 12 ? 'PM' : 'AM';
// Convert hours from 24-hour to 12-hour format
const formattedHours = hours % 12 || 12; // Converts 0 to 12 for midnight
// Format the time string
const formattedTimeString = `${formattedHours.toString().padStart(2, '0')}:${minutes.toString().padStart(2, '0')}:${seconds.toString().padStart(2, '0')} ${period}`;
return formattedTimeString;
}
</script>
<svelte:head>
<title> {$numberOfUnreadNotification > 0 ? `(${$numberOfUnreadNotification})` : ''} Options Watchlist · stocknear</title>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width" />
<meta name="description" content="A option watchlist tracker tool. Add stocks and ETFs to keep track of their performance.">
<!-- Other meta tags -->
<meta property="og:title" content="Options Watchlist · stocknear"/>
<meta property="og:description" content="A option watchlist tracker tool. Add stocks and ETFs to keep track of their performance.">
<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="Options Watchlist · stocknear"/>
<meta name="twitter:description" content="A option watchlist tracker tool. Add stocks and ETFs to keep track of their performance.">
<!-- Add more Twitter meta tags as needed -->
</svelte:head>
<section class="w-full max-w-3xl sm:max-w-screen-2xl overflow-hidden min-h-screen pt-5 pb-40 lg:px-3">
<div class="text-sm sm:text-[1rem] breadcrumbs ml-4">
<ul>
<li><a href="/" class="text-gray-300">Home</a></li>
<li class="text-gray-300">Options Watchlist</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 lg:w-3/4 lg:pr-5">
<div class="w-full m-auto bg-[#27272A] sm:rounded-xl h-auto pl-10 pr-10 pt-5 sm:pb-10 sm:pt-10 mt-3 mb-8">
<div class="grid grid-cols-1 sm:grid-cols-2 gap-10">
<!-- Start Column -->
<div>
<div class="flex flex-row justify-center items-center">
<h1 class="text-4xl sm:text-5xl text-white font-bold mb-5">
Watchlist
</h1>
</div>
<span class="text-white text-md font-medium text-center flex justify-center items-center ">
Monitor the performance and recent updates of your favorite options.
</span>
</div>
<!-- End Column -->
<!-- Start Column -->
<div class="relative m-auto mb-5 mt-5 sm:mb-0 sm:mt-0">
<svg class="w-40 -my-5" viewBox="0 0 200 200" xmlns="http://www.w3.org/2000/svg">
<defs>
<filter id="glow">
<feGaussianBlur stdDeviation="5" result="glow"/>
<feMerge>
<feMergeNode in="glow"/>
<feMergeNode in="SourceGraphic"/>
</feMerge>
</filter>
</defs>
<path fill="#1E40AF" d="M57.6,-58.7C72.7,-42.6,81.5,-21.3,82,0.5C82.5,22.3,74.7,44.6,59.7,60.1C44.6,75.6,22.3,84.3,0,84.3C-22.3,84.2,-44.6,75.5,-61.1,60.1C-77.6,44.6,-88.3,22.3,-87.6,0.7C-86.9,-20.8,-74.7,-41.6,-58.2,-57.7C-41.6,-73.8,-20.8,-85.2,0.2,-85.4C21.3,-85.6,42.6,-74.7,57.6,-58.7Z" transform="translate(100 100)" filter="url(#glow)" />
</svg>
<div class="z-1 absolute top-3 right-10 ">
<img class="w-24" src={cloudFrontUrl+"/assets/options_logo.png"} alt="logo" loading='lazy'>
</div>
</div>
<!-- End Column -->
</div>
</div>
{#if optionsWatchlist?.length !== 0}
<!--Start Table-->
<div class="w-screen sm:w-full rounded-lg overflow-hidden overflow-x-scroll no-scrollbar">
<table class="table table-pin-cols table-sm table-compact rounded-none sm:rounded-md w-full border-bg-[#09090B] m-auto mt-4 overflow-x-auto">
<thead>
<tr class="">
<td class="text-slate-200 font-semibold text-sm text-start">Time</td>
<td class="text-slate-200 font-semibold text-sm text-start">Date</td>
<td class="text-slate-200 font-semibold text-sm text-end">Expiry</td>
<td class="text-slate-200 font-semibold text-sm text-end">Strike</td>
<td class="text-slate-200 font-semibold text-sm text-end">C/P</td>
<td class="text-slate-200 font-semibold text-sm text-start">Sent.</td>
<td class="text-slate-200 font-semibold text-sm text-start">Exec.</td>
<td class="text-slate-200 font-semibold text-sm text-end">Spot</td>
<td class="text-slate-200 font-semibold text-sm text-end">Price</td>
<td class="text-slate-200 font-semibold text-sm text-end">Prem.</td>
<td class="text-slate-200 font-semibold text-sm text-start">Type</td>
<td class="text-slate-200 font-semibold text-sm text-end">Vol.</td>
<td class="text-slate-200 font-semibold text-sm text-end">OI</td>
</tr>
</thead>
<tbody>
{#each optionsWatchlist as item}
<!-- row -->
<tr class="odd:bg-[#27272A] border-b-[#09090B]">
<td class="text-white text-sm text-start whitespace-nowrap">
{formatTime(item?.time)}
</td>
<td class="text-white text-sm sm:text-[1rem] text-start">
{formatDate(item?.date)}
</td>
<td class="text-white text-sm sm:text-[1rem] text-end">
{item?.dte < 0 ? 'expired' : item?.dte +'d'}
</td>
<td class="text-sm sm:text-[1rem] text-end text-white">
{item?.strike_price}
</td>
<td class="text-sm sm:text-[1rem] {item?.put_call === 'Calls' ? 'text-[#00FC50]' : 'text-[#FC2120]'} text-start">
{item?.put_call}
</td>
<td class="text-sm sm:text-[1rem] {item?.sentiment === 'Bullish' ? 'text-[#00FC50]' : item?.sentiment === 'Bearish' ? 'text-[#FC2120]' : 'text-[#C6A755]'} text-start">
{item?.sentiment}
</td>
<td class="text-sm sm:text-[1rem] text-white text-start whitespace-nowrap">
{item?.execution_estimate}
</td>
<td class="text-sm sm:text-[1rem] text-end text-white">
{item?.underlying_price}
</td>
<td class="text-sm sm:text-[1rem] text-end text-white">
{item?.price}
</td>
<td class="text-sm sm:text-[1rem] text-end font-medium {item?.put_call === 'Puts' ? 'text-[#CB281C]' : 'text-[#0FB307]'} ">
{abbreviateNumber(item?.cost_basis)}
</td>
<td class="text-sm sm:text-[1rem] text-start {item?.type === 'Sweep' ? 'text-[#C6A755]' : 'text-[#976DB7]'}">
{item?.type}
</td>
<td class="text-white text-end">
{new Intl.NumberFormat("en", {
minimumFractionDigits: 0,
maximumFractionDigits: 0
}).format(item?.volume)}
</td>
<td class="text-white text-end">
{new Intl.NumberFormat("en", {
minimumFractionDigits: 0,
maximumFractionDigits: 0
}).format(item?.open_interest)}
</td>
</tr>
{/each}
</tbody>
</table>
</div>
<!--End Table-->
{:else}
<div class="flex flex-col justify-center items-center m-auto pt-8">
<span class="text-white font-bold text-white text-2xl sm:text-3xl">
Empty Options List
</span>
<span class="text-white text-sm sm:text-lg font-medium m-auto p-4 text-center">
Add your unusual options contracts and start tracking them now!
</span>
{#if !data?.user}
<a class="w-64 flex mt-5 justify-center items-center m-auto btn text-white bg-purple-600 sm:hover:bg-purple-700 transition duration-150 ease-in-out group" href="/register">
<span>Get Started</span>
<span class="tracking-normal group-hover:translate-x-0.5 transition-transform duration-150 ease-in-out">
<svg class="w-4 h-4" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><g transform="rotate(90 12 12)"><g fill="none"><path d="M24 0v24H0V0h24ZM12.593 23.258l-.011.002l-.071.035l-.02.004l-.014-.004l-.071-.035c-.01-.004-.019-.001-.024.005l-.004.01l-.017.428l.005.02l.01.013l.104.074l.015.004l.012-.004l.104-.074l.012-.016l.004-.017l-.017-.427c-.002-.01-.009-.017-.017-.018Zm.265-.113l-.013.002l-.185.093l-.01.01l-.003.011l.018.43l.005.012l.008.007l.201.093c.012.004.023 0 .029-.008l.004-.014l-.034-.614c-.003-.012-.01-.02-.02-.022Zm-.715.002a.023.023 0 0 0-.027.006l-.006.014l-.034.614c0 .012.007.02.017.024l.015-.002l.201-.093l.01-.008l.004-.011l.017-.43l-.003-.012l-.01-.01l-.184-.092Z"/><path fill="white" d="M13.06 3.283a1.5 1.5 0 0 0-2.12 0L5.281 8.939a1.5 1.5 0 0 0 2.122 2.122L10.5 7.965V19.5a1.5 1.5 0 0 0 3 0V7.965l3.096 3.096a1.5 1.5 0 1 0 2.122-2.122L13.06 3.283Z"/></g></g></svg>
</span>
</a>
{:else}
<a href="/options-flow" class="w-64 flex mt-5 justify-center items-center m-auto btn text-white bg-purple-600 sm:hover:bg-purple-700 transition duration-150 ease-in-out group">
<span class="font-semibold text-[1rem]">Follow the Whales
<svg class="inline-block -mt-2 -ml-1 w-8 h-8" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32"><g fill="none"><path fill="#00a6ed" d="M24.04 6.508C27.007 9.5 29 12.953 29 16.468c0 6.422-1.95 10.392-6.648 12.315a16 16 0 0 1-1.652-.318l-.012.582c-.105.563-.485 1.352-1.625.703c-.84-.478-2.112-1.55-2.33-4.281c-.511-1.271-.617-2.91-.733-4.969c-.164-2.91-3.078-3.89-5-4c-1.84-.105-8.316-.467-8.869-.498a15 15 0 0 1-.01-1.111c0-2.66 3.363-4.9 5.713-4.9h6.55c3.46 0 6.27 2.81 6.27 6.27c-.17 2.411 3.373 3.405 3.82.78c.492-2.896-.591-6.166-2.435-8.494c-.195-.047-.308-.047-.445.047c-1.258 1.258-2.16 1.312-3.852.914a1 1 0 0 0-.239-.103c-.228-.077-.435-.147-.331-.608c.14-.625 1.125-1.719 1.125-1.719c.773-.906 1.758-1.11 2.226-1.055c.402.047.515-.068.58-.135l.03-.029c.065-.051.133-.437.133-.437c-.164-.875.043-1.678.547-2.383c0 0 .882-1.266 1.687-1.344c.72-.07.803.37.879.768q.012.071.027.139c.334 1.535.17 1.902-.215 2.765c-.065.147-.137.308-.214.492c-.13.308-.058.434.038.605zM8.219 29.938c-1.735 0-3.64-2.438-3.11-5.922c.313.2 2.615 2.052 3.75 3.14c.329.844.11 2.782-.64 2.782"/><path fill="#1c1c1c" d="M15.517 15a.61.61 0 0 1-.604-.604v-.758c0-.33.274-.604.604-.604s.604.274.604.604v.758a.604.604 0 0 1-.604.604"/><path fill="#aeddff" d="M20.706 28.208q.745.278 1.692.556q-.84.348-1.8.608q.06-.166.09-.325zm-3.973-4.468c-.107-.771-.157-1.671-.217-2.74l-.016-.281c-.164-2.911-3.766-4.61-5.687-4.719c-1.839-.105-8.138-.01-8.682 0c.339 7.399 6.034 14.639 13.885 14.095a27 27 0 0 0 3.007-.367c-1-.583-2.564-2.007-2.29-5.988"/></g></svg>
</span>
</a>
{/if}
</div>
{/if}
</main>
<aside class="hidden lg:block relative fixed w-1/4 ml-4">
{#if data?.user?.tier !== 'Pro' || data?.user?.freeTrial}
<div on:click={() => goto('/pricing')} class="w-full bg-[#141417] duration-100 ease-out sm:hover:text-white text-gray-400 sm:hover:border-gray-700 border border-gray-800 rounded-lg h-fit pb-4 mt-4 cursor-pointer">
<div class="w-auto lg:w-full p-1 flex flex-col m-auto px-2 sm:px-0">
<div class="w-full flex justify-between items-center p-3 mt-3">
<h2 class="text-start text-xl font-semibold text-white ml-3">
Pro Subscription 🔥
</h2>
<ArrowLogo class="w-8 h-8 mr-3 flex-shrink-0"/>
</div>
<span class="text-white p-3 ml-3 mr-3">
Upgrade now for unlimited access to all data and tools.
</span>
</div>
</div>
{/if}
<div on:click={() => goto('/price-alert')} class="w-full bg-[#141417] duration-100 ease-out sm:hover:text-white text-gray-400 sm:hover:border-gray-700 border border-gray-800 rounded-lg h-fit pb-4 mt-4 cursor-pointer">
<div class="w-auto lg:w-full p-1 flex flex-col m-auto px-2 sm:px-0">
<div class="w-full flex justify-between items-center p-3 mt-3">
<h2 class="text-start text-xl font-semibold text-white ml-3">
Price Alert ⏰
</h2>
<ArrowLogo class="w-8 h-8 mr-3 flex-shrink-0"/>
</div>
<span class="text-white p-3 ml-3 mr-3">
Customize your alerts to never miss out again
</span>
</div>
</div>
<div on:click={() => goto('/stock-screener')} class="w-full bg-[#141417] duration-100 ease-out sm:hover:text-white text-gray-400 sm:hover:border-gray-700 border border-gray-800 rounded-lg h-fit pb-4 mt-4 cursor-pointer">
<div class="w-auto lg:w-full p-1 flex flex-col m-auto px-2 sm:px-0">
<div class="w-full flex justify-between items-center p-3 mt-3">
<h2 class="text-start text-xl font-semibold text-white ml-3">
Stock Screener 🔎
</h2>
<ArrowLogo class="w-8 h-8 mr-3 flex-shrink-0"/>
</div>
<span class="text-white p-3 ml-3 mr-3">
Build your Stock Screener to find profitable stocks.
</span>
</div>
</div>
</aside>
</div>
</div>
</div>
</section>

View File

@ -16,7 +16,9 @@ let cloudFrontUrl = import.meta.env.VITE_IMAGE_URL;
export let data;
export let data../$types.js;
/*
const rawData = data?.getMiniPlotsIndex;
@ -43,7 +45,6 @@ function getCurrentDateFormatted() {
return `${month} ${day}, ${year}`;
}
/*
let priceDataSP500;
let priceDataNasdaq;
let priceDataDowJones;
@ -370,7 +371,7 @@ onDestroy( () => {
<section class="w-full max-w-3xl sm:max-w-screen-2xl overflow-hidden min-h-screen pt-5 pb-40 lg:px-3">
<div class="text-sm sm:text-[1rem] breadcrumbs ">
<div class="text-sm sm:text-[1rem] breadcrumbs ml-4">
<ul>
<li><a href="/" class="text-gray-300">Home</a></li>
<li class="text-gray-300">Watchlist</li>
@ -386,7 +387,7 @@ onDestroy( () => {
<main class="w-full lg:w-3/4 lg:pr-5">
<div class="w-full m-auto sm:bg-[#27272A] sm:rounded-xl h-auto pl-10 pr-10 pt-5 sm:pb-10 sm:pt-10 mt-3 mb-8">
<div class="w-full m-auto bg-[#27272A] sm:rounded-xl h-auto pl-10 pr-10 pt-5 sm:pb-10 sm:pt-10 mt-3 mb-8">
<div class="grid grid-cols-1 sm:grid-cols-2 gap-10">
<!-- Start Column -->
@ -397,14 +398,14 @@ onDestroy( () => {
</h1>
</div>
<span class="hidden sm:block text-white text-md font-medium text-center flex justify-center items-center ">
<span class="text-white text-md font-medium text-center flex justify-center items-center ">
Monitor the performance and recent updates of your favorite stocks.
</span>
</div>
<!-- End Column -->
<!-- Start Column -->
<div class="hidden sm:block relative m-auto mb-5 mt-5 sm:mb-0 sm:mt-0">
<div class="relative m-auto mb-5 mt-5 sm:mb-0 sm:mt-0">
<svg class="w-40 -my-5" viewBox="0 0 200 200" xmlns="http://www.w3.org/2000/svg">
<defs>
<filter id="glow">