This commit is contained in:
MuslemRahimi 2024-12-24 23:48:22 +01:00
parent 43e1e5b02e
commit 89b60b4d96
4 changed files with 2318 additions and 3 deletions

View File

@ -319,7 +319,7 @@
<Combobox.Input
id="combobox-input"
on:input={search}
class="grow rounded-sm border border-gray-600 py-2 pl-9 text-[1rem] placeholder-gray-400 focus:border-default focus:shadow-lg focus:outline-none focus:ring-0 tiny:pl-8 xs:pl-10 text-white md:py-2 w-full bg-secondary sm:hover:bg-[#09090B] focus:bg-[#09090B] dark:bg-dark-700 dark:hover:bg-dark-700 dark:focus:bg-dark-700"
class="grow rounded-sm border border-gray-600 py-2 pl-9 text-[1rem] placeholder-gray-400 focus:border-default focus:shadow-lg focus:outline-none focus:ring-0 tiny:pl-8 xs:pl-10 text-white md:py-2 w-full bg-secondary focus:bg-secondary"
placeholder="Company or stock symbol..."
aria-label="Company or stock symbol..."
/>
@ -351,7 +351,7 @@
</div>
</div>
<Combobox.Content
class="w-auto z-40 -mt-0.5 rounded-md border border-gray-700 bg-[#09090B] px-1 py-3 shadow-popover outline-none"
class="w-auto z-40 -mt-0.5 rounded-md border border-gray-700 bg-secondary px-1 py-3 shadow-popover outline-none"
sideOffset={8}
on:keydown={handleKeyDown}
>
@ -370,7 +370,7 @@
</div>
</Combobox.Item>
{/each}
{:else if inputValue?.length === 0}
{:else if inputValue?.length === 0 && searchHistory?.length > 0}
{#each searchHistory as item}
<Combobox.Item
class="cursor-pointer text-white border-b border-gray-600 last:border-none flex h-fit w-auto select-none items-center rounded-button py-3 pl-3 pr-1.5 text-sm capitalize outline-none transition-all duration-75 data-[highlighted]:bg-primary"

View File

@ -0,0 +1,56 @@
export const load = async ({ locals, cookies }) => {
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(),
};
};

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,376 @@
interface FilterContext {
flowTypeCache?: Map<string, number>;
}
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 {};