diff --git a/src/routes/stock-screener/+page.svelte b/src/routes/stock-screener/+page.svelte index f47ff9ef..c5d816b3 100644 --- a/src/routes/stock-screener/+page.svelte +++ b/src/routes/stock-screener/+page.svelte @@ -1374,7 +1374,7 @@ // Check if the default condition is "between" if (allRules[ruleName].defaultCondition === "between") { - valueMappings[ruleName] = allRules[ruleName].defaultValue || ["", ""]; + valueMappings[ruleName] = allRules[ruleName].defaultValue || [null, null]; } else { valueMappings[ruleName] = allRules[ruleName].defaultValue; } @@ -1883,28 +1883,54 @@ const handleKeyDown = (event) => { return checkedItems?.has(ruleName) && checkedItems?.get(ruleName).has(item); } + // Utility function to convert values to comparable numbers + function parseValue(val) { + if (typeof val === "string") { + // Handle percentage values + if (val.endsWith("%")) { + return parseFloat(val); + } + + // Handle values with suffixes like K (thousand), M (million), B (billion) + const suffixMap = { + K: 1e3, + M: 1e6, + B: 1e9, + }; + + const suffix = val.slice(-1).toUpperCase(); + const numberPart = parseFloat(val); + + if (suffix in suffixMap) { + return numberPart * suffixMap[suffix]; + } + } + + return parseFloat(val); + } + + // Custom sorting function + function customSort(a, b) { + return parseValue(a) - parseValue(b); + } + + // Main function async function handleChangeValue(value) { if (checkedItems.has(ruleName)) { const itemsSet = checkedItems.get(ruleName); - // For "between", value is expected to be an array [min, max] - const sortedValue = Array.isArray(value) - ? value.sort((a, b) => a - b) - : value; + const sortedValue = Array.isArray(value) ? value.sort(customSort) : value; const valueKey = Array.isArray(sortedValue) ? sortedValue.join("-") : sortedValue; if (itemsSet?.has(valueKey)) { - itemsSet?.delete(valueKey); // Remove the value if it's already in the set + itemsSet?.delete(valueKey); } else { - itemsSet?.add(valueKey); // Add the value if it's not in the set + itemsSet?.add(valueKey); } } else { - // If the ruleName is not in checkedItems, create a new set for this rule - const sortedValue = Array.isArray(value) - ? value.sort((a, b) => a - b) - : value; + const sortedValue = Array.isArray(value) ? value.sort(customSort) : value; const valueKey = Array.isArray(sortedValue) ? sortedValue.join("-") : sortedValue; @@ -1932,39 +1958,34 @@ const handleKeyDown = (event) => { ) { searchQuery = ""; - // Ensure valueMappings[ruleName] is initialized as an array if (!Array.isArray(valueMappings[ruleName])) { valueMappings[ruleName] = []; } - const sortedValue = Array.isArray(value) - ? value.sort((a, b) => a - b) + const sortedValue = Array?.isArray(value) + ? value?.sort(customSort) : value; - const valueKey = Array.isArray(sortedValue) + const valueKey = Array?.isArray(sortedValue) ? sortedValue.join("-") : sortedValue; const index = valueMappings[ruleName].indexOf(valueKey); if (index === -1) { - // Add the value if it's not already selected valueMappings[ruleName].push(valueKey); } else { - // Remove the value if it's already selected valueMappings[ruleName].splice(index, 1); } - // If no values are selected, set the value to "any" if (valueMappings[ruleName].length === 0) { valueMappings[ruleName] = "any"; } await updateStockScreenerData(); } else if (ruleName in valueMappings) { - // Handle "over", "under", and "between" conditions - if (ruleCondition[ruleName] === "between" && Array.isArray(value)) { - valueMappings[ruleName] = value.sort((a, b) => a - b); // Ensure lower value is first + if (ruleCondition[ruleName] === "between" && Array?.isArray(value)) { + valueMappings[ruleName] = value?.sort(customSort); } else { - valueMappings[ruleName] = value; // Store single values for "over" and "under" + valueMappings[ruleName] = value; } } else { console.warn(`Unhandled rule: ${ruleName}`); @@ -1988,14 +2009,17 @@ const handleKeyDown = (event) => { await handleChangeValue(newValue); } - async function handleValueInput(event) { + async function handleValueInput(event, ruleName, index = null) { const newValue = event.target.value; - if (newValue?.length > 0) { - console.log("yes"); + + if (ruleCondition[ruleName] === "between") { + const currentValues = valueMappings[ruleName] || ["", ""]; + currentValues[index] = newValue; + await handleChangeValue(currentValues); + } else { await handleChangeValue(newValue); } } - async function popularStrategy(state: string) { ruleOfList = []; const strategies = { @@ -2757,12 +2781,15 @@ const handleKeyDown = (event) => { {#if valueMappings[row?.rule] === "any"} Any - {:else} + {:else if ["under", "over"]?.includes(ruleCondition[row?.rule])} {ruleCondition[row?.rule] ?.replace("under", "Under") - ?.replace("over", "Over") - ?.replace("between", "Between")} + ?.replace("over", "Over")} {valueMappings[row?.rule]} + {:else if ruleCondition[row?.rule] === "between"} + {Array.isArray(valueMappings[row?.rule]) + ? `${valueMappings[row?.rule][0]}-${valueMappings[row?.rule][1] ?? "Any"}` + : "Any"} {/if} { {#if !["sma20", "sma50", "sma100", "sma200", "ema20", "ema50", "ema100", "ema200", "grahamNumber", "analystRating", "halalStocks", "score", "sector", "industry", "country"]?.includes(row?.rule)} { -
+ + 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"]?.includes(ruleCondition[ruleName]?.toLowerCase())}
{ {#if !["sma20", "sma50", "sma100", "sma200", "ema20", "ema50", "ema100", "ema200", "grahamNumber", "analystRating", "halalStocks", "score", "sector", "industry", "country"]?.includes(row?.rule)} {#each row?.step as newValue, index} - {#if ruleCondition[row?.rule] === "between" && newValue && row?.step[index + 1]} + {#if ruleCondition[row?.rule] === "between"} + {#if newValue && row?.step[index + 1]} + + + + {/if} + {:else} {/if} - {:else} - - - {/each} {:else if ["sma20", "sma50", "sma100", "sma200", "ema20", "ema50", "ema100", "ema200", "grahamNumber"]?.includes(row?.rule)} {#each row?.step as item} diff --git a/src/routes/stock-screener/workers/filterWorker.ts b/src/routes/stock-screener/workers/filterWorker.ts index 13826d37..e10125a6 100644 --- a/src/routes/stock-screener/workers/filterWorker.ts +++ b/src/routes/stock-screener/workers/filterWorker.ts @@ -43,139 +43,215 @@ const movingAverageConditions = { }; // Convert the input to a value or return it as-is if it's already an array -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", - ...sectorList, - ...industryList, - ...listOfRelevantCountries, - "hold", - "sell", - "buy", - "strong buy", - "strong sell", - "compliant", - "non-compliant", - "stock price", - ]); - 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`); +function convertUnitToValue(input: string | number | string[]) { + try { + if (Array.isArray(input)) { + return input.map(convertUnitToValue); // Recursively convert array elements } + if (typeof input === "number") return input; + if (typeof input !== "string") { + return input; // Return as-is if not a string or number + } + + const lowerInput = input.toLowerCase(); + + // Pre-compute the set for quick lookups + const nonNumericValues = new Set([ + "any", + ...sectorList, + ...industryList, + ...listOfRelevantCountries, + "hold", + "sell", + "buy", + "strong buy", + "strong sell", + "compliant", + "non-compliant", + "stock price", + ]); + + if (nonNumericValues.has(lowerInput)) return input; + + // Handle percentage values + if (input.endsWith("%")) { + const numericValue = parseFloat(input.slice(0, -1)); // Remove '%' and convert to number + if (isNaN(numericValue)) { + return input; // Return original input if conversion fails + } + return numericValue / 100; // Convert percentage to a decimal + } + + // Handle units (B, M, K) + 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; + } + + // Default numeric conversion (if no unit specified) + const numericValue = parseFloat(input); + if (isNaN(numericValue)) { + return input; // Return original input if conversion fails + } + return numericValue; + } catch (error) { + console.warn(`Error converting value: ${input}`, error); + return input; // Return original input in case of any unexpected errors } - - 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; } - -// Filter the stock screener data based on the provided rules async function filterStockScreenerData(stockScreenerData, ruleOfList) { - return stockScreenerData?.filter((item) => { - return ruleOfList.every((rule) => { - const itemValue = item[rule.name]; - const ruleValue = convertUnitToValue(rule.value); - const ruleName = rule.name.toLowerCase(); + try { + return stockScreenerData?.filter((item) => { + return ruleOfList.every((rule) => { + try { + const itemValue = item[rule.name]; + const ruleValue = convertUnitToValue(rule.value); + const ruleName = rule.name.toLowerCase(); - // Handle trend and fundamental analysis - if (["trendAnalysis", "fundamentalAnalysis"].includes(rule.name)) { - const accuracy = item[rule.name]?.accuracy; - if (rule.condition === "over" && accuracy <= ruleValue) return false; - if (rule.condition === "under" && accuracy > ruleValue) return false; - } + // If ruleValue is the original input (conversion failed), + // we'll treat it as a special case + if (typeof ruleValue === "string") { + // For most string inputs, we'll consider it a match + if (rule.value === "any") return true; - // Handle categorical data like analyst ratings, sector, country - else if ( - [ - "analystRating", - "halalStocks", - "score", - "sector", - "industry", - "country", - ].includes(rule.name) - ) { - if (rule.value === "any") return true; + // For specific categorical checks + if ( + [ + "analystRating", + "halalStocks", + "score", + "sector", + "industry", + "country", + ].includes(rule.name) + ) { + if (Array.isArray(ruleValue) && !ruleValue.includes(itemValue)) + return false; + if (!Array.isArray(ruleValue) && itemValue !== ruleValue) return false; + } - if (Array.isArray(ruleValue) && !ruleValue.includes(itemValue)) - return false; - if (!Array.isArray(ruleValue) && itemValue !== ruleValue) return false; - } - - // Handle moving averages - else if ( - [ - "ema20", - "ema50", - "ema100", - "ema200", - "sma20", - "sma50", - "sma100", - "sma200", - "grahamnumber", //grahamNumber into lowerCase form - ].includes(ruleName) - ) { - if (ruleValue === "any") return true; - - for (const condition of ruleValue) { - if (movingAverageConditions[condition]) { - if (!movingAverageConditions[condition](item)) return false; - } else { - //console.warn(`Unknown condition: ${condition}`); + // For other cases, we'll skip filtering + return true; } + + // Handle categorical data like analyst ratings, sector, country + if ( + [ + "analystRating", + "halalStocks", + "score", + "sector", + "industry", + "country", + ].includes(rule.name) + ) { + if (rule.value === "any") return true; + + if (Array.isArray(ruleValue) && !ruleValue.includes(itemValue)) + return false; + if (!Array.isArray(ruleValue) && itemValue !== ruleValue) return false; + } + + // Handle moving averages + else if ( + [ + "ema20", + "ema50", + "ema100", + "ema200", + "sma20", + "sma50", + "sma100", + "sma200", + "grahamnumber", // grahamNumber into lowerCase form + ].includes(ruleName) + ) { + if (ruleValue === "any") return true; + + for (const condition of ruleValue) { + if (movingAverageConditions[condition]) { + if (!movingAverageConditions[condition](item)) return false; + } else { + // console.warn(`Unknown condition: ${condition}`); + } + } + + return true; // If all conditions are met + } + + // Handle "between" condition + else if (rule.condition === "between" && Array?.isArray(ruleValue)) { + // 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) { + if (itemValue >= max) return false; // If min is missing, only check against max + } else if (max === "" || max === undefined || max === null) { + if (itemValue <= min) return false; // If max is missing, only check against min + } else { + // If both min and max are defined, proceed with the normal comparison + if (itemValue <= min || itemValue >= max) return false; + } + } + + // Default numeric or string comparison + else if (typeof ruleValue === "string") { + return true; // Skip non-numeric comparisons + } else if (itemValue === null) { + return false; // Null values do not meet any condition + } else if (rule.condition === "over" && itemValue <= ruleValue) { + return false; + } else if (rule.condition === "under" && itemValue > ruleValue) { + return false; + } + + return true; + } catch (ruleError) { + console.warn(`Error processing rule for item:`, rule, ruleError); + return true; // Default to including the item if rule processing fails } - - return true; // If all conditions are met - } - - // 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; - }); - }); + }); + }) || stockScreenerData; // Return original data if filtering completely fails + } catch (error) { + console.error('Error in filterStockScreenerData:', error); + return stockScreenerData; // Return original data if any catastrophic error occurs + } } onmessage = async (event: MessageEvent) => { const { stockScreenerData, ruleOfList } = event.data || {}; - const filteredData = await filterStockScreenerData( - stockScreenerData, - ruleOfList - ); + try { + const filteredData = await filterStockScreenerData( + stockScreenerData, + ruleOfList + ); - postMessage({ message: "success", filteredData }); + postMessage({ + message: "success", + filteredData, + originalDataLength: stockScreenerData?.length || 0, + filteredDataLength: filteredData?.length || 0 + }); + } catch (error) { + console.error('Error in onmessage handler:', error); + postMessage({ + message: "error", + originalData: stockScreenerData, + error: error.toString() + }); + } }; -export {}; +export {}; \ No newline at end of file