add advanced filter to options flow
This commit is contained in:
parent
a2fab526b9
commit
9e8197e1e3
@ -1,4 +1,4 @@
|
||||
<script lang='ts'>
|
||||
<script lang="ts">
|
||||
import { goto } from '$app/navigation';
|
||||
import { screenWidth, numberOfUnreadNotification, etfTicker, stockTicker, isOpen } from '$lib/store';
|
||||
import notifySound from '$lib/audio/options-flow-reader.mp3';
|
||||
@ -6,10 +6,245 @@
|
||||
import { abbreviateNumber } from '$lib/utils';
|
||||
import { onMount, onDestroy } from 'svelte';
|
||||
import toast from 'svelte-french-toast';
|
||||
import * as DropdownMenu from "$lib/components/shadcn/dropdown-menu/index.js";
|
||||
import { Button } from "$lib/components/shadcn/button/index.js";
|
||||
import VirtualList from 'svelte-tiny-virtual-list';
|
||||
import { writable } from 'svelte/store';
|
||||
|
||||
export let data;
|
||||
|
||||
let ruleOfList = [];
|
||||
let displayRules = [];
|
||||
let syncWorker: Worker | undefined;
|
||||
let ruleName = '';
|
||||
let shouldLoadWorker = writable(false);
|
||||
|
||||
const allRules = {
|
||||
volume: { label: 'Volume', step: ['100K','10K','1K'], defaultCondition: 'over', defaultValue: '1K' },
|
||||
open_interest: { label: 'Open Interest', step: ['100K','10K','1K'], defaultCondition: 'over', defaultValue: '1K' },
|
||||
cost_basis: { label: 'Premium', step: ['10M','5M','1M','500K','100K','50K','10K','5K'], defaultCondition: 'over', defaultValue: '50K' },
|
||||
put_call: { label: 'Contract Type', step: ["Calls", "Puts"], defaultValue: 'any' },
|
||||
sentiment: { label: 'Sentiment', step: ["Bullish","Neutral", "Bearish"], defaultValue: 'any' },
|
||||
execution_estimate: { label: 'Execution', step: ["At Ask","At Bid"], defaultValue: 'any' },
|
||||
option_activity_type: { label: 'Option Type', step: ["Sweep","Trade"], defaultValue: 'any' },
|
||||
|
||||
};
|
||||
|
||||
// Generate allRows from allRules
|
||||
$: allRows = Object?.entries(allRules)
|
||||
?.sort(([, a], [, b]) => a.label.localeCompare(b.label)) // Sort by label
|
||||
?.map(([ruleName, ruleProps]) => ({
|
||||
rule: ruleName,
|
||||
...ruleProps
|
||||
}));
|
||||
|
||||
let ruleCondition = {};
|
||||
let valueMappings = {};
|
||||
|
||||
Object.keys(allRules).forEach(ruleName => {
|
||||
ruleCondition[ruleName] = allRules[ruleName].defaultCondition;
|
||||
valueMappings[ruleName] = allRules[ruleName].defaultValue;
|
||||
});
|
||||
|
||||
// Update ruleCondition and valueMappings based on existing rules
|
||||
ruleOfList.forEach(rule => {
|
||||
ruleCondition[rule.name] = rule.condition || allRules[rule.name].defaultCondition;
|
||||
valueMappings[rule.name] = rule.value || allRules[rule.name].defaultValue;
|
||||
});
|
||||
|
||||
|
||||
async function handleDeleteRule(state) {
|
||||
for (let i = 0; i < ruleOfList.length; i++) {
|
||||
if (ruleOfList[i].name === state) {
|
||||
ruleOfList.splice(i, 1); // Remove the element at index i from the ruleOfList
|
||||
ruleOfList = [...ruleOfList]
|
||||
break; // Exit the loop after deleting the element
|
||||
}
|
||||
}
|
||||
|
||||
if(ruleOfList?.length === 0)
|
||||
{
|
||||
ruleName = '';
|
||||
}
|
||||
else if (state === ruleName)
|
||||
{
|
||||
ruleName = '';
|
||||
}
|
||||
|
||||
displayRules = allRows?.filter(row => ruleOfList.some(rule => rule.name === row.rule));
|
||||
}
|
||||
|
||||
|
||||
|
||||
async function handleResetAll() {
|
||||
ruleOfList = [];
|
||||
ruleOfList = [...ruleOfList];
|
||||
ruleName = '';
|
||||
displayRules = allRows?.filter(row => ruleOfList.some(rule => rule.name === row.rule));
|
||||
|
||||
}
|
||||
|
||||
function changeRule(state: string)
|
||||
{
|
||||
ruleName = state;
|
||||
handleAddRule()
|
||||
|
||||
//const closePopup = document.getElementById("ruleModal");
|
||||
//closePopup?.dispatchEvent(new MouseEvent('click'))
|
||||
}
|
||||
|
||||
function handleAddRule() {
|
||||
|
||||
if (ruleName === '') {
|
||||
toast.error('Please select a rule', {
|
||||
style: 'border-radius: 200px; background: #333; color: #fff;'
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
let newRule;
|
||||
|
||||
switch (ruleName) {
|
||||
case "put_call":
|
||||
case "sentiment":
|
||||
case "execution_estimate","option_activity_type":
|
||||
case "option_activity_type":
|
||||
newRule = { name: ruleName, value: Array.isArray(valueMappings[ruleName]) ? valueMappings[ruleName] : [valueMappings[ruleName]] }; // Ensure value is an array
|
||||
break;
|
||||
default:
|
||||
newRule = {
|
||||
name: ruleName,
|
||||
condition: ruleCondition[ruleName],
|
||||
value: valueMappings[ruleName]
|
||||
};
|
||||
break;
|
||||
}
|
||||
handleRule(newRule);
|
||||
}
|
||||
|
||||
|
||||
|
||||
async function handleRule(newRule) {
|
||||
const existingRuleIndex = ruleOfList.findIndex(rule => rule.name === newRule.name);
|
||||
if (existingRuleIndex !== -1) {
|
||||
const existingRule = ruleOfList[existingRuleIndex];
|
||||
if (existingRule.value === newRule.value && existingRule.condition === newRule.condition) {
|
||||
toast.error('Rule already exists!', {
|
||||
style: 'border-radius: 200px; background: #333; color: #fff;'
|
||||
});
|
||||
} else {
|
||||
ruleOfList[existingRuleIndex] = newRule;
|
||||
ruleOfList = [...ruleOfList]; // Trigger reactivity
|
||||
toast.success('Rule updated', {
|
||||
style: 'border-radius: 200px; background: #333; color: #fff;'
|
||||
});
|
||||
}
|
||||
} else {
|
||||
ruleOfList = [...ruleOfList, newRule];
|
||||
toast.success('Rule added', {
|
||||
style: 'border-radius: 200px; background: #333; color: #fff;'
|
||||
});
|
||||
|
||||
shouldLoadWorker.set(true);
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
const loadWorker = async () => {
|
||||
syncWorker.postMessage({ rawData, ruleOfList });
|
||||
console.log(ruleOfList)
|
||||
};
|
||||
|
||||
const handleMessage = (event) => {
|
||||
displayRules = allRows?.filter(row => ruleOfList.some(rule => rule.name === row.rule));
|
||||
displayedData = event.data?.filteredData ?? [];
|
||||
//console.log(displayedData)
|
||||
};
|
||||
|
||||
|
||||
function changeRuleCondition(name: string, state: string) {
|
||||
ruleName = name;
|
||||
ruleCondition[ruleName] = state;
|
||||
}
|
||||
|
||||
let checkedItems = new Set();
|
||||
|
||||
function isChecked(item) {
|
||||
return checkedItems.has(item);
|
||||
}
|
||||
|
||||
async function handleChangeValue(value) {
|
||||
|
||||
|
||||
if (checkedItems.has(value)) {
|
||||
checkedItems.delete(value);
|
||||
} else {
|
||||
checkedItems.add(value);
|
||||
}
|
||||
if (["put_call","sentiment","execution_estimate","option_activity_type"]?.includes(ruleName)) {
|
||||
// Ensure valueMappings[ruleName] is initialized as an array
|
||||
if (!Array.isArray(valueMappings[ruleName])) {
|
||||
valueMappings[ruleName] = []; // Initialize as an empty array if not already
|
||||
}
|
||||
|
||||
const index = valueMappings[ruleName].indexOf(value);
|
||||
if (index === -1) {
|
||||
// Add the country if it's not already selected
|
||||
valueMappings[ruleName].push(value);
|
||||
} else {
|
||||
// Remove the country if it's already selected (deselect)
|
||||
valueMappings[ruleName].splice(index, 1);
|
||||
}
|
||||
|
||||
// If no countries are selected, set value to "any"
|
||||
if (valueMappings[ruleName].length === 0) {
|
||||
valueMappings[ruleName] = "any";
|
||||
}
|
||||
|
||||
} else if (ruleName in valueMappings) {
|
||||
// Handle non-country rules as single values
|
||||
valueMappings[ruleName] = value;
|
||||
} else {
|
||||
console.warn(`Unhandled rule: ${ruleName}`);
|
||||
}
|
||||
|
||||
const ruleToUpdate = ruleOfList?.find(rule => rule.name === ruleName);
|
||||
if (ruleToUpdate) {
|
||||
ruleToUpdate.value = valueMappings[ruleToUpdate.name];
|
||||
ruleToUpdate.condition = ruleCondition[ruleToUpdate.name];
|
||||
ruleOfList = [...ruleOfList];
|
||||
}
|
||||
|
||||
shouldLoadWorker.set(true);
|
||||
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
$: {
|
||||
if (ruleOfList) {
|
||||
const ruleToUpdate = ruleOfList?.find(rule => rule.name === ruleName);
|
||||
if (ruleToUpdate) {
|
||||
ruleToUpdate.value = valueMappings[ruleToUpdate.name];
|
||||
ruleToUpdate.condition = ruleCondition[ruleToUpdate.name];
|
||||
ruleOfList = [...ruleOfList];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
const nyseDate = new Date(data?.getOptionsFlowFeed?.at(0)?.date ?? null)?.toLocaleString('en-US', {
|
||||
month: 'short',
|
||||
@ -58,7 +293,7 @@ const nyseDate = new Date(data?.getOptionsFlowFeed?.at(0)?.date ?? null)?.toLoca
|
||||
let optionSentiment;
|
||||
let optionPrice;
|
||||
let optionTradeCount;
|
||||
let optionExecutionEstimate;
|
||||
let optionexecution_estimate;
|
||||
let optionExchange;
|
||||
|
||||
/*
|
||||
@ -124,7 +359,7 @@ function handleViewData(optionData) {
|
||||
optionPremium = abbreviateNumber(optionData?.cost_basis,true);
|
||||
optionExpiry = reformatDate(optionData?.date_expiration);
|
||||
optionContract = optionData?.put_call;
|
||||
optionType = optionData?.type;
|
||||
optionType = optionData?.option_activity_type;
|
||||
optionStrike = optionData?.strike_price;
|
||||
optionVolume = new Intl.NumberFormat("en", {minimumFractionDigits: 0, maximumFractionDigits: 0}).format(optionData?.volume);
|
||||
optionSpot = optionData?.underlying_price;
|
||||
@ -132,7 +367,7 @@ function handleViewData(optionData) {
|
||||
optionSentiment = optionData?.sentiment;
|
||||
optionPrice = optionData?.price;
|
||||
optionTradeCount = optionData?.tradeCount;
|
||||
optionExecutionEstimate = optionData?.executionEstimate;
|
||||
optionexecution_estimate = optionData?.execution_estimate;
|
||||
optionExchange = optionData?.exchange;
|
||||
|
||||
const openPopup = $screenWidth < 640 ? document.getElementById("optionDetailsMobileModal") : document.getElementById("optionDetailsDesktopModal");
|
||||
@ -164,11 +399,6 @@ function handleViewData(optionData) {
|
||||
filteredData = filteredData.filter(item => item?.ticker === filterQuery?.toUpperCase());
|
||||
}
|
||||
|
||||
// Apply filterList if it exists
|
||||
if (filterList?.length !== 0) {
|
||||
filteredData = filterExpiringSoon(filteredData, Math.max(...filterList));
|
||||
}
|
||||
|
||||
if (filteredData.length !== 0) {
|
||||
notFound = false;
|
||||
} else {
|
||||
@ -208,6 +438,22 @@ function handleViewData(optionData) {
|
||||
rawData = data?.getOptionsFlowFeed;
|
||||
displayedData = rawData;
|
||||
calculateStats(rawData);
|
||||
|
||||
if (!syncWorker) {
|
||||
const SyncWorker = await import('./workers/filterWorker?worker');
|
||||
syncWorker = new SyncWorker.default();
|
||||
syncWorker.onmessage = handleMessage;
|
||||
}
|
||||
|
||||
shouldLoadWorker.subscribe(async (value) => {
|
||||
if (value) {
|
||||
isLoaded = false;
|
||||
await loadWorker();
|
||||
shouldLoadWorker.set(false); // Reset after worker is loaded
|
||||
isLoaded = true;
|
||||
}
|
||||
});
|
||||
|
||||
isLoaded = true;
|
||||
|
||||
if ($isOpen) {
|
||||
@ -215,6 +461,7 @@ function handleViewData(optionData) {
|
||||
}
|
||||
|
||||
|
||||
|
||||
});
|
||||
|
||||
onDestroy(async() => {
|
||||
@ -409,26 +656,6 @@ function debounce(fn, delay) {
|
||||
const debouncedHandleInput = debounce(handleInput, 300);
|
||||
|
||||
|
||||
async function handleFilter(newFilter) {
|
||||
//e.preventDefault();
|
||||
|
||||
const filterSet = new Set(filterList);
|
||||
|
||||
// Check if the new filter already exists in the list
|
||||
if (filterSet?.has(newFilter)) {
|
||||
// If it exists, remove it from the list
|
||||
filterSet?.delete(newFilter);
|
||||
} else {
|
||||
// If it doesn't exist, add it to the list
|
||||
filterSet?.add(newFilter);
|
||||
|
||||
}
|
||||
filterList = Array?.from(filterSet);
|
||||
//console.log(filterList)
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
// Function to filter elements with date_expiration within a given number of days
|
||||
const filterExpiringSoon = (data, days) => {
|
||||
@ -498,7 +725,7 @@ $: {
|
||||
|
||||
|
||||
|
||||
<section class="w-full max-w-screen sm:max-w-6xl xl:max-w-7xl flex justify-center items-center bg-[#09090B] ">
|
||||
<section class="w-full max-w-screen sm:max-w-6xl xl:max-w-7xl flex justify-center items-center bg-[#09090B] pb-20">
|
||||
|
||||
|
||||
<div class="w-full m-auto pl-3 pr-3">
|
||||
@ -519,11 +746,14 @@ $: {
|
||||
|
||||
|
||||
|
||||
<div class="flex flex-col sm:flex-row items-center w-full bg-[#262626] rounded-lg px-3">
|
||||
|
||||
|
||||
<div class="flex flex-row items-center justify-center sm:justify-start mt-6 pb-5">
|
||||
<label data-tip="Audio Preference" on:click={() => muted = !muted} class="xl:tooltip xl:tooltip-bottom flex flex-col items-center mr-6 cursor-pointer">
|
||||
|
||||
|
||||
<div class="rounded-lg border border-gray-700 bg-[#262626] p-2">
|
||||
<div class="flex flex-col sm:flex-row items-center pt-3 sm:pt-1 pb-3 sm:border-b sm:border-gray-600">
|
||||
<div class="flex flex-row items-center justify-center sm:justify-start">
|
||||
<label data-tip="Audio Preference" on:click={() => muted = !muted} class="xl:tooltip xl:tooltip-bottom flex flex-col items-center mr-3 cursor-pointer">
|
||||
<div class="rounded-full w-10 h-10 relative bg-[#000] flex items-center justify-center">
|
||||
{#if !muted}
|
||||
<svg class="w-4 h-4" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><path fill="#fff" d="M9 2.5a.5.5 0 0 0-.849-.358l-2.927 2.85H3.5a1.5 1.5 0 0 0-1.5 1.5v2.99a1.5 1.5 0 0 0 1.5 1.5h1.723l2.927 2.875A.5.5 0 0 0 9 13.5zm1.111 2.689a.5.5 0 0 1 .703-.08l.002.001l.002.002l.005.004l.015.013l.046.04c.036.034.085.08.142.142c.113.123.26.302.405.54c.291.48.573 1.193.573 2.148c0 .954-.282 1.668-.573 2.148a3.394 3.394 0 0 1-.405.541a2.495 2.495 0 0 1-.202.196l-.008.007h-.001s-.447.243-.703-.078a.5.5 0 0 1 .075-.7l.002-.002l-.001.001l.002-.001h-.001l.018-.016c.018-.017.048-.045.085-.085a2.4 2.4 0 0 0 .284-.382c.21-.345.428-.882.428-1.63c0-.747-.218-1.283-.428-1.627a2.382 2.382 0 0 0-.368-.465a.5.5 0 0 1-.096-.717m1.702-2.08a.5.5 0 1 0-.623.782l.011.01l.052.045c.047.042.116.107.201.195c.17.177.4.443.63.794c.46.701.92 1.733.92 3.069a5.522 5.522 0 0 1-.92 3.065c-.23.35-.46.614-.63.79a3.922 3.922 0 0 1-.252.24l-.011.01h-.001a.5.5 0 0 0 .623.782l.033-.027l.075-.065c.063-.057.15-.138.253-.245a6.44 6.44 0 0 0 .746-.936a6.522 6.522 0 0 0 1.083-3.614a6.542 6.542 0 0 0-1.083-3.618a6.517 6.517 0 0 0-.745-.938a4.935 4.935 0 0 0-.328-.311l-.023-.019l-.007-.006l-.002-.002zM10.19 5.89l-.002-.001Z"/></svg>
|
||||
@ -535,7 +765,7 @@ $: {
|
||||
|
||||
|
||||
<span class="text-xs sm:text-sm sm:text-lg {!mode ? 'text-white' : 'text-gray-400'} mr-3">
|
||||
Paused
|
||||
{$isOpen ? 'Paused' : 'Market Closed'}
|
||||
</span>
|
||||
|
||||
<label class="inline-flex cursor-pointer relative focus-none">
|
||||
@ -553,8 +783,7 @@ $: {
|
||||
|
||||
</div>
|
||||
|
||||
<!--Start Filter-->
|
||||
<div class="sm:ml-auto w-full sm:w-fit">
|
||||
<div class="sm:ml-auto w-full sm:w-fit pt-5">
|
||||
<div class="relative flex flex-col sm:flex-row items-center">
|
||||
<div class="relative w-full sm:w-fit pl-3 py-2 sm:py-1.5 sm:mr-5 mb-4 sm:mb-0 flex-auto text-center bg-[#313131] rounded-lg border border-gray-600">
|
||||
<label class="flex flex-row items-center ">
|
||||
@ -575,21 +804,118 @@ $: {
|
||||
</span>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<div class="sm:mt-3 flex flex-col gap-y-2.5 sm:flex-row lg:gap-y-2 pb-1">
|
||||
<label for="ruleModal" class="inline-flex cursor-pointer items-center justify-center space-x-1 whitespace-nowrap rounded-md border border-transparent bg-blue-brand_light py-2 pl-3 pr-4 text-base font-semibold text-white shadow-sm bg-[#000] sm:hover:bg-[#09090B]/60 ease-out focus:outline-none focus:ring-2 focus:ring-blue-500 sm:text-smaller">
|
||||
<svg class="h-5 w-5" viewBox="0 0 20 20" fill="currentColor" style="max-width:40px" aria-hidden="true">
|
||||
<path fill-rule="evenodd" d="M10 3a1 1 0 011 1v5h5a1 1 0 110 2h-5v5a1 1 0 11-2 0v-5H4a1 1 0 110-2h5V4a1 1 0 011-1z" clip-rule="evenodd"></path>
|
||||
</svg>
|
||||
<div>Add Filters</div>
|
||||
</label>
|
||||
|
||||
{#if ruleOfList?.length !== 0}
|
||||
<label on:click={handleResetAll} class="sm:ml-3 cursor-pointer inline-flex items-center justify-center space-x-1 whitespace-nowrap rounded-md border border-transparent bg-blue-brand_light py-2 pl-3 pr-4 text-base font-semibold text-white shadow-sm bg-[#000] sm:hover:text-red-500 ease-out focus:outline-none focus:ring-2 focus:ring-blue-500 sm:text-smaller">
|
||||
<svg class="h-4 w-4" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 21 21"><g fill="none" fill-rule="evenodd" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round"><path d="M3.578 6.487A8 8 0 1 1 2.5 10.5"/><path d="M7.5 6.5h-4v-4"/></g></svg>
|
||||
<div>Reset All</div>
|
||||
</label>
|
||||
{/if}
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
<div class="py-2 sm:py-1.5 mb-5 sm:mb-0 flex-auto text-center bg-[#000] rounded-lg w-full sm:w-fit">
|
||||
<label for="filterList" class="sm:flex sm:flex-row justify-center items-center cursor-pointer px-5">
|
||||
<svg class="h-6 w-6 inline-block" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><path fill="none" stroke="#fff" stroke-linecap="round" stroke-linejoin="round" stroke-width="32" d="M32 144h448M112 256h288M208 368h96"/></svg>
|
||||
<span class="m-auto text-[1rem] text-white ml-2 px-0 py-1 bg-inherit">
|
||||
Filters
|
||||
<div class="sm:grid sm:grid-cols-2 sm:gap-x-2.5 lg:grid-cols-3 w-full mt-2 border-t border-b border-gray-600">
|
||||
{#each displayRules as row (row?.rule)}
|
||||
<!--Start Added Rules-->
|
||||
<div class="flex items-center justify-between space-x-2 px-1 py-1.5 text-smaller leading-tight text-default">
|
||||
<div class="text-white text-[1rem]">
|
||||
{row?.label?.replace('[%]','')}
|
||||
</div>
|
||||
<div class="flex items-center">
|
||||
<button on:click={() => handleDeleteRule(row?.rule)} class="mr-1.5 cursor-pointer text-gray-300 sm:hover:text-red-500 focus:outline-none" title="Remove filter">
|
||||
<svg class="w-6 h-6" fill="none" viewBox="0 0 24 24" stroke="CurrentColor" style="max-width:40px">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10 14l2-2m0 0l2-2m-2 2l-2-2m2 2l2 2m7-2a9 9 0 11-18 0 9 9 0 0118 0z"></path>
|
||||
</svg>
|
||||
</button>
|
||||
<div class="relative inline-block text-left">
|
||||
<div on:click={() => ruleName = row?.rule}>
|
||||
<DropdownMenu.Root>
|
||||
<DropdownMenu.Trigger asChild let:builder>
|
||||
<Button builders={[builder]} class="bg-[#000] h-[40px] flex flex-row justify-between items-center w-[150px] xs:w-[140px] sm:w-[150px] px-3 text-white rounded-lg truncate">
|
||||
<span class="truncate ml-2 text-sm sm:text-[1rem]">
|
||||
{#if valueMappings[row?.rule] === 'any'}
|
||||
Any
|
||||
{:else}
|
||||
{ruleCondition[row?.rule] !== undefined ? ruleCondition[row?.rule] : ''} {valueMappings[row?.rule]}
|
||||
{/if}
|
||||
</span>
|
||||
<svg class=" ml-1 h-6 w-6 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-56 h-fit max-h-72 overflow-y-auto scroller">
|
||||
{#if !['put_call',"sentiment", "execution_estimate","option_activity_type"]?.includes(row?.rule)}
|
||||
<DropdownMenu.Label class="absolute mt-2 h-11 border-gray-800 border-b -top-1 z-20 fixed sticky bg-[#09090B]">
|
||||
<div class="flex items-center justify-start gap-x-1">
|
||||
<div class="relative inline-block flex flex-row items-center justify-center">
|
||||
<label on:click={() => changeRuleCondition(row?.rule, 'under')} class="cursor-pointer flex flex-row mr-2 justify-center items-center">
|
||||
<input type="radio" class="radio checked:bg-purple-600 bg-[#09090B] border border-slate-800 mr-2"
|
||||
checked={ruleCondition[row?.rule] === 'under'} name={row?.rule} />
|
||||
<span class="label-text text-white">Under</span>
|
||||
</label>
|
||||
<label on:click={() => changeRuleCondition(row?.rule, 'over')} class="cursor-pointer flex flex-row ml-2 justify-center items-center">
|
||||
<input type="radio" class="radio checked:bg-purple-600 bg-[#09090B] border border-slate-800 mr-2"
|
||||
checked={ruleCondition[row?.rule] === 'over'} name={row?.rule} />
|
||||
<span class="label-text text-white">Over</span>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</DropdownMenu.Label>
|
||||
{:else}
|
||||
<div class="relative sticky z-40 focus:outline-none -top-1"
|
||||
tabindex="0" role="menu" style="">
|
||||
|
||||
</div>
|
||||
{/if}
|
||||
<DropdownMenu.Group class="min-h-10 mt-2">
|
||||
{#if !['put_call',"sentiment", "execution_estimate","option_activity_type"]?.includes(row?.rule)}
|
||||
{#each row?.step as newValue}
|
||||
<DropdownMenu.Item class="sm:hover:bg-[#27272A]">
|
||||
|
||||
<button on:click={() => {handleChangeValue(newValue)}} class="block w-full border-b border-gray-600 px-4 py-1.5 text-left text-sm sm:text-[1rem] rounded text-white last:border-0 sm:hover:bg-[#27272A] focus:bg-blue-100 focus:text-gray-900 focus:outline-none">
|
||||
{ruleCondition[row?.rule] !== undefined ? ruleCondition[row?.rule] : ''} {newValue}
|
||||
</button>
|
||||
</DropdownMenu.Item>
|
||||
{/each}
|
||||
{:else if ['put_call',"sentiment", "execution_estimate","option_activity_type"]?.includes(row?.rule)}
|
||||
{#each row?.step as item}
|
||||
<DropdownMenu.Item class="sm:hover:bg-[#27272A]">
|
||||
<div class="flex items-center" on:click|capture={(event) => event.preventDefault()}>
|
||||
<label on:click={() => {handleChangeValue(item)}} class="cursor-pointer text-white" for={item}>
|
||||
<input type="checkbox" checked={isChecked(item)}>
|
||||
<span class="ml-2">{item}</span>
|
||||
</label>
|
||||
</div>
|
||||
<!--End Filter-->
|
||||
</DropdownMenu.Item>
|
||||
{/each}
|
||||
|
||||
{/if}
|
||||
</DropdownMenu.Group>
|
||||
</DropdownMenu.Content>
|
||||
</DropdownMenu.Root>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!--End Added Rules-->
|
||||
{/each}
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
</div>
|
||||
|
||||
@ -789,8 +1115,6 @@ $: {
|
||||
<!-- Page wrapper -->
|
||||
<div class="flex justify-center w-full m-auto h-full overflow-hidden">
|
||||
|
||||
|
||||
<!-- Content area -->
|
||||
<div class="mt-4 w-full overflow-x-auto overflow-y-auto h-[850px]">
|
||||
<div class="table-container">
|
||||
<div class="table">
|
||||
@ -854,8 +1178,8 @@ $: {
|
||||
{abbreviateNumber(displayedData[index]?.cost_basis)}
|
||||
</div>
|
||||
|
||||
<div style="justify-content: center;" class="td text-sm sm:text-[1rem] text-start {displayedData[index]?.type === 'Sweep' ? 'text-[#C6A755]' : 'text-[#976DB7]'}">
|
||||
{displayedData[index]?.type}
|
||||
<div style="justify-content: center;" class="td text-sm sm:text-[1rem] text-start {displayedData[index]?.option_activity_type === 'Sweep' ? 'text-[#C6A755]' : 'text-[#976DB7]'}">
|
||||
{displayedData[index]?.option_activity_type}
|
||||
</div>
|
||||
|
||||
<div style="justify-content: center;" class="td text-sm sm:text-[1rem] text-white text-end">
|
||||
@ -910,84 +1234,130 @@ $: {
|
||||
|
||||
|
||||
<!--Start View All List-->
|
||||
<input type="checkbox" id="filterList" class="modal-toggle" />
|
||||
{#if $screenWidth >= 640}
|
||||
<!--Start Choose Rule Modal-->
|
||||
<input type="checkbox" id="ruleModal" class="modal-toggle" />
|
||||
|
||||
<dialog id="filterList" class="modal modal-bottom sm:modal-middle ">
|
||||
<dialog id="ruleModal" class="modal modal-bottom sm:modal-middle ">
|
||||
|
||||
|
||||
<label id="filterList" for="filterList" class="cursor-pointer modal-backdrop bg-[#000] bg-opacity-[0.5]"></label>
|
||||
<label id="ruleModal" for="ruleModal" class="cursor-pointer modal-backdrop bg-[#000] bg-opacity-[0.5]"></label>
|
||||
|
||||
|
||||
<div class="modal-box w-full bg-[#09090B] sm:border sm:border-slate-800 overflow-hidden rounded-md">
|
||||
<div class="modal-box w-full bg-[#262626] border border-slate-800 h-[800px] overflow-hidden ">
|
||||
|
||||
|
||||
<div class="relative z-50 mx-2 max-h-[80vh] rounded bg-default opacity-100 bp:mx-3 sm:mx-4 w-full max-w-[1024px]" aria-modal="true">
|
||||
<label for="filterList" class="cursor-pointer absolute right-0 top-0 m-2 sm:right-1 sm:top-1" aria-label="Close">
|
||||
<svg class="w-6 h-6 text-icon" fill="none" viewBox="0 0 24 24" stroke="currentColor" style="max-width:40px">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"></path>
|
||||
<div class="flex flex-col w-full mt-10 sm:mt-0">
|
||||
|
||||
|
||||
<div class="text-white text-3xl font-semibold mb-5">
|
||||
Add Filters
|
||||
</div>
|
||||
|
||||
<!--Start Search bar-->
|
||||
<form class="w-11/12 h-8 mb-8" on:keydown={(e) => e?.key === 'Enter' ? e.preventDefault() : '' }>
|
||||
<label for="search" class="mb-2 text-sm font-medium text-gray-200 sr-only">Search</label>
|
||||
<div class="relative">
|
||||
<div class="absolute inset-y-0 start-0 flex items-center ps-3 pointer-events-none">
|
||||
<svg class="w-4 h-4 text-gray-200 dark:text-gray-400" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 20 20">
|
||||
<path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="m19 19-4-4m0-7A7 7 0 1 1 1 8a7 7 0 0 1 14 0Z"/>
|
||||
</svg>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
<!-- End Search bar-->
|
||||
|
||||
<div class="border-default p-3 xs:p-4 xl:w-[1024px]">
|
||||
<h3 class="mb-1 text-lg xs:mb-2 text-white font-semibold">
|
||||
Filter Expiration Dates up to
|
||||
</h3>
|
||||
</div>
|
||||
<div class="text-white text-sm bg-[#262626] bg-opacity-[0.4] overflow-y-scroll scroller pt-3 rounded-lg max-h-[500px] sm:max-h-[420px] md:max-h-[540px] lg:max-h-[600px]">
|
||||
|
||||
<div class="h-[35vh] sm:h-[10vh] overflow-auto overscroll-contain px-3 pb-4 xs:h-[60vh] xs:px-4 lg:max-h-[600px] mt-5">
|
||||
<div>
|
||||
<div class="flex flex-wrap">
|
||||
<div class="flex w-full items-center space-x-1.5 py-1.5 md:w-1/2 lg:w-1/3 lg:py-1">
|
||||
<input on:click={() => handleFilter(7)} id="oneWeek" type="checkbox" class="cursor-pointer h-[18px] w-[18px] rounded-sm ring-offset-0 bg-gray-600 lg:h-4 lg:w-4">
|
||||
<div class="-mt-0.5">
|
||||
<label for="oneWeek" class="cursor-pointer text-white lg:text-sm">1 Week</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex w-full items-center space-x-1.5 py-1.5 md:w-1/2 lg:w-1/3 lg:py-1">
|
||||
<input on:click={() => handleFilter(30)} id="oneMonth" type="checkbox" class="cursor-pointer h-[18px] w-[18px] rounded-sm ring-offset-0 bg-gray-600 lg:h-4 lg:w-4">
|
||||
<div class="-mt-0.5">
|
||||
<label for="oneMonth" class="cursor-pointer text-white lg:text-sm">1 Month</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex w-full items-center space-x-1.5 py-1.5 md:w-1/2 lg:w-1/3 lg:py-1">
|
||||
<input on:click={() => handleFilter(90)} id="threeMonths" type="checkbox" class="cursor-pointer h-[18px] w-[18px] rounded-sm ring-offset-0 bg-gray-600 lg:h-4 lg:w-4">
|
||||
<div class="-mt-0.5">
|
||||
<label for="threeMonths" class="cursor-pointer text-white lg:text-sm">3 Months</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex w-full items-center space-x-1.5 py-1.5 md:w-1/2 lg:w-1/3 lg:py-1">
|
||||
<input on:click={() => handleFilter(180)} id="sixMonths" type="checkbox" class="cursor-pointer h-[18px] w-[18px] rounded-sm ring-offset-0 bg-gray-600 lg:h-4 lg:w-4">
|
||||
<div class="-mt-0.5">
|
||||
<label for="sixMonths" class="cursor-pointer text-white lg:text-sm">6 Months</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex w-full items-center space-x-1.5 py-1.5 md:w-1/2 lg:w-1/3 lg:py-1">
|
||||
<input on:click={() => handleFilter(365)} id="oneYear" type="checkbox" class="cursor-pointer h-[18px] w-[18px] rounded-sm ring-offset-0 bg-gray-600 lg:h-4 lg:w-4">
|
||||
<div class="-mt-0.5">
|
||||
<label for="oneYear" class="cursor-pointer text-white lg:text-sm">1 Year</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div on:click={() => handleFilter(1095)} class="flex w-full items-center space-x-1.5 py-1.5 md:w-1/2 lg:w-1/3 lg:py-1">
|
||||
<input id="threeYears" type="checkbox" class="cursor-pointer h-[18px] w-[18px] rounded-sm ring-offset-0 bg-gray-600 lg:h-4 lg:w-4">
|
||||
<div class="-mt-0.5">
|
||||
<label for="threeYears" class="cursor-pointer text-white lg:text-sm">3 Years</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="text-white relative">
|
||||
|
||||
|
||||
<table class="table table-sm table-compact">
|
||||
<!-- head -->
|
||||
<tbody>
|
||||
{#each allRows as row, index}
|
||||
<tr on:click={() => changeRule(row?.rule)} class="hover:bg-[#333333] cursor-pointer">
|
||||
<td class="border-b border-[#262626]">{index+1}</td>
|
||||
<td class="text-start border-b border-[#262626]">
|
||||
{#if ruleOfList.find((rule) => rule?.name === row?.rule)}
|
||||
<svg class="flex-shrink-0 w-5 h-5 sm:w-6 sm:h-6 text-green-400 inline-block" fill="currentColor" viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z" clip-rule="evenodd"></path></svg>
|
||||
{/if}
|
||||
</td>
|
||||
<td class="text-start border-b border-[#262626]">{row?.label}</td>
|
||||
</tr>
|
||||
{/each}
|
||||
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
</div>
|
||||
</dialog>
|
||||
<!--End Choose Rule Modal-->
|
||||
{:else}
|
||||
|
||||
<div class="drawer drawer-end z-50">
|
||||
<input id="ruleModal" type="checkbox" class="drawer-toggle" />
|
||||
|
||||
<div class="drawer-side overflow-x-hidden">
|
||||
|
||||
|
||||
<div class="menu w-screen min-h-full bg-[#000] text-base-content overflow-hidden">
|
||||
<div style="top: 0rem;" class="flex flex-row fixed sticky h-14 z-40 bg-[#000] justify-center items-center w-full border-b border-slate-900">
|
||||
<label for="ruleModal" class="cursor-pointer mr-auto ml-3">
|
||||
<svg class="w-6 h-6 inline-block " xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path fill="#fff" d="M9.125 21.1L.7 12.7q-.15-.15-.213-.325T.425 12q0-.2.063-.375T.7 11.3l8.425-8.425q.35-.35.875-.35t.9.375q.375.375.375.875t-.375.875L3.55 12l7.35 7.35q.35.35.35.863t-.375.887q-.375.375-.875.375t-.875-.375Z"/></svg>
|
||||
</label>
|
||||
|
||||
</div>
|
||||
|
||||
<div class="flex flex-row items-center justify-center w-full m-auto mb-8 mt-5">
|
||||
|
||||
<!--Start Search bar-->
|
||||
<form class="w-11/12 h-8" on:keydown={(e) => e?.key === 'Enter' ? e.preventDefault() : '' }>
|
||||
<label for="search" class="mb-2 text-sm font-medium text-gray-200 sr-only">Search</label>
|
||||
<div class="relative">
|
||||
<div class="absolute inset-y-0 start-0 flex items-center ps-3 pointer-events-none">
|
||||
<svg class="w-4 h-4 text-gray-200 dark:text-gray-400" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 20 20">
|
||||
<path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="m19 19-4-4m0-7A7 7 0 1 1 1 8a7 7 0 0 1 14 0Z"/>
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
<!-- End Search bar-->
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
<table class="table table-sm table-compact overflow-y-scroll text-white mb-10">
|
||||
<!-- head -->
|
||||
<tbody>
|
||||
{#each allRows as row, index}
|
||||
<tr on:click={() => changeRule(row?.rule)} class="border-b border-slate-800 cursor-pointer">
|
||||
<td class="w-6">{index+1}</td>
|
||||
<td class="w-3">
|
||||
{#if ruleOfList.find((rule) => rule?.name === row?.rule)}
|
||||
<svg class="flex-shrink-0 w-5 h-5 sm:w-6 sm:h-6 text-green-400 inline-block" fill="currentColor" viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z" clip-rule="evenodd"></path></svg>
|
||||
{/if}
|
||||
</td>
|
||||
<td class="text-start">{row?.label}</td>
|
||||
</tr>
|
||||
{/each}
|
||||
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/if}
|
||||
|
||||
|
||||
|
||||
@ -1056,7 +1426,7 @@ $: {
|
||||
</tr>
|
||||
<tr class="border-b border-slate-700 odd:bg-[#27272A]">
|
||||
<td class="font-semibold">Execution Estimate</td>
|
||||
<td class="">{optionExecutionEstimate}</td>
|
||||
<td class="">{optionexecution_estimate}</td>
|
||||
<td class="font-semibold"></td>
|
||||
<td class=""></td>
|
||||
</tr>
|
||||
@ -1136,7 +1506,7 @@ $: {
|
||||
</tr>
|
||||
<tr class="border-b border-slate-700 odd:bg-[#27272A]">
|
||||
<td class="font-semibold">Execution Est.</td>
|
||||
<td class="">{optionExecutionEstimate}</td>
|
||||
<td class="">{optionexecution_estimate}</td>
|
||||
<td class="font-semibold"></td>
|
||||
<td class=""></td>
|
||||
</tr>
|
||||
|
||||
92
src/routes/options-flow/workers/filterWorker.ts
Normal file
92
src/routes/options-flow/workers/filterWorker.ts
Normal file
@ -0,0 +1,92 @@
|
||||
function convertUnitToValue(
|
||||
input: string | number | string[]
|
||||
): number | string[] | string {
|
||||
if (Array.isArray(input)) return input;
|
||||
if (typeof input === "number") return input;
|
||||
if (typeof input !== "string") {
|
||||
throw new TypeError(
|
||||
`Expected a string or number, but received ${typeof input}`
|
||||
);
|
||||
}
|
||||
const lowerInput = input.toLowerCase();
|
||||
// Pre-compute the set for quick lookups
|
||||
const nonNumericValues = new Set([
|
||||
"any",
|
||||
"puts",
|
||||
"calls",
|
||||
"bullish",
|
||||
"neutral",
|
||||
"bearish",
|
||||
"at bid",
|
||||
"at ask",
|
||||
"sweep",
|
||||
"trade",
|
||||
]);
|
||||
if (nonNumericValues.has(lowerInput)) return input;
|
||||
if (input.endsWith("%")) {
|
||||
const numericValue = parseFloat(input.slice(0, -1));
|
||||
if (isNaN(numericValue)) {
|
||||
throw new Error(`Unable to convert ${input} to a number`);
|
||||
}
|
||||
return numericValue;
|
||||
}
|
||||
const units = { B: 1_000_000_000, M: 1_000_000, K: 1_000 };
|
||||
const match = input.match(/^(\d+(\.\d+)?)([BMK])?$/);
|
||||
if (match) {
|
||||
const value = parseFloat(match[1]);
|
||||
const unit = match[3] as keyof typeof units;
|
||||
return unit ? value * units[unit] : value;
|
||||
}
|
||||
const numericValue = parseFloat(input);
|
||||
if (isNaN(numericValue)) {
|
||||
throw new Error(`Unable to convert ${input} to a number`);
|
||||
}
|
||||
return numericValue;
|
||||
}
|
||||
|
||||
function isAny(value: string | string[]): boolean {
|
||||
if (typeof value === "string") return value.toLowerCase() === "any";
|
||||
if (Array.isArray(value))
|
||||
return value.length === 1 && value[0].toLowerCase() === "any";
|
||||
return false;
|
||||
}
|
||||
|
||||
async function filterRawData(rawData, ruleOfList) {
|
||||
return rawData?.filter((item) => {
|
||||
return ruleOfList.every((rule) => {
|
||||
const itemValue = item[rule.name];
|
||||
const ruleValue = convertUnitToValue(rule.value);
|
||||
const ruleName = rule.name.toLowerCase();
|
||||
|
||||
// Handle categorical data like analyst ratings, sector, country
|
||||
if (
|
||||
[
|
||||
"put_call",
|
||||
"sentiment",
|
||||
"execution_estimate",
|
||||
"option_activity_type",
|
||||
].includes(ruleName)
|
||||
) {
|
||||
if (isAny(ruleValue)) return true; // Return true for "any" or ["any"]
|
||||
if (Array.isArray(ruleValue)) return ruleValue.includes(itemValue);
|
||||
return itemValue === ruleValue;
|
||||
}
|
||||
|
||||
// Default numeric or string comparison
|
||||
if (typeof ruleValue === "string") return true; // Skip non-numeric comparisons
|
||||
if (itemValue === null) return false; // Null values do not meet any condition
|
||||
if (rule.condition === "over" && itemValue <= ruleValue) return false;
|
||||
if (rule.condition === "under" && itemValue > ruleValue) return false;
|
||||
return true;
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
onmessage = async (event: MessageEvent) => {
|
||||
const { rawData, ruleOfList } = event.data || {};
|
||||
const filteredData = await filterRawData(rawData, ruleOfList);
|
||||
postMessage({ message: "success", filteredData });
|
||||
console.log(filteredData);
|
||||
};
|
||||
|
||||
export {};
|
||||
@ -172,7 +172,6 @@ $: allRows = Object?.entries(allRules)
|
||||
}));
|
||||
|
||||
|
||||
allRows?.sort((a, b) => a.label.localeCompare(b.label));
|
||||
let filteredRows;
|
||||
let searchTerm = '';
|
||||
/*
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user