diff --git a/src/routes/options-flow/+page.svelte b/src/routes/options-flow/+page.svelte index 7f58e3a8..63f6fd36 100644 --- a/src/routes/options-flow/+page.svelte +++ b/src/routes/options-flow/+page.svelte @@ -95,7 +95,7 @@ }, execution_estimate: { label: "Execution", - step: ["At Ask", "At Bid", "At Midpoint", "Above Ask", "Below Bid"], + step: ["Above Ask", "Below Bid", "At Ask", "At Bid", "At Midpoint"], defaultValue: "any", }, option_activity_type: { @@ -105,17 +105,8 @@ }, date_expiration: { label: "Date Expiration", - step: [ - "Same Day", - "1 day", - "1 Week", - "2 Weeks", - "1 Month", - "3 Months", - "6 Months", - "1 Year", - "3 Years", - ], + step: ["250", "180", "100", "80", "60", "50", "30", "20", "10", "5", "0"], + defaultCondition: "over", defaultValue: "any", }, underlying_type: { @@ -218,7 +209,6 @@ case "sentiment": case "execution_estimate": case "option_activity_type": - case "date_expiration": case "underlying_type": newRule = { name: ruleName, @@ -350,7 +340,6 @@ "sentiment", "execution_estimate", "option_activity_type", - "date_expiration", "underlying_type", ]?.includes(ruleName) ) { @@ -1135,7 +1124,7 @@ function sendMessage(message) { - {#if !["put_call", "sentiment", "execution_estimate", "option_activity_type", "date_expiration", "underlying_type"]?.includes(row?.rule)} + {#if !["put_call", "sentiment", "execution_estimate", "option_activity_type", "underlying_type"]?.includes(row?.rule)} @@ -1295,7 +1284,7 @@ function sendMessage(message) { > {/if} - {#if !["put_call", "sentiment", "execution_estimate", "option_activity_type", "date_expiration", "underlying_type"]?.includes(row?.rule)} + {#if !["put_call", "sentiment", "execution_estimate", "option_activity_type", "underlying_type"]?.includes(row?.rule)} {#each row?.step as newValue, index} {#if ruleCondition[row?.rule] === "between"} {#if newValue && row?.step[index + 1]} @@ -1339,7 +1328,7 @@ function sendMessage(message) { {/if} {/each} - {:else if ["put_call", "sentiment", "execution_estimate", "option_activity_type", "date_expiration", "underlying_type"]?.includes(row?.rule)} + {:else if ["put_call", "sentiment", "execution_estimate", "option_activity_type", "underlying_type"]?.includes(row?.rule)} {#each row?.step as item} = 0 && daysDiff <= 1; - case "1 week": - return daysDiff >= 0 && daysDiff <= 7; - case "2 weeks": - return daysDiff >= 0 && daysDiff <= 14; - case "1 month": - return daysDiff >= 0 && daysDiff <= 30; - case "3 months": - return daysDiff >= 0 && daysDiff <= 90; - case "6 months": - return daysDiff >= 0 && daysDiff <= 180; - case "1 year": - return daysDiff >= 0 && daysDiff <= 365; - case "3 years": - return daysDiff >= 0 && daysDiff <= 1095; - default: - return false; - } -} - -async function filterRawData(rawData, ruleOfList, filterQuery) { - // Early return for empty inputs - if (!rawData?.length ) { - return rawData || []; - } - - // Preprocess filter tickers - const filterTickers = filterQuery - ? filterQuery.split(",").map((ticker) => ticker.trim().toUpperCase()) - : []; - - // Precompile rules for more efficient filtering - const compiledRules = ruleOfList.map(rule => { - const ruleName = rule.name.toLowerCase(); - const ruleValue = convertUnitToValue(rule.value); - - return { - ...rule, - compiledCheck: createRuleCheck(rule, ruleName, ruleValue) - }; - }); - - // Optimized filtering with precompiled rules - return rawData.filter(item => { - // Early ticker filtering - if (filterTickers.length > 0 && - !filterTickers.includes(item.ticker.toUpperCase())) { - return false; - } - - // Apply all precompiled rules - return compiledRules.every(rule => rule.compiledCheck(item)); - }); -} - -// Centralized rule checking logic function createRuleCheck(rule, ruleName, ruleValue) { - // Handle volumeOIRatio - if (ruleName === 'volumeoiratio') { + const now = new Date(new Date().toLocaleString("en-US", { timeZone: "America/New_York" })); + + + if (ruleName === 'volumeoiratio') { return (item) => { const volume = parseFloat(item.volume); const openInterest = parseFloat(item.open_interest); @@ -190,99 +87,99 @@ function createRuleCheck(rule, ruleName, ruleValue) { }; } - // Handle "between" condition - if (rule.condition === 'between') { - return (item) => { - const itemValue = parseFloat(item[rule.name]); - - // Handle array of ruleValue for between condition - if (!Array.isArray(ruleValue)) return true; - - // Convert rule values, ensuring they are valid - const [min, max] = ruleValue.map(convertUnitToValue); - - // Handle the case where one or both values are missing (empty string or undefined) - if ((min === '' || min === undefined || min === null) && - (max === '' || max === undefined || max === null)) { - return true; // If both values are empty or undefined, consider the condition as met (open-ended) - } - - // If only one of min or max is missing, handle it as open-ended - if (min === '' || min === undefined || min === null) { - return itemValue <= max; // If min is missing, only check against max - } - - if (max === '' || max === undefined || max === null) { - return itemValue >= min; // If max is missing, only check against min - } - - // If both min and max are defined, proceed with the normal comparison - return itemValue > min && itemValue < max; - }; - } - - // Handle date_expiration if (ruleName === 'date_expiration') { - // If 'any' is specified or no specific range is set - if (isAny(ruleValue)) return () => true; - - return (item) => { - if (Array.isArray(ruleValue)) { - return ruleValue.some(range => isDateWithinRange(item[rule.name], range)); - } - return isDateWithinRange(item[rule.name], ruleValue); - }; + // 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; } - // Categorical data handling - const categoricalFields = [ - 'put_call', - 'sentiment', - 'execution_estimate', - 'option_activity_type', - 'underlying_type' - ]; + return (item) => { + const expirationDate = new Date(item[rule.name]); + if (isNaN(expirationDate)) return false; // Handle invalid dates - if (categoricalFields.includes(ruleName)) { - // If 'any' is specified - if (isAny(ruleValue)) return () => true; + const daysDiff = (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; + } + + return false; + }; +} + // Handle string-based conditions (sentiment, option_type, etc.) + if (categoricalFields?.includes(ruleName)) { 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 null or undefined - if (itemValue === null || itemValue === undefined) return false; - - const lowerItemValue = itemValue.toString().toLowerCase(); - - // Handle array of values + // Handle array of values for categorical fields if (Array.isArray(ruleValue)) { - return ruleValue.some( - value => lowerItemValue === value.toString().toLowerCase() + // 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() ); } - - // Single value comparison - return lowerItemValue === ruleValue.toString().toLowerCase(); + + // 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; }; } - // Default numeric comparison + // Fallback to other numeric conditions return (item) => { const itemValue = item[rule.name]; + if (typeof ruleValue === 'string') return true; // Handle cases where the rule value is a string but not 'sentiment' or 'option_type' - // Skip string rule values - if (typeof ruleValue === 'string') return true; - - // Handle null or undefined if (itemValue === null || itemValue === undefined) return false; const numericItemValue = parseFloat(itemValue); - - // Invalid numeric conversion if (isNaN(numericItemValue)) return false; - // Comparison conditions if (rule.condition === 'over' && numericItemValue <= ruleValue) return false; if (rule.condition === 'under' && numericItemValue >= ruleValue) return false; @@ -290,13 +187,47 @@ function createRuleCheck(rule, ruleName, ruleValue) { }; } + +async function filterRawData(rawData, ruleOfList, filterQuery) { + // Early return for empty inputs + if (!rawData?.length ) { + return rawData || []; + } + + // Preprocess filter tickers + const filterTickers = filterQuery + ? filterQuery.split(",").map((ticker) => ticker.trim().toUpperCase()) + : []; + + // Precompile rules for more efficient filtering + const compiledRules = ruleOfList.map(rule => { + const ruleName = rule?.name?.toLowerCase(); + const ruleValue = convertUnitToValue(rule.value); + + return { + ...rule, + compiledCheck: createRuleCheck(rule, ruleName, ruleValue) + }; + }); + + // Optimized filtering with precompiled rules + return rawData?.filter(item => { + // Early ticker filtering + if (filterTickers?.length > 0 && + !filterTickers?.includes(item.ticker.toUpperCase())) { + return false; + } + + // Apply all precompiled rules + return compiledRules.every(rule => rule?.compiledCheck(item)); + }); +} + // Web Worker message handler onmessage = async (event: MessageEvent) => { const { rawData, ruleOfList, filterQuery } = event.data || {}; - // Filter the data let filteredData = await filterRawData(rawData, ruleOfList, filterQuery); - // Remove duplicates based on id filteredData = Array.from( new Map(filteredData?.map((item) => [item?.id, item]))?.values()