diff --git a/src/lib/components/Table/DarkPoolTable.svelte b/src/lib/components/Table/DarkPoolTable.svelte index d1a04c90..96ae6449 100644 --- a/src/lib/components/Table/DarkPoolTable.svelte +++ b/src/lib/components/Table/DarkPoolTable.svelte @@ -49,8 +49,8 @@ premium: "none", assetType: "none", volume: "none", - avgVolume: "none", - dailyVolume: "none", + sizeAvgVolRatio: "none", + sizeVolRatio: "none", size: "none", sector: "none", }; @@ -128,14 +128,14 @@ const volB = parseFloat(b.volume); return sortOrder === "asc" ? volA - volB : volB - volA; }, - dailyVolume: (a, b) => { - const volA = parseFloat(a.dailyVolumePercentage); - const volB = parseFloat(b.dailyVolumePercentage); + sizeVolRatio: (a, b) => { + const volA = parseFloat(a.sizeVolRatio); + const volB = parseFloat(b.sizeVolRatio); return sortOrder === "asc" ? volA - volB : volB - volA; }, - avgVolume: (a, b) => { - const volA = parseFloat(a.avgVolumePercentage); - const volB = parseFloat(b.avgVolumePercentage); + sizeAvgVolRatio: (a, b) => { + const volA = parseFloat(a.sizeAvgVolRatio); + const volB = parseFloat(b.sizeAvgVolRatio); return sortOrder === "asc" ? volA - volB : volB - volA; }, assetType: (a, b) => { @@ -304,16 +304,16 @@
sortData("dailyVolume")} + on:click={() => sortData("sizeVolRatio")} class="td cursor-pointer select-none bg-[#121217] text-slate-300 font-bold text-xs text-start uppercase" > % Size / Vol
sortData("avgVolume")} + on:click={() => sortData("sizeAvgVolRatio")} class="td cursor-pointer select-none bg-[#121217] text-slate-300 font-bold text-xs text-start uppercase" > % Size / Avg Vol - {displayedData[index]?.dailyVolumePercentage > 0.01 - ? displayedData[index]?.dailyVolumePercentage?.toFixed(2) + "%" + {displayedData[index]?.sizeVolRatio > 0.01 + ? displayedData[index]?.sizeVolRatio?.toFixed(2) + "%" : "< 0.01%"}
@@ -473,8 +473,8 @@ style="justify-content: center;" class="td text-sm sm:text-[1rem] text-white text-end" > - {displayedData[index]?.avgVolume > 0.01 - ? displayedData[index]?.avgVolume?.toFixed(2) + "%" + {displayedData[index]?.sizeAvgVolRatio > 0.01 + ? displayedData[index]?.sizeAvgVolRatio?.toFixed(2) + "%" : "< 0.01%"}
diff --git a/src/routes/dark-pool-flow/+page.svelte b/src/routes/dark-pool-flow/+page.svelte index f40ef488..61f5dbdb 100644 --- a/src/routes/dark-pool-flow/+page.svelte +++ b/src/routes/dark-pool-flow/+page.svelte @@ -7,7 +7,7 @@ isOpen, } from "$lib/store"; - import { cn } from "$lib/utils"; + import { cn, sectorList } from "$lib/utils"; import { onMount, onDestroy } from "svelte"; import toast from "svelte-french-toast"; import { DateFormatter, type DateValue } from "@internationalized/date"; @@ -25,10 +25,11 @@ export let data; let shouldLoadWorker = writable(false); - let ruleOfList = data?.getPredefinedCookieRuleOfList || []; + let ruleOfList = []; let displayRules = []; let filteredData = []; let filterQuery = $page.url.searchParams.get("query") || ""; + let pagePathName = $page?.url?.pathname; let socket: WebSocket | null = null; // Initialize socket as null @@ -55,7 +56,19 @@ }, volume: { label: "Volume", - step: ["100K", "50K", "20K", "10K", "5K", "2K", "1K", "100", "0"], + step: ["100M", "50M", "20M", "10M", "1M", "500K", "200K", "100K", "50K"], + defaultCondition: "over", + defaultValue: "any", + }, + sizeVolRatio: { + label: "Size / Volume", + step: ["20%", "15%", "10%", "5%", "3%", "1%"], + defaultCondition: "over", + defaultValue: "any", + }, + sizeAvgVolRatio: { + label: "Size / Avg Volume", + step: ["20%", "15%", "10%", "5%", "3%", "1%"], defaultCondition: "over", defaultValue: "any", }, @@ -77,27 +90,19 @@ defaultCondition: "over", defaultValue: "any", }, - option_activity_type: { - label: "Option Type", - step: ["Sweep", "Trade"], - defaultValue: "any", - }, assetType: { label: "Asset Type", step: ["Stock", "ETF"], defaultValue: "any", }, + sector: { + label: "Sector", + step: sectorList, + defaultValue: "any", + }, }; - const categoricalRules = [ - "moneyness", - "flowType", - "put_call", - "sentiment", - "execution_estimate", - "option_activity_type", - "assetType", - ]; + const categoricalRules = ["assetType", "sector"]; // Generate allRows from allRules $: allRows = Object?.entries(allRules) @@ -121,13 +126,6 @@ } }); - // 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) { @@ -147,7 +145,7 @@ ruleOfList?.some((rule) => rule.name === row.rule), ); shouldLoadWorker.set(true); - //await saveCookieRuleOfList(); + saveRules(); } async function handleResetAll() { @@ -165,7 +163,7 @@ ruleOfList.some((rule) => rule.name === row.rule), ); displayedData = rawData; - //await saveCookieRuleOfList(); + saveRules(); } function changeRule(state: string) { @@ -380,7 +378,7 @@ // Trigger worker load and save cookie shouldLoadWorker.set(true); - //await saveCookieRuleOfList(); + saveRules(); } async function stepSizeValue(value, condition) { @@ -534,18 +532,13 @@ } */ - async function saveCookieRuleOfList() { - const postData = { - ruleOfList: ruleOfList, - }; - - const response = await fetch("/api/options-flow-filter-cookie", { - method: "POST", - headers: { - "Content-Type": "application/json", - }, - body: JSON.stringify(postData), - }); // make a POST request to the server with the FormData object + function saveRules() { + try { + // Save the version along with the rules + localStorage?.setItem(pagePathName, JSON?.stringify(ruleOfList)); + } catch (e) { + console.log("Failed saving indicator rules: ", e); + } } /* @@ -555,10 +548,36 @@ */ onMount(async () => { + try { + const savedRules = localStorage?.getItem(pagePathName); + + if (savedRules) { + const parsedRules = JSON.parse(savedRules); + // Compare and update ruleOfList based on allRows + ruleOfList = parsedRules.map((rule) => { + const matchingRow = allRows.find((row) => row.name === rule.name); + if (matchingRow && matchingRow.type !== rule.type) { + return { ...rule, type: matchingRow.type }; + } + return rule; + }); + } + } catch (e) { + ruleOfList = []; + console.warn(e); + } + + // 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; + }); + if (filterQuery?.length > 0) { shouldLoadWorker.set(true); } - if (ruleOfList?.length !== 0) { + if (ruleOfList?.length > 0) { shouldLoadWorker.set(true); console.log("initial filter"); } diff --git a/src/routes/dark-pool-flow/workers/filterWorker.ts b/src/routes/dark-pool-flow/workers/filterWorker.ts index 543527ea..2f60e3e5 100644 --- a/src/routes/dark-pool-flow/workers/filterWorker.ts +++ b/src/routes/dark-pool-flow/workers/filterWorker.ts @@ -1,14 +1,13 @@ +import { sectorList } from "$lib/utils"; + interface FilterContext { flowTypeCache?: Map; } const categoricalFields = [ - 'put_call', - 'sentiment', - 'execution_estimate', - 'option_activity_type', - 'underlying_type' + 'assetType', + 'sector', ]; function convertUnitToValue( @@ -22,26 +21,14 @@ function convertUnitToValue( ); } const lowerInput = input?.toLowerCase(); - const nonNumericValues = new Set([ - "any", - "puts", - "calls", - "bullish", - "neutral", - "bearish", - "at bid", - "at ask", - "at midpoint", - "above ask", - "below bid", - "sweep", - "trade", - "stock", - "etf", - "itm", - "otm", - "repeated flow" - ]); +const nonNumericValues = new Set([ + "any", + "stock", + "etf", + ...sectorList.map(sector => sector.toLowerCase()), +]); + + if (nonNumericValues.has(lowerInput)) return input; if (input.endsWith("%")) { const numericValue = parseFloat(input.slice(0, -1)); @@ -64,80 +51,18 @@ function convertUnitToValue( 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; -} - - function createRuleCheck(rule, ruleName, ruleValue) { - const now = new Date(new Date().toLocaleString("en-US", { timeZone: "America/New_York" })); +if (rule.value === 'any') return () => true; - -if (ruleName === 'flowtype') { - return (item: any, context: FilterContext = {}) => { - // Check for 'any' rule or other non-repeated flow conditions - if (ruleValue === 'any' || ruleValue?.includes("any")) { - return true; - } - - // Handle Repeated Flow logic - if (ruleValue?.includes('Repeated Flow')) { - // Initialize flowTypeCache if it doesn't exist - context.flowTypeCache = context.flowTypeCache || new Map(); - - // Create a unique key for repeated flow based on item characteristics - const key = `${item.ticker}-${item.put_call}-${item.strike_price}-${item.date_expiration}`; - - // Increment the count for the key in the flowTypeCache - const currentCount = (context.flowTypeCache.get(key) || 0) + 1; - context.flowTypeCache.set(key, currentCount); - - // Return true if this flow appears more than N times (3 in this case) - return currentCount > 3; - } - - // Fallback for other flow type conditions (i.e., non-repeated flow) - return true; - }; -} - - - if (ruleName === 'moneyness') { - return (item) => { - if (ruleValue === 'any' || ruleValue?.includes("any")) return true; - - const currentPrice = parseFloat(item?.underlying_price); - const strikePrice = parseFloat(item?.strike_price); - const optionType = item?.put_call; - if (isNaN(currentPrice) || isNaN(strikePrice)) return false; - - // Determine moneyness - let moneyness = ''; - if (optionType === 'Calls') { - moneyness = currentPrice > strikePrice ? 'ITM' : 'OTM'; - } else if (optionType === 'Puts') { - moneyness = currentPrice < strikePrice ? 'ITM' : 'OTM'; - } - // Check if the item matches the ruleValue ('itm' or 'otm') - if (!ruleValue?.includes(moneyness)) return false; - return true; - }; - } - -if (ruleName === 'volumeoiratio') { +if (['sizeVolRatio','sizeAvgVolRatio']?.includes(ruleName)) { return (item) => { - const volume = parseFloat(item.volume); - const openInterest = parseFloat(item.open_interest); - if (isNaN(volume) || isNaN(openInterest) || openInterest === 0) { + if (isNaN(ruleValue) || ruleValue === 0) { return false; } - const ratio = Math.ceil((volume / openInterest) * 100); + const ratio = rule?.value; // Handle 'between' condition for volume to open interest ratio if (rule.condition === 'between' && Array.isArray(ruleValue)) { @@ -161,125 +86,15 @@ if (ruleName === 'volumeoiratio') { } -if (ruleName === 'sizeoiratio') { - return (item) => { - const size = parseFloat(item?.size); - const openInterest = parseFloat(item?.open_interest); - if (isNaN(size) || isNaN(openInterest) || openInterest === 0) { - return false; - } - const ratio = Math?.ceil((size / openInterest) * 100); - - // Handle 'between' condition for size to open interest ratio - if (rule.condition === 'between' && Array.isArray(ruleValue)) { - const [minRatio, maxRatio] = ruleValue?.map(convertUnitToValue); // Convert ruleValue to numeric values - - if (minRatio === null && maxRatio === null) return true; - if (minRatio === null) return ratio <= maxRatio; - if (maxRatio === null) return ratio >= minRatio; - - return ratio >= minRatio && ratio <= maxRatio; - } - - // Existing conditions for 'over' and 'under' - if (rule.condition === 'over' && ratio <= ruleValue) return false; - if (rule.condition === 'under' && ratio >= ruleValue) return false; - if (rule.condition === 'exactly' && ratio !== ruleValue) return false; - - - return true; - }; -} - - - if (ruleName === 'date_expiration') { - // If ruleValue is empty, undefined, "any", or an array containing only "any", return a function that always returns true - if (ruleValue === "" || ruleValue === undefined || isAny(ruleValue)) { - return () => true; - } - - - return (item) => { - const expirationDate = new Date(item[rule.name]); - if (isNaN(expirationDate)) return false; // Handle invalid dates - - const daysDiff = Math?.ceil((expirationDate.getTime() - now.getTime()) / (1000 * 60 * 60 * 24)); - - - if (rule.condition === 'between' && Array.isArray(ruleValue)) { - const [minDays, maxDays] = ruleValue.map(val => - val === '' || val === null || val === undefined ? null : parseFloat(val.toString()) - ); - - if (minDays === null && maxDays === null) return true; - if (minDays === null) return daysDiff <= maxDays; - if (maxDays === null) return daysDiff >= minDays; - - return daysDiff >= minDays && daysDiff <= maxDays; - } - - if (rule.condition === 'over' && typeof ruleValue === 'number') { - return daysDiff >= ruleValue; - } - - if (rule.condition === 'under' && typeof ruleValue === 'number') { - return daysDiff <= ruleValue; - } - - if (rule.condition === 'exactly' && typeof ruleValue === 'number') { - return daysDiff === ruleValue; - } - - - return false; - }; -} // Handle string-based conditions (sentiment, option_type, etc.) - if (categoricalFields?.includes(ruleName)) { + if (categoricalFields.includes(rule.name)) { return (item) => { - // If ruleValue is empty, undefined, or "any", return true for all items - if (ruleValue === "" || ruleValue === undefined || isAny(ruleValue)) { - return true; - } - const itemValue = item[rule.name]; - - // Handle array of values for categorical fields - if (Array?.isArray(ruleValue)) { - // Remove any empty or undefined values from ruleValue - const validRuleValues = ruleValue?.filter(val => val !== "" && val !== undefined); - - // If no valid values remain, return true for all items - if (validRuleValues.length === 0) { - return true; - } - - // If itemValue is an array, check if any of the values match - if (Array?.isArray(itemValue)) { - return validRuleValues?.some(val => - itemValue.some(iv => iv.toLowerCase() === val.toLowerCase()) - ); - } - - // If itemValue is a string, check if it's in the validRuleValues array - return validRuleValues?.some(val => - itemValue?.toLowerCase() === val.toLowerCase() - ); + if (Array.isArray(ruleValue)) { + return ruleValue.includes(itemValue); } - - // Handle single string value - if (typeof ruleValue === 'string') { - // If itemValue is an array, check if any value matches - if (Array?.isArray(itemValue)) { - return itemValue?.some(iv => iv.toLowerCase() === ruleValue.toLowerCase()); - } - - // If both are strings, do a direct comparison - return itemValue?.toLowerCase() === ruleValue.toLowerCase(); - } - - return false; + return itemValue === ruleValue; }; }