From 89b60b4d969dad36905647a9ea183289c244f877 Mon Sep 17 00:00:00 2001 From: MuslemRahimi Date: Tue, 24 Dec 2024 23:48:22 +0100 Subject: [PATCH] ui fix --- src/lib/components/Searchbar.svelte | 6 +- src/routes/dark-pool-flow/+page.server.ts | 56 + src/routes/dark-pool-flow/+page.svelte | 1883 +++++++++++++++++ .../dark-pool-flow/workers/filterWorker.ts | 376 ++++ 4 files changed, 2318 insertions(+), 3 deletions(-) create mode 100644 src/routes/dark-pool-flow/+page.server.ts create mode 100644 src/routes/dark-pool-flow/+page.svelte create mode 100644 src/routes/dark-pool-flow/workers/filterWorker.ts diff --git a/src/lib/components/Searchbar.svelte b/src/lib/components/Searchbar.svelte index 7fb17e44..43f77ffc 100644 --- a/src/lib/components/Searchbar.svelte +++ b/src/lib/components/Searchbar.svelte @@ -319,7 +319,7 @@ @@ -351,7 +351,7 @@ @@ -370,7 +370,7 @@ {/each} - {:else if inputValue?.length === 0} + {:else if inputValue?.length === 0 && searchHistory?.length > 0} {#each searchHistory as item} { + const { apiURL, apiKey, pb, user } = locals; + + const getOptionsFlowFeed = async () => { + // make the POST request to the endpoint + const response = await fetch(apiURL + "/options-flow-feed", { + method: "GET", + headers: { + "Content-Type": "application/json", + "X-API-KEY": apiKey, + }, + }); + const output = await response.json(); + + return output; + }; + + const getPredefinedCookieRuleOfList = async () => { + // make the POST request to the endpoint + const ruleOfList = cookies.get("options-flow-filter-cookie") ?? []; + const output = + ruleOfList?.length !== 0 + ? JSON.parse(ruleOfList) + : [ + { name: "cost_basis", value: "any" }, + { name: "date_expiration", value: "any" }, + ]; + + 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(), + }; +}; diff --git a/src/routes/dark-pool-flow/+page.svelte b/src/routes/dark-pool-flow/+page.svelte new file mode 100644 index 00000000..b1627042 --- /dev/null +++ b/src/routes/dark-pool-flow/+page.svelte @@ -0,0 +1,1883 @@ + + + + + + + + {$numberOfUnreadNotification > 0 ? `(${$numberOfUnreadNotification})` : ""} Options + Flow Feed · Stocknear + + + + + + + + + + + + + + + + + +
+
+ + + {#if !$isOpen} +
+ Live flow of {data?.user?.tier === "Pro" && selectedDate + ? df.format(selectedDate?.toDate()) + : nyseDate} (NYSE Time) +
+ {/if} + +
+
+
+ + + + {$isOpen ? "Paused" : "Market Closed"} + + + + +
+ + Live Flow + +
+
+ +
+
+
+ + {#if notFound === true} + + No Results Found + + {/if} +
+ + + + + + + + + +
+
+
+ +
+ +
+ + {#if showFilters} +
+ + + {#if ruleOfList?.length !== 0} + + {/if} +
+ +
+ {#each displayRules as row (row?.rule)} + +
+
+ {row?.label?.length > 20 + ? row?.label?.slice(0, 20)?.replace("[%]", "") + "..." + : row?.label?.replace("[%]", "")} + +
+ +
+ +
+
(ruleName = row?.rule)}> + + + + + + {#if !categoricalRules?.includes(row?.rule)} + +
+ +
+ + + + + + {#each ["Over", "Under", "Between", "Exactly"] as item} + + changeRuleCondition( + row?.rule, + item, + )} + class="cursor-pointer text-[1rem] font-normal" + >{item} + {/each} + + + +
+ + {#if ruleCondition[row?.rule] === "between"} +
+ + handleValueInput(e, row?.rule, 0)} + class="ios-zoom-fix block max-w-[3.5rem] rounded-sm placeholder:text-gray-200 font-normal p-1 text-sm shadow-sm focus:border-blue-500 focus:ring-blue-500 bg-secondary" + /> + + & + + + handleValueInput(e, row?.rule, 1)} + class="ios-zoom-fix block max-w-[3.5rem] rounded-sm placeholder:text-gray-200 font-normal p-1 text-sm shadow-sm focus:border-blue-500 focus:ring-blue-500 bg-secondary" + /> +
+ {:else} + + handleValueInput(e, row?.rule)} + class="ios-zoom-fix block max-w-[4.8rem] rounded-sm placeholder:text-gray-200 font-normal p-1 text-sm shadow-sm focus:border-blue-500 focus:ring-blue-500 bg-secondary" + /> + {/if} + + {#if ["over", "under", "exactly"]?.includes(ruleCondition[ruleName]?.toLowerCase())} +
+ + +
+ {/if} + +
+
+ {:else} + + {/if} + + {#if !categoricalRules?.includes(row?.rule)} + {#each row?.step as newValue, index} + {#if ruleCondition[row?.rule] === "between"} + {#if newValue && row?.step[index + 1]} + + + + {/if} + {:else} + + + + {/if} + {/each} + {:else if categoricalRules?.includes(row?.rule)} + {#each row?.step as item} + +
+ event.preventDefault()} + > + +
+
+ {/each} + {/if} +
+
+
+
+
+
+
+ + {/each} +
+ {/if} +
+ + {#if isLoaded} +
+
+ +
+
+ Flow Sentiment + {flowSentiment} +
+
+ + +
+
+ Put/Call + + {putCallRatio?.toFixed(3)} + +
+ +
+ + + + + + = 1 + ? 0 + : 100 - (putCallRatio * 100)?.toFixed(2)} + > + + + +
+ {putCallRatio?.toFixed(2)} +
+
+ +
+ + +
+
+ Call Flow + + {new Intl.NumberFormat("en", { + minimumFractionDigits: 0, + maximumFractionDigits: 0, + }).format(displayCallVolume)} + +
+ +
+ + + + + + + + + +
+ {callPercentage}% +
+
+ +
+ + +
+
+ Put Flow + + {new Intl.NumberFormat("en", { + minimumFractionDigits: 0, + maximumFractionDigits: 0, + }).format(displayPutVolume)} + +
+ +
+ + + + + + + + + +
+ {putPercentage}% +
+
+ +
+
+
+ + +
+ {#if displayedData?.length !== 0} +
+ +
+ {:else} +
+ + Looks like your taste is one-of-a-kind! No matches found... yet! +
+ {/if} +
+ + + {:else} +
+
+ +
+
+ {/if} +
+
+ + + + + + + + + + + + + + + + + + + + + + diff --git a/src/routes/dark-pool-flow/workers/filterWorker.ts b/src/routes/dark-pool-flow/workers/filterWorker.ts new file mode 100644 index 00000000..7a12741e --- /dev/null +++ b/src/routes/dark-pool-flow/workers/filterWorker.ts @@ -0,0 +1,376 @@ + +interface FilterContext { + flowTypeCache?: Map; +} + + const categoricalFields = [ + 'put_call', + 'sentiment', + 'execution_estimate', + 'option_activity_type', + 'underlying_type' + ]; + +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(); + 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" + ]); + 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; +} + + + +function createRuleCheck(rule, ruleName, ruleValue) { + const now = new Date(new Date().toLocaleString("en-US", { timeZone: "America/New_York" })); + + +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') { + return (item) => { + const volume = parseFloat(item.volume); + const openInterest = parseFloat(item.open_interest); + + if (isNaN(volume) || isNaN(openInterest) || openInterest === 0) { + return false; + } + + const ratio = Math.ceil((volume / openInterest) * 100); + + // Handle 'between' condition for volume 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 === '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)) { + 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() + ); + } + + // 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; + }; + } + +// 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' + + if (itemValue === null || itemValue === undefined) return false; + + const numericItemValue = parseFloat(itemValue); + + + if (isNaN(numericItemValue)) return false; + + // Handle 'between' condition for numeric fields using convertUnitToValue + if (rule.condition === 'between' && Array.isArray(ruleValue)) { + const [minValue, maxValue] = ruleValue.map(convertUnitToValue); + + if (minValue === null && maxValue === null) return true; + if (minValue === null) return numericItemValue <= maxValue; + if (maxValue === null) return numericItemValue >= minValue; + + return numericItemValue >= minValue && numericItemValue <= maxValue; + } + + // Existing conditions + if (rule.condition === 'exactly' && numericItemValue !== ruleValue) return false; + if (rule.condition === 'over' && numericItemValue <= ruleValue) return false; + if (rule.condition === 'under' && numericItemValue >= ruleValue) return false; + + + + return true; +}; + + +} + + +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()) + : []; + + // Initialize context with optional flowTypeCache + const context: FilterContext = { + flowTypeCache: new Map() + }; + + // 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, passing the context + return compiledRules?.every(rule => rule?.compiledCheck(item, context)); + }); +} + +// Web Worker message handler remains the same +onmessage = async (event: MessageEvent) => { + const { rawData, ruleOfList, filterQuery } = event.data || {}; + // Filter the data + let filteredData = await filterRawData(rawData, ruleOfList, filterQuery); + + filteredData = Array.from( + new Map(filteredData?.map((item) => [item?.id, item]))?.values() + ); + + postMessage({ message: "success", filteredData }); +}; + +export {}; \ No newline at end of file