fine tune screener with between rule

This commit is contained in:
MuslemRahimi 2024-12-09 01:03:55 +01:00
parent 2f9c4cf65a
commit 085d3c3abe
2 changed files with 324 additions and 179 deletions

View File

@ -1374,7 +1374,7 @@
// Check if the default condition is "between" // Check if the default condition is "between"
if (allRules[ruleName].defaultCondition === "between") { if (allRules[ruleName].defaultCondition === "between") {
valueMappings[ruleName] = allRules[ruleName].defaultValue || ["", ""]; valueMappings[ruleName] = allRules[ruleName].defaultValue || [null, null];
} else { } else {
valueMappings[ruleName] = allRules[ruleName].defaultValue; valueMappings[ruleName] = allRules[ruleName].defaultValue;
} }
@ -1883,28 +1883,54 @@ const handleKeyDown = (event) => {
return checkedItems?.has(ruleName) && checkedItems?.get(ruleName).has(item); 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) { async function handleChangeValue(value) {
if (checkedItems.has(ruleName)) { if (checkedItems.has(ruleName)) {
const itemsSet = checkedItems.get(ruleName); const itemsSet = checkedItems.get(ruleName);
// For "between", value is expected to be an array [min, max] const sortedValue = Array.isArray(value) ? value.sort(customSort) : value;
const sortedValue = Array.isArray(value)
? value.sort((a, b) => a - b)
: value;
const valueKey = Array.isArray(sortedValue) const valueKey = Array.isArray(sortedValue)
? sortedValue.join("-") ? sortedValue.join("-")
: sortedValue; : sortedValue;
if (itemsSet?.has(valueKey)) { if (itemsSet?.has(valueKey)) {
itemsSet?.delete(valueKey); // Remove the value if it's already in the set itemsSet?.delete(valueKey);
} else { } else {
itemsSet?.add(valueKey); // Add the value if it's not in the set itemsSet?.add(valueKey);
} }
} else { } else {
// If the ruleName is not in checkedItems, create a new set for this rule const sortedValue = Array.isArray(value) ? value.sort(customSort) : value;
const sortedValue = Array.isArray(value)
? value.sort((a, b) => a - b)
: value;
const valueKey = Array.isArray(sortedValue) const valueKey = Array.isArray(sortedValue)
? sortedValue.join("-") ? sortedValue.join("-")
: sortedValue; : sortedValue;
@ -1932,39 +1958,34 @@ const handleKeyDown = (event) => {
) { ) {
searchQuery = ""; searchQuery = "";
// Ensure valueMappings[ruleName] is initialized as an array
if (!Array.isArray(valueMappings[ruleName])) { if (!Array.isArray(valueMappings[ruleName])) {
valueMappings[ruleName] = []; valueMappings[ruleName] = [];
} }
const sortedValue = Array.isArray(value) const sortedValue = Array?.isArray(value)
? value.sort((a, b) => a - b) ? value?.sort(customSort)
: value; : value;
const valueKey = Array.isArray(sortedValue) const valueKey = Array?.isArray(sortedValue)
? sortedValue.join("-") ? sortedValue.join("-")
: sortedValue; : sortedValue;
const index = valueMappings[ruleName].indexOf(valueKey); const index = valueMappings[ruleName].indexOf(valueKey);
if (index === -1) { if (index === -1) {
// Add the value if it's not already selected
valueMappings[ruleName].push(valueKey); valueMappings[ruleName].push(valueKey);
} else { } else {
// Remove the value if it's already selected
valueMappings[ruleName].splice(index, 1); valueMappings[ruleName].splice(index, 1);
} }
// If no values are selected, set the value to "any"
if (valueMappings[ruleName].length === 0) { if (valueMappings[ruleName].length === 0) {
valueMappings[ruleName] = "any"; valueMappings[ruleName] = "any";
} }
await updateStockScreenerData(); await updateStockScreenerData();
} else if (ruleName in valueMappings) { } else if (ruleName in valueMappings) {
// Handle "over", "under", and "between" conditions if (ruleCondition[ruleName] === "between" && Array?.isArray(value)) {
if (ruleCondition[ruleName] === "between" && Array.isArray(value)) { valueMappings[ruleName] = value?.sort(customSort);
valueMappings[ruleName] = value.sort((a, b) => a - b); // Ensure lower value is first
} else { } else {
valueMappings[ruleName] = value; // Store single values for "over" and "under" valueMappings[ruleName] = value;
} }
} else { } else {
console.warn(`Unhandled rule: ${ruleName}`); console.warn(`Unhandled rule: ${ruleName}`);
@ -1988,14 +2009,17 @@ const handleKeyDown = (event) => {
await handleChangeValue(newValue); await handleChangeValue(newValue);
} }
async function handleValueInput(event) { async function handleValueInput(event, ruleName, index = null) {
const newValue = event.target.value; 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); await handleChangeValue(newValue);
} }
} }
async function popularStrategy(state: string) { async function popularStrategy(state: string) {
ruleOfList = []; ruleOfList = [];
const strategies = { const strategies = {
@ -2757,12 +2781,15 @@ const handleKeyDown = (event) => {
<span class="truncate ml-2 text-sm sm:text-[1rem]"> <span class="truncate ml-2 text-sm sm:text-[1rem]">
{#if valueMappings[row?.rule] === "any"} {#if valueMappings[row?.rule] === "any"}
Any Any
{:else} {:else if ["under", "over"]?.includes(ruleCondition[row?.rule])}
{ruleCondition[row?.rule] {ruleCondition[row?.rule]
?.replace("under", "Under") ?.replace("under", "Under")
?.replace("over", "Over") ?.replace("over", "Over")}
?.replace("between", "Between")}
{valueMappings[row?.rule]} {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}
</span> </span>
<svg <svg
@ -2781,7 +2808,7 @@ const handleKeyDown = (event) => {
</Button> </Button>
</DropdownMenu.Trigger> </DropdownMenu.Trigger>
<DropdownMenu.Content <DropdownMenu.Content
class="w-64 h-fit max-h-72 overflow-y-auto scroller" class="w-64 min-h-64 max-h-72 overflow-y-auto scroller"
> >
{#if !["sma20", "sma50", "sma100", "sma200", "ema20", "ema50", "ema100", "ema200", "grahamNumber", "analystRating", "halalStocks", "score", "sector", "industry", "country"]?.includes(row?.rule)} {#if !["sma20", "sma50", "sma100", "sma200", "ema20", "ema50", "ema100", "ema200", "grahamNumber", "analystRating", "halalStocks", "score", "sector", "industry", "country"]?.includes(row?.rule)}
<DropdownMenu.Label <DropdownMenu.Label
@ -2840,13 +2867,49 @@ const handleKeyDown = (event) => {
</DropdownMenu.Root> </DropdownMenu.Root>
</div> </div>
{#if ruleCondition[row?.rule] === "between"}
<div class="flex gap-x-1 -ml-2 z-10 -mt-1">
<input
type="text"
placeholder="Min"
value={Array.isArray(
valueMappings[row?.rule],
)
? (valueMappings[row?.rule][0] ?? "")
: ""}
on:input={(e) =>
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"
/>
<span
class="text-white text-[1rem] font-normal mt-1"
>
&
</span>
<input
type="text"
placeholder="Max"
value={Array.isArray(
valueMappings[row?.rule],
)
? (valueMappings[row?.rule][1] ?? "")
: ""}
on:input={(e) =>
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"
/>
</div>
{:else}
<input <input
type="text" type="text"
placeholder="Value" placeholder="Value"
value={valueMappings[row?.rule]} value={valueMappings[row?.rule]}
on:input={(e) => handleValueInput(e)} on:input={(e) =>
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" 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 ["over", "under"]?.includes(ruleCondition[ruleName]?.toLowerCase())}
<div <div
class="ml-2 flex touch-manipulation flex-row items-center gap-x-1.5" class="ml-2 flex touch-manipulation flex-row items-center gap-x-1.5"
@ -2926,8 +2989,11 @@ const handleKeyDown = (event) => {
<DropdownMenu.Group class="min-h-10 mt-2"> <DropdownMenu.Group class="min-h-10 mt-2">
{#if !["sma20", "sma50", "sma100", "sma200", "ema20", "ema50", "ema100", "ema200", "grahamNumber", "analystRating", "halalStocks", "score", "sector", "industry", "country"]?.includes(row?.rule)} {#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} {#each row?.step as newValue, index}
{#if ruleCondition[row?.rule] === "between" && newValue && row?.step[index + 1]} {#if ruleCondition[row?.rule] === "between"}
<DropdownMenu.Item class="sm:hover:bg-primary"> {#if newValue && row?.step[index + 1]}
<DropdownMenu.Item
class="sm:hover:bg-primary"
>
<button <button
on:click={() => { on:click={() => {
handleChangeValue([ handleChangeValue([
@ -2941,7 +3007,9 @@ const handleKeyDown = (event) => {
"between", "between",
"Between", "Between",
)} )}
{row?.step[index + 1]} - {row?.step[index]} {row?.step[index + 1]} - {row?.step[
index
]}
</button> </button>
</DropdownMenu.Item> </DropdownMenu.Item>
{/if} {/if}
@ -2959,6 +3027,7 @@ const handleKeyDown = (event) => {
{newValue} {newValue}
</button> </button>
</DropdownMenu.Item> </DropdownMenu.Item>
{/if}
{/each} {/each}
{:else if ["sma20", "sma50", "sma100", "sma200", "ema20", "ema50", "ema100", "ema200", "grahamNumber"]?.includes(row?.rule)} {:else if ["sma20", "sma50", "sma100", "sma200", "ema20", "ema50", "ema100", "ema200", "grahamNumber"]?.includes(row?.rule)}
{#each row?.step as item} {#each row?.step as item}

View File

@ -43,17 +43,18 @@ const movingAverageConditions = {
}; };
// Convert the input to a value or return it as-is if it's already an array // Convert the input to a value or return it as-is if it's already an array
function convertUnitToValue( function convertUnitToValue(input: string | number | string[]) {
input: string | number | string[] try {
): number | string[] | string { if (Array.isArray(input)) {
if (Array.isArray(input)) return input; return input.map(convertUnitToValue); // Recursively convert array elements
}
if (typeof input === "number") return input; if (typeof input === "number") return input;
if (typeof input !== "string") { if (typeof input !== "string") {
throw new TypeError( return input; // Return as-is if not a string or number
`Expected a string or number, but received ${typeof input}`
);
} }
const lowerInput = input.toLowerCase(); const lowerInput = input.toLowerCase();
// Pre-compute the set for quick lookups // Pre-compute the set for quick lookups
const nonNumericValues = new Set([ const nonNumericValues = new Set([
"any", "any",
@ -69,16 +70,19 @@ function convertUnitToValue(
"non-compliant", "non-compliant",
"stock price", "stock price",
]); ]);
if (nonNumericValues.has(lowerInput)) return input; if (nonNumericValues.has(lowerInput)) return input;
// Handle percentage values
if (input.endsWith("%")) { if (input.endsWith("%")) {
const numericValue = parseFloat(input.slice(0, -1)); const numericValue = parseFloat(input.slice(0, -1)); // Remove '%' and convert to number
if (isNaN(numericValue)) { if (isNaN(numericValue)) {
throw new Error(`Unable to convert ${input} to a number`); return input; // Return original input if conversion fails
} }
return numericValue; 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 units = { B: 1_000_000_000, M: 1_000_000, K: 1_000 };
const match = input.match(/^(\d+(\.\d+)?)([BMK])?$/); const match = input.match(/^(\d+(\.\d+)?)([BMK])?$/);
@ -88,31 +92,55 @@ function convertUnitToValue(
return unit ? value * units[unit] : value; return unit ? value * units[unit] : value;
} }
// Default numeric conversion (if no unit specified)
const numericValue = parseFloat(input); const numericValue = parseFloat(input);
if (isNaN(numericValue)) { if (isNaN(numericValue)) {
throw new Error(`Unable to convert ${input} to a number`); return input; // Return original input if conversion fails
} }
return numericValue; return numericValue;
} catch (error) {
console.warn(`Error converting value: ${input}`, error);
return input; // Return original input in case of any unexpected errors
}
} }
// Filter the stock screener data based on the provided rules
async function filterStockScreenerData(stockScreenerData, ruleOfList) { async function filterStockScreenerData(stockScreenerData, ruleOfList) {
try {
return stockScreenerData?.filter((item) => { return stockScreenerData?.filter((item) => {
return ruleOfList.every((rule) => { return ruleOfList.every((rule) => {
try {
const itemValue = item[rule.name]; const itemValue = item[rule.name];
const ruleValue = convertUnitToValue(rule.value); const ruleValue = convertUnitToValue(rule.value);
const ruleName = rule.name.toLowerCase(); const ruleName = rule.name.toLowerCase();
// Handle trend and fundamental analysis // If ruleValue is the original input (conversion failed),
if (["trendAnalysis", "fundamentalAnalysis"].includes(rule.name)) { // we'll treat it as a special case
const accuracy = item[rule.name]?.accuracy; if (typeof ruleValue === "string") {
if (rule.condition === "over" && accuracy <= ruleValue) return false; // For most string inputs, we'll consider it a match
if (rule.condition === "under" && accuracy > ruleValue) return false; 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;
}
// For other cases, we'll skip filtering
return true;
} }
// Handle categorical data like analyst ratings, sector, country // Handle categorical data like analyst ratings, sector, country
else if ( if (
[ [
"analystRating", "analystRating",
"halalStocks", "halalStocks",
@ -156,26 +184,74 @@ async function filterStockScreenerData(stockScreenerData, ruleOfList) {
return true; // If all conditions are met 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 // Default numeric or string comparison
if (typeof ruleValue === "string") return true; // Skip non-numeric comparisons else if (typeof ruleValue === "string") {
if (itemValue === null) return false; // Null values do not meet any condition return true; // Skip non-numeric comparisons
if (rule.condition === "over" && itemValue <= ruleValue) return false; } else if (itemValue === null) {
if (rule.condition === "under" && itemValue > ruleValue) return false; 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; return true;
} catch (ruleError) {
console.warn(`Error processing rule for item:`, rule, ruleError);
return true; // Default to including the item if rule processing fails
}
}); });
}); }) || 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) => { onmessage = async (event: MessageEvent) => {
const { stockScreenerData, ruleOfList } = event.data || {}; const { stockScreenerData, ruleOfList } = event.data || {};
try {
const filteredData = await filterStockScreenerData( const filteredData = await filterStockScreenerData(
stockScreenerData, stockScreenerData,
ruleOfList 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 {};