ui fix
This commit is contained in:
parent
43e1e5b02e
commit
89b60b4d96
@ -319,7 +319,7 @@
|
|||||||
<Combobox.Input
|
<Combobox.Input
|
||||||
id="combobox-input"
|
id="combobox-input"
|
||||||
on:input={search}
|
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..."
|
placeholder="Company or stock symbol..."
|
||||||
aria-label="Company or stock symbol..."
|
aria-label="Company or stock symbol..."
|
||||||
/>
|
/>
|
||||||
@ -351,7 +351,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<Combobox.Content
|
<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}
|
sideOffset={8}
|
||||||
on:keydown={handleKeyDown}
|
on:keydown={handleKeyDown}
|
||||||
>
|
>
|
||||||
@ -370,7 +370,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</Combobox.Item>
|
</Combobox.Item>
|
||||||
{/each}
|
{/each}
|
||||||
{:else if inputValue?.length === 0}
|
{:else if inputValue?.length === 0 && searchHistory?.length > 0}
|
||||||
{#each searchHistory as item}
|
{#each searchHistory as item}
|
||||||
<Combobox.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"
|
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"
|
||||||
|
|||||||
56
src/routes/dark-pool-flow/+page.server.ts
Normal file
56
src/routes/dark-pool-flow/+page.server.ts
Normal 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(),
|
||||||
|
};
|
||||||
|
};
|
||||||
1883
src/routes/dark-pool-flow/+page.svelte
Normal file
1883
src/routes/dark-pool-flow/+page.svelte
Normal file
File diff suppressed because it is too large
Load Diff
376
src/routes/dark-pool-flow/workers/filterWorker.ts
Normal file
376
src/routes/dark-pool-flow/workers/filterWorker.ts
Normal 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 {};
|
||||||
Loading…
x
Reference in New Issue
Block a user