update screener of options flow page

This commit is contained in:
MuslemRahimi 2024-12-09 13:05:59 +01:00
parent 0455cac269
commit 703d30b6ab
3 changed files with 436 additions and 141 deletions

View File

@ -44,7 +44,7 @@
const allRules = { const allRules = {
volume: { volume: {
label: "Volume", label: "Volume",
step: ["100K", "10K", "1K"], step: ["100K", "50K", "20K", "10K", "5K", "2K", "1K", "100", "0"],
defaultCondition: "over", defaultCondition: "over",
defaultValue: "any", defaultValue: "any",
}, },
@ -133,7 +133,13 @@
Object.keys(allRules).forEach((ruleName) => { Object.keys(allRules).forEach((ruleName) => {
ruleCondition[ruleName] = allRules[ruleName].defaultCondition; ruleCondition[ruleName] = allRules[ruleName].defaultCondition;
valueMappings[ruleName] = allRules[ruleName].defaultValue;
// Check if the default condition is "between"
if (allRules[ruleName].defaultCondition === "between") {
valueMappings[ruleName] = allRules[ruleName].defaultValue || [null, null];
} else {
valueMappings[ruleName] = allRules[ruleName].defaultValue;
}
}); });
// Update ruleCondition and valueMappings based on existing rules // Update ruleCondition and valueMappings based on existing rules
@ -277,9 +283,17 @@
//console.log(displayedData) //console.log(displayedData)
}; };
function changeRuleCondition(name: string, state: string) { async function changeRuleCondition(name: string, state: string) {
ruleName = name; ruleName = name;
ruleCondition[ruleName] = state; if (
ruleCondition[ruleName] === "between" &&
["over", "under"]?.includes(state?.toLowerCase())
) {
valueMappings[ruleName] = "";
}
ruleCondition[ruleName] = state?.toLowerCase();
await handleChangeValue(valueMappings[ruleName]);
} }
let checkedItems = new Set(ruleOfList.flatMap((rule) => rule.value)); let checkedItems = new Set(ruleOfList.flatMap((rule) => rule.value));
@ -288,12 +302,43 @@
return checkedItems.has(item); return checkedItems.has(item);
} }
// Utility function to convert values to comparable numbers
// 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);
}
async function handleChangeValue(value) { async function handleChangeValue(value) {
// Toggle checkedItems logic
if (checkedItems.has(value)) { if (checkedItems.has(value)) {
checkedItems.delete(value); checkedItems.delete(value);
} else { } else {
checkedItems.add(value); checkedItems.add(value);
} }
// Specific rule handling for options-related rules
if ( if (
[ [
"put_call", "put_call",
@ -306,29 +351,37 @@
) { ) {
// Ensure valueMappings[ruleName] is initialized as an array // Ensure valueMappings[ruleName] is initialized as an array
if (!Array.isArray(valueMappings[ruleName])) { if (!Array.isArray(valueMappings[ruleName])) {
valueMappings[ruleName] = []; // Initialize as an empty array if not already valueMappings[ruleName] = [];
} }
// Similar logic to the original function for adding/removing values
const index = valueMappings[ruleName].indexOf(value); const index = valueMappings[ruleName].indexOf(value);
if (index === -1) { if (index === -1) {
// Add the country if it's not already selected
valueMappings[ruleName].push(value); valueMappings[ruleName].push(value);
// Sort the array when a new value is added
valueMappings[ruleName] = valueMappings[ruleName].sort(customSort);
} else { } else {
// Remove the country if it's already selected (deselect)
valueMappings[ruleName].splice(index, 1); valueMappings[ruleName].splice(index, 1);
} }
// If no countries are selected, set value to "any" // Set to "any" if no values are selected
if (valueMappings[ruleName].length === 0) { if (valueMappings[ruleName].length === 0) {
valueMappings[ruleName] = "any"; valueMappings[ruleName] = "any";
} }
} else if (ruleName in valueMappings) { } else if (ruleName in valueMappings) {
// Handle non-country rules as single values // For rules that require sorting (like range or numeric values)
valueMappings[ruleName] = value; if (ruleCondition[ruleName] === "between" && Array.isArray(value)) {
// Sort the array for between conditions
valueMappings[ruleName] = value.sort(customSort);
} else {
// Handle non-specific rules as single values
valueMappings[ruleName] = value;
}
} else { } else {
console.warn(`Unhandled rule: ${ruleName}`); console.warn(`Unhandled rule: ${ruleName}`);
} }
// Update ruleOfList (if applicable)
const ruleToUpdate = ruleOfList?.find((rule) => rule.name === ruleName); const ruleToUpdate = ruleOfList?.find((rule) => rule.name === ruleName);
if (ruleToUpdate) { if (ruleToUpdate) {
ruleToUpdate.value = valueMappings[ruleToUpdate.name]; ruleToUpdate.value = valueMappings[ruleToUpdate.name];
@ -336,10 +389,40 @@
ruleOfList = [...ruleOfList]; ruleOfList = [...ruleOfList];
} }
// Trigger worker load and save cookie
shouldLoadWorker.set(true); shouldLoadWorker.set(true);
await saveCookieRuleOfList(); await saveCookieRuleOfList();
} }
async function stepSizeValue(value, condition) {
const match = value.toString().match(/^(-?[\d.]+)([KMB%]?)$/);
if (!match) return value;
let [_, number, suffix] = match;
number = parseFloat(number);
let step = 1;
number += condition === "add" ? step : -step;
// Round to 2 decimal places for consistency
number = parseFloat(number?.toFixed(2));
const newValue = suffix ? `${number}${suffix}` : Math?.round(number);
await handleChangeValue(newValue);
}
async function handleValueInput(event, ruleName, index = null) {
const newValue = event.target.value;
if (ruleCondition[ruleName] === "between") {
const currentValues = valueMappings[ruleName] || ["", ""];
currentValues[index] = newValue;
await handleChangeValue(currentValues);
} else {
await handleChangeValue(newValue);
}
}
const nyseDate = new Date( const nyseDate = new Date(
data?.getOptionsFlowFeed?.at(0)?.date ?? null, data?.getOptionsFlowFeed?.at(0)?.date ?? null,
)?.toLocaleString("en-US", { )?.toLocaleString("en-US", {
@ -1272,10 +1355,14 @@ function sendMessage(message) {
<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 if ruleCondition[row?.rule] === "between"}
{Array.isArray(valueMappings[row?.rule])
? `${valueMappings[row?.rule][0]}-${valueMappings[row?.rule][1] ?? "Any"}`
: "Any"}
{:else} {:else}
{ruleCondition[row?.rule] !== undefined {ruleCondition[row?.rule]
? ruleCondition[row?.rule] ?.replace("under", "Under")
: ""} ?.replace("over", "Over") ?? ""}
{valueMappings[row?.rule]} {valueMappings[row?.rule]}
{/if} {/if}
</span> </span>
@ -1295,7 +1382,7 @@ function sendMessage(message) {
</Button> </Button>
</DropdownMenu.Trigger> </DropdownMenu.Trigger>
<DropdownMenu.Content <DropdownMenu.Content
class="w-56 h-fit max-h-72 overflow-y-auto " class="w-64 min-h-auto max-h-72 overflow-y-auto scroller"
> >
{#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", "date_expiration", "underlying_type"]?.includes(row?.rule)}
<DropdownMenu.Label <DropdownMenu.Label
@ -1304,42 +1391,148 @@ function sendMessage(message) {
<div <div
class="flex items-center justify-start gap-x-1" class="flex items-center justify-start gap-x-1"
> >
<!--Start Dropdown for Condition-->
<div <div
class="relative inline-block flex flex-row items-center justify-center" class="-ml-2 relative inline-block text-left"
> >
<label <DropdownMenu.Root>
on:click={() => <DropdownMenu.Trigger asChild let:builder
changeRuleCondition(row?.rule, "under")} ><Button
class="cursor-pointer flex flex-row mr-2 justify-center items-center" builders={[builder]}
> class="w-fit -mt-1 -ml-2 bg-[#09090B] flex flex-row justify-between items-center text-white"
<input >
type="radio" <span
class="radio checked:bg-[#fff] bg-[#09090B] border border-gray-600 mr-2" class="truncate ml-2 text-sm sm:text-[1rem]"
checked={ruleCondition[row?.rule] === >
"under"} {ruleCondition[ruleName]
name={row?.rule} ?.replace("under", "Under")
/> ?.replace("over", "Over")
<span class="label-text text-white" ?.replace("between", "Between")}
>Under</span </span>
> <svg
</label> class="mt-1 -mr-1 ml-1 h-5 w-5 xs:ml-2 !ml-0 sm:ml-0 inline-block"
<label viewBox="0 0 20 20"
on:click={() => fill="currentColor"
changeRuleCondition(row?.rule, "over")} style="max-width:40px"
class="cursor-pointer flex flex-row ml-2 justify-center items-center" aria-hidden="true"
> ><path
<input fill-rule="evenodd"
type="radio" d="M5.293 7.293a1 1 0 011.414 0L10 10.586l3.293-3.293a1 1 0 111.414 1.414l-4 4a1 1 0 01-1.414 0l-4-4a1 1 0 010-1.414z"
class="radio checked:bg-[#fff] bg-[#09090B] border border-gray-600 mr-2" clip-rule="evenodd"
checked={ruleCondition[row?.rule] === ></path></svg
"over"} >
name={row?.rule} </Button>
/> </DropdownMenu.Trigger>
<span class="label-text text-white" <DropdownMenu.Content>
>Over</span <DropdownMenu.Group>
> {#each ["Over", "Under", "Between"] as item}
</label> <DropdownMenu.Item
on:click={() =>
changeRuleCondition(
row?.rule,
item,
)}
class="cursor-pointer text-[1rem] font-normal"
>{item}</DropdownMenu.Item
>
{/each}
</DropdownMenu.Group>
</DropdownMenu.Content>
</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
type="text"
placeholder="Value"
value={valueMappings[row?.rule] === "any"
? ""
: valueMappings[row?.rule]}
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"
/>
{/if}
{#if ["over", "under"]?.includes(ruleCondition[ruleName]?.toLowerCase())}
<div
class="ml-2 flex touch-manipulation flex-row items-center gap-x-1.5"
>
<button
on:click={() =>
stepSizeValue(
valueMappings[row?.rule],
"add",
)}
><svg
class="size-6 cursor-pointer text-white"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
style="max-width:40px"
><path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M12 9v6m3-3H9m12 0a9 9 0 11-18 0 9 9 0 0118 0z"
></path></svg
></button
>
<button
on:click={() =>
stepSizeValue(
valueMappings[row?.rule],
"minus",
)}
><svg
class="size-6 cursor-pointer text-white"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
style="max-width:40px"
><path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M15 12H9m12 0a9 9 0 11-18 0 9 9 0 0118 0z"
></path></svg
></button
>
</div>
{/if}
<!--End Dropdown for Condition-->
</div> </div>
</DropdownMenu.Label> </DropdownMenu.Label>
{:else} {:else}
@ -1352,22 +1545,48 @@ function sendMessage(message) {
{/if} {/if}
<DropdownMenu.Group class="min-h-10 mt-2"> <DropdownMenu.Group class="min-h-10 mt-2">
{#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", "date_expiration", "underlying_type"]?.includes(row?.rule)}
{#each row?.step as newValue} {#each row?.step as newValue, index}
<DropdownMenu.Item {#if ruleCondition[row?.rule] === "between"}
class="sm:hover:bg-[#2A2E39]" {#if newValue && row?.step[index + 1]}
> <DropdownMenu.Item
<button class="sm:hover:bg-primary"
on:click={() => { >
handleChangeValue(newValue); <button
}} on:click={() => {
class="block w-full border-b border-gray-600 px-4 py-1.5 text-left text-sm sm:text-[1rem] rounded text-white last:border-0 sm:hover:bg-[#2A2E39] focus:bg-blue-100 focus:text-gray-900 focus:outline-none" handleChangeValue([
row?.step[index],
row?.step[index + 1],
]);
}}
class="block w-full border-b border-gray-600 px-4 py-1.5 text-left text-sm sm:text-[1rem] rounded text-white last:border-0 sm:hover:bg-primary focus:bg-blue-100 focus:text-gray-900 focus:outline-none"
>
{ruleCondition[row?.rule]?.replace(
"between",
"Between",
)}
{row?.step[index + 1]} - {row?.step[
index
]}
</button>
</DropdownMenu.Item>
{/if}
{:else}
<DropdownMenu.Item
class="sm:hover:bg-primary"
> >
{ruleCondition[row?.rule] !== undefined <button
? ruleCondition[row?.rule] on:click={() => {
: ""} handleChangeValue(newValue);
{newValue} }}
</button> class="block w-full border-b border-gray-600 px-4 py-1.5 text-left text-sm sm:text-[1rem] rounded text-white last:border-0 sm:hover:bg-primary focus:bg-blue-100 focus:text-gray-900 focus:outline-none"
</DropdownMenu.Item> >
{ruleCondition[row?.rule]
?.replace("under", "Under")
?.replace("over", "Over")}
{newValue}
</button>
</DropdownMenu.Item>
{/if}
{/each} {/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", "date_expiration", "underlying_type"]?.includes(row?.rule)}
{#each row?.step as item} {#each row?.step as item}

View File

@ -136,97 +136,173 @@ function isDateWithinRange(dateString: string, range: string): boolean {
} }
async function filterRawData(rawData, ruleOfList, filterQuery) { async function filterRawData(rawData, ruleOfList, filterQuery) {
// Split filterQuery into an array of tickers if it's a comma-separated string // Early return for empty inputs
if (!rawData?.length || !ruleOfList?.length) {
return rawData || [];
}
// Preprocess filter tickers
const filterTickers = filterQuery const filterTickers = filterQuery
? filterQuery.split(",").map((ticker) => ticker.trim().toUpperCase()) ? filterQuery.split(",").map((ticker) => ticker.trim().toUpperCase())
: []; : [];
return rawData?.filter((item) => { // Precompile rules for more efficient filtering
// Check if the item's ticker matches any of the tickers in filterTickers const compiledRules = ruleOfList.map(rule => {
if ( const ruleName = rule.name.toLowerCase();
filterTickers.length > 0 && const ruleValue = convertUnitToValue(rule.value);
!filterTickers.includes(item.ticker.toUpperCase())
) { return {
return false; // Exclude if the ticker doesn't match any in filterTickers ...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;
} }
return ruleOfList.every((rule) => { // Apply all precompiled rules
const ruleName = rule.name.toLowerCase(); return compiledRules.every(rule => rule.compiledCheck(item));
const ruleValue = convertUnitToValue(rule.value);
// Handle volumeOIRatio
if (ruleName === "volumeoiratio") {
const volume = parseFloat(item.volume);
const openInterest = parseFloat(item.open_interest);
if (isNaN(volume) || isNaN(openInterest) || openInterest === 0) {
return false; // Invalid data, exclude this item
}
const ratio = (volume / openInterest) * 100;
if (rule.condition === "over" && ratio <= ruleValue) return false;
if (rule.condition === "under" && ratio >= ruleValue) return false;
return true;
}
const itemValue = item[rule.name];
// Handle date_expiration
if (ruleName === "date_expiration") {
if (isAny(ruleValue)) return true;
if (Array.isArray(ruleValue)) {
return ruleValue.some((range) => isDateWithinRange(itemValue, range));
}
return isDateWithinRange(itemValue, ruleValue as string);
}
// Handle categorical data
else if (
[
"put_call",
"sentiment",
"execution_estimate",
"option_activity_type",
"underlying_type",
].includes(ruleName)
) {
if (isAny(ruleValue)) return true;
if (itemValue === null || itemValue === undefined) return false;
const lowerItemValue = itemValue.toString().toLowerCase();
if (Array.isArray(ruleValue)) {
return ruleValue.some(
(value) => lowerItemValue === value.toString().toLowerCase(),
);
}
return lowerItemValue === ruleValue.toString().toLowerCase();
}
// Default numeric or string comparison
if (typeof ruleValue === "string") return true;
if (itemValue === null || itemValue === undefined) return false;
const numericItemValue = parseFloat(itemValue);
if (isNaN(numericItemValue)) return false;
if (rule.condition === "over" && numericItemValue <= ruleValue)
return false;
if (rule.condition === "under" && numericItemValue >= ruleValue)
return false;
return true;
});
}); });
} }
// Centralized rule checking logic
function createRuleCheck(rule, ruleName, ruleValue) {
// Handle volumeOIRatio
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 = (volume / openInterest) * 100;
if (rule.condition === 'over' && ratio <= ruleValue) return false;
if (rule.condition === 'under' && ratio >= ruleValue) return false;
return true;
};
}
// 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);
};
}
// Categorical data handling
const categoricalFields = [
'put_call',
'sentiment',
'execution_estimate',
'option_activity_type',
'underlying_type'
];
if (categoricalFields.includes(ruleName)) {
// If 'any' is specified
if (isAny(ruleValue)) return () => true;
return (item) => {
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
if (Array.isArray(ruleValue)) {
return ruleValue.some(
value => lowerItemValue === value.toString().toLowerCase()
);
}
// Single value comparison
return lowerItemValue === ruleValue.toString().toLowerCase();
};
}
// Default numeric comparison
return (item) => {
const itemValue = item[rule.name];
// 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;
return true;
};
}
// Web Worker message handler
onmessage = async (event: MessageEvent) => { onmessage = async (event: MessageEvent) => {
const { rawData, ruleOfList, filterQuery } = event.data || {}; const { rawData, ruleOfList, filterQuery } = event.data || {};
// Filter the data
let filteredData = await filterRawData(rawData, ruleOfList, filterQuery); let filteredData = await filterRawData(rawData, ruleOfList, filterQuery);
filteredData = Array?.from(
new Map(filteredData?.map((item) => [item?.id, item]))?.values(), // Remove duplicates based on id
filteredData = Array.from(
new Map(filteredData?.map((item) => [item?.id, item]))?.values()
); );
postMessage({ message: "success", filteredData }); postMessage({ message: "success", filteredData });
}; };
export {}; export {};

View File

@ -2814,7 +2814,7 @@ const handleKeyDown = (event) => {
</Button> </Button>
</DropdownMenu.Trigger> </DropdownMenu.Trigger>
<DropdownMenu.Content <DropdownMenu.Content
class="w-64 min-h-64 max-h-72 overflow-y-auto scroller" class="w-64 min-h-auto 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