update dark pool feed flow

This commit is contained in:
MuslemRahimi 2024-12-25 14:59:56 +01:00
parent aa697ccf45
commit fd286269a1
3 changed files with 97 additions and 263 deletions

View File

@ -49,8 +49,8 @@
premium: "none", premium: "none",
assetType: "none", assetType: "none",
volume: "none", volume: "none",
avgVolume: "none", sizeAvgVolRatio: "none",
dailyVolume: "none", sizeVolRatio: "none",
size: "none", size: "none",
sector: "none", sector: "none",
}; };
@ -128,14 +128,14 @@
const volB = parseFloat(b.volume); const volB = parseFloat(b.volume);
return sortOrder === "asc" ? volA - volB : volB - volA; return sortOrder === "asc" ? volA - volB : volB - volA;
}, },
dailyVolume: (a, b) => { sizeVolRatio: (a, b) => {
const volA = parseFloat(a.dailyVolumePercentage); const volA = parseFloat(a.sizeVolRatio);
const volB = parseFloat(b.dailyVolumePercentage); const volB = parseFloat(b.sizeVolRatio);
return sortOrder === "asc" ? volA - volB : volB - volA; return sortOrder === "asc" ? volA - volB : volB - volA;
}, },
avgVolume: (a, b) => { sizeAvgVolRatio: (a, b) => {
const volA = parseFloat(a.avgVolumePercentage); const volA = parseFloat(a.sizeAvgVolRatio);
const volB = parseFloat(b.avgVolumePercentage); const volB = parseFloat(b.sizeAvgVolRatio);
return sortOrder === "asc" ? volA - volB : volB - volA; return sortOrder === "asc" ? volA - volB : volB - volA;
}, },
assetType: (a, b) => { assetType: (a, b) => {
@ -304,16 +304,16 @@
</div> </div>
<div <div
on:click={() => sortData("dailyVolume")} on:click={() => sortData("sizeVolRatio")}
class="td cursor-pointer select-none bg-[#121217] text-slate-300 font-bold text-xs text-start uppercase" class="td cursor-pointer select-none bg-[#121217] text-slate-300 font-bold text-xs text-start uppercase"
> >
% Size / Vol % Size / Vol
<svg <svg
class="flex-shrink-0 w-4 h-4 inline-block {sortOrders[ class="flex-shrink-0 w-4 h-4 inline-block {sortOrders[
'dailyVolume' 'sizeVolRatio'
] === 'asc' ] === 'asc'
? 'rotate-180' ? 'rotate-180'
: sortOrders['dailyVolume'] === 'desc' : sortOrders['sizeVolRatio'] === 'desc'
? '' ? ''
: 'hidden'} " : 'hidden'} "
viewBox="0 0 20 20" viewBox="0 0 20 20"
@ -328,16 +328,16 @@
</div> </div>
<div <div
on:click={() => sortData("avgVolume")} on:click={() => sortData("sizeAvgVolRatio")}
class="td cursor-pointer select-none bg-[#121217] text-slate-300 font-bold text-xs text-start uppercase" class="td cursor-pointer select-none bg-[#121217] text-slate-300 font-bold text-xs text-start uppercase"
> >
% Size / Avg Vol % Size / Avg Vol
<svg <svg
class="flex-shrink-0 w-4 h-4 inline-block {sortOrders[ class="flex-shrink-0 w-4 h-4 inline-block {sortOrders[
'avgVolume' 'sizeAvgVolRatio'
] === 'asc' ] === 'asc'
? 'rotate-180' ? 'rotate-180'
: sortOrders['avgVolume'] === 'desc' : sortOrders['sizeAvgVolRatio'] === 'desc'
? '' ? ''
: 'hidden'} " : 'hidden'} "
viewBox="0 0 20 20" viewBox="0 0 20 20"
@ -464,8 +464,8 @@
style="justify-content: center;" style="justify-content: center;"
class="td text-sm sm:text-[1rem] text-white text-end" class="td text-sm sm:text-[1rem] text-white text-end"
> >
{displayedData[index]?.dailyVolumePercentage > 0.01 {displayedData[index]?.sizeVolRatio > 0.01
? displayedData[index]?.dailyVolumePercentage?.toFixed(2) + "%" ? displayedData[index]?.sizeVolRatio?.toFixed(2) + "%"
: "< 0.01%"} : "< 0.01%"}
</div> </div>
@ -473,8 +473,8 @@
style="justify-content: center;" style="justify-content: center;"
class="td text-sm sm:text-[1rem] text-white text-end" class="td text-sm sm:text-[1rem] text-white text-end"
> >
{displayedData[index]?.avgVolume > 0.01 {displayedData[index]?.sizeAvgVolRatio > 0.01
? displayedData[index]?.avgVolume?.toFixed(2) + "%" ? displayedData[index]?.sizeAvgVolRatio?.toFixed(2) + "%"
: "< 0.01%"} : "< 0.01%"}
</div> </div>

View File

@ -7,7 +7,7 @@
isOpen, isOpen,
} from "$lib/store"; } from "$lib/store";
import { cn } from "$lib/utils"; import { cn, sectorList } from "$lib/utils";
import { onMount, onDestroy } from "svelte"; import { onMount, onDestroy } from "svelte";
import toast from "svelte-french-toast"; import toast from "svelte-french-toast";
import { DateFormatter, type DateValue } from "@internationalized/date"; import { DateFormatter, type DateValue } from "@internationalized/date";
@ -25,10 +25,11 @@
export let data; export let data;
let shouldLoadWorker = writable(false); let shouldLoadWorker = writable(false);
let ruleOfList = data?.getPredefinedCookieRuleOfList || []; let ruleOfList = [];
let displayRules = []; let displayRules = [];
let filteredData = []; let filteredData = [];
let filterQuery = $page.url.searchParams.get("query") || ""; let filterQuery = $page.url.searchParams.get("query") || "";
let pagePathName = $page?.url?.pathname;
let socket: WebSocket | null = null; // Initialize socket as null let socket: WebSocket | null = null; // Initialize socket as null
@ -55,7 +56,19 @@
}, },
volume: { volume: {
label: "Volume", label: "Volume",
step: ["100K", "50K", "20K", "10K", "5K", "2K", "1K", "100", "0"], step: ["100M", "50M", "20M", "10M", "1M", "500K", "200K", "100K", "50K"],
defaultCondition: "over",
defaultValue: "any",
},
sizeVolRatio: {
label: "Size / Volume",
step: ["20%", "15%", "10%", "5%", "3%", "1%"],
defaultCondition: "over",
defaultValue: "any",
},
sizeAvgVolRatio: {
label: "Size / Avg Volume",
step: ["20%", "15%", "10%", "5%", "3%", "1%"],
defaultCondition: "over", defaultCondition: "over",
defaultValue: "any", defaultValue: "any",
}, },
@ -77,27 +90,19 @@
defaultCondition: "over", defaultCondition: "over",
defaultValue: "any", defaultValue: "any",
}, },
option_activity_type: {
label: "Option Type",
step: ["Sweep", "Trade"],
defaultValue: "any",
},
assetType: { assetType: {
label: "Asset Type", label: "Asset Type",
step: ["Stock", "ETF"], step: ["Stock", "ETF"],
defaultValue: "any", defaultValue: "any",
}, },
sector: {
label: "Sector",
step: sectorList,
defaultValue: "any",
},
}; };
const categoricalRules = [ const categoricalRules = ["assetType", "sector"];
"moneyness",
"flowType",
"put_call",
"sentiment",
"execution_estimate",
"option_activity_type",
"assetType",
];
// Generate allRows from allRules // Generate allRows from allRules
$: allRows = Object?.entries(allRules) $: allRows = Object?.entries(allRules)
@ -121,13 +126,6 @@
} }
}); });
// Update ruleCondition and valueMappings based on existing rules
ruleOfList.forEach((rule) => {
ruleCondition[rule.name] =
rule.condition || allRules[rule.name].defaultCondition;
valueMappings[rule.name] = rule.value || allRules[rule.name].defaultValue;
});
async function handleDeleteRule(state) { async function handleDeleteRule(state) {
for (let i = 0; i < ruleOfList.length; i++) { for (let i = 0; i < ruleOfList.length; i++) {
if (ruleOfList[i].name === state) { if (ruleOfList[i].name === state) {
@ -147,7 +145,7 @@
ruleOfList?.some((rule) => rule.name === row.rule), ruleOfList?.some((rule) => rule.name === row.rule),
); );
shouldLoadWorker.set(true); shouldLoadWorker.set(true);
//await saveCookieRuleOfList(); saveRules();
} }
async function handleResetAll() { async function handleResetAll() {
@ -165,7 +163,7 @@
ruleOfList.some((rule) => rule.name === row.rule), ruleOfList.some((rule) => rule.name === row.rule),
); );
displayedData = rawData; displayedData = rawData;
//await saveCookieRuleOfList(); saveRules();
} }
function changeRule(state: string) { function changeRule(state: string) {
@ -380,7 +378,7 @@
// Trigger worker load and save cookie // Trigger worker load and save cookie
shouldLoadWorker.set(true); shouldLoadWorker.set(true);
//await saveCookieRuleOfList(); saveRules();
} }
async function stepSizeValue(value, condition) { async function stepSizeValue(value, condition) {
@ -534,18 +532,13 @@
} }
*/ */
async function saveCookieRuleOfList() { function saveRules() {
const postData = { try {
ruleOfList: ruleOfList, // Save the version along with the rules
}; localStorage?.setItem(pagePathName, JSON?.stringify(ruleOfList));
} catch (e) {
const response = await fetch("/api/options-flow-filter-cookie", { console.log("Failed saving indicator rules: ", e);
method: "POST", }
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify(postData),
}); // make a POST request to the server with the FormData object
} }
/* /*
@ -555,10 +548,36 @@
*/ */
onMount(async () => { onMount(async () => {
try {
const savedRules = localStorage?.getItem(pagePathName);
if (savedRules) {
const parsedRules = JSON.parse(savedRules);
// Compare and update ruleOfList based on allRows
ruleOfList = parsedRules.map((rule) => {
const matchingRow = allRows.find((row) => row.name === rule.name);
if (matchingRow && matchingRow.type !== rule.type) {
return { ...rule, type: matchingRow.type };
}
return rule;
});
}
} catch (e) {
ruleOfList = [];
console.warn(e);
}
// Update ruleCondition and valueMappings based on existing rules
ruleOfList?.forEach((rule) => {
ruleCondition[rule.name] =
rule.condition || allRules[rule.name].defaultCondition;
valueMappings[rule.name] = rule.value || allRules[rule.name].defaultValue;
});
if (filterQuery?.length > 0) { if (filterQuery?.length > 0) {
shouldLoadWorker.set(true); shouldLoadWorker.set(true);
} }
if (ruleOfList?.length !== 0) { if (ruleOfList?.length > 0) {
shouldLoadWorker.set(true); shouldLoadWorker.set(true);
console.log("initial filter"); console.log("initial filter");
} }

View File

@ -1,14 +1,13 @@
import { sectorList } from "$lib/utils";
interface FilterContext { interface FilterContext {
flowTypeCache?: Map<string, number>; flowTypeCache?: Map<string, number>;
} }
const categoricalFields = [ const categoricalFields = [
'put_call', 'assetType',
'sentiment', 'sector',
'execution_estimate',
'option_activity_type',
'underlying_type'
]; ];
function convertUnitToValue( function convertUnitToValue(
@ -22,26 +21,14 @@ function convertUnitToValue(
); );
} }
const lowerInput = input?.toLowerCase(); const lowerInput = input?.toLowerCase();
const nonNumericValues = new Set([ const nonNumericValues = new Set([
"any", "any",
"puts", "stock",
"calls", "etf",
"bullish", ...sectorList.map(sector => sector.toLowerCase()),
"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 (nonNumericValues.has(lowerInput)) return input;
if (input.endsWith("%")) { if (input.endsWith("%")) {
const numericValue = parseFloat(input.slice(0, -1)); const numericValue = parseFloat(input.slice(0, -1));
@ -64,80 +51,18 @@ function convertUnitToValue(
return numericValue; 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) { function createRuleCheck(rule, ruleName, ruleValue) {
const now = new Date(new Date().toLocaleString("en-US", { timeZone: "America/New_York" })); if (rule.value === 'any') return () => true;
if (['sizeVolRatio','sizeAvgVolRatio']?.includes(ruleName)) {
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) => { return (item) => {
const volume = parseFloat(item.volume);
const openInterest = parseFloat(item.open_interest);
if (isNaN(volume) || isNaN(openInterest) || openInterest === 0) { if (isNaN(ruleValue) || ruleValue === 0) {
return false; return false;
} }
const ratio = Math.ceil((volume / openInterest) * 100); const ratio = rule?.value;
// Handle 'between' condition for volume to open interest ratio // Handle 'between' condition for volume to open interest ratio
if (rule.condition === 'between' && Array.isArray(ruleValue)) { if (rule.condition === 'between' && Array.isArray(ruleValue)) {
@ -161,125 +86,15 @@ if (ruleName === 'volumeoiratio') {
} }
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.) // Handle string-based conditions (sentiment, option_type, etc.)
if (categoricalFields?.includes(ruleName)) { if (categoricalFields.includes(rule.name)) {
return (item) => { 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]; const itemValue = item[rule.name];
if (Array.isArray(ruleValue)) {
// Handle array of values for categorical fields return ruleValue.includes(itemValue);
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()
);
} }
return itemValue === ruleValue;
// 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;
}; };
} }