frontend/src/routes/stock-screener/+page.svelte
2024-12-22 11:09:42 +01:00

3815 lines
125 KiB
Svelte

<script lang="ts">
import { onMount, onDestroy } from "svelte";
import { goto } from "$app/navigation";
import {
clearCache,
screenWidth,
numberOfUnreadNotification,
getCache,
setCache,
} from "$lib/store";
import toast from "svelte-french-toast";
import {
abbreviateNumber,
sectorList,
industryList,
listOfRelevantCountries,
groupScreenerRules,
} from "$lib/utils";
import * as DropdownMenu from "$lib/components/shadcn/dropdown-menu/index.js";
import { Button } from "$lib/components/shadcn/button/index.js";
import TableHeader from "$lib/components/Table/TableHeader.svelte";
import DownloadData from "$lib/components/DownloadData.svelte";
import Input from "$lib/components/Input.svelte";
//const userConfirmation = confirm('Unsaved changes detected. Leaving now will discard your strategy. Continue?');
import { writable } from "svelte/store";
let shouldLoadWorker = writable(false);
export let data;
export let form;
let showFilters = true;
let isLoaded = false;
let syncWorker: Worker | undefined;
let downloadWorker: Worker | undefined;
let searchQuery = "";
let infoText = {};
let tooltipTitle;
$: testList = [];
let strategyList = data?.getAllStrategies;
let selectedStrategy = strategyList?.at(0)?.id ?? "";
let ruleOfList = strategyList?.at(0)?.rules ?? [];
let groupedRules = {};
let displayRules = [];
let selectedPopularStrategy = "";
let displayTableTab = "general";
let otherTabRules = [];
let stockScreenerData = data?.getStockScreenerData?.filter((item) =>
Object?.values(item)?.every(
(value) =>
value !== null &&
value !== undefined &&
(typeof value !== "object" ||
Object?.values(value)?.every(
(subValue) => subValue !== null && subValue !== undefined,
)),
),
);
// Define all possible rules and their properties
const allRules = {
avgVolume: {
label: "Average Volume",
step: ["100M", "10M", "1M", "100K", "10K", "1K", "0"],
defaultCondition: "over",
defaultValue: 0,
category: ["Most Popular", "Price & Volume"],
},
volume: {
label: "Volume",
step: ["100M", "10M", "1M", "100K", "10K", "1K", "0"],
defaultCondition: "over",
defaultValue: 0,
category: "Price & Volume",
},
rsi: {
label: "Relative Strength Index",
step: [90, 80, 70, 60, 50, 40, 30, 20],
category: "Technical Analysis",
defaultCondition: "over",
defaultValue: 40,
},
stochRSI: {
label: "Stochastic RSI Fast",
step: [90, 80, 70, 60, 50, 40, 30, 20],
category: "Technical Analysis",
defaultCondition: "over",
defaultValue: 40,
},
mfi: {
label: "Money Flow Index",
step: [90, 80, 70, 60, 50, 40, 30, 20],
category: "Technical Analysis",
defaultCondition: "over",
defaultValue: 40,
},
cci: {
label: "Commodity Channel Index",
step: [250, 200, 100, 50, 20, 0, -20, -50, -100, -200, -250],
category: "Technical Analysis",
defaultCondition: "over",
defaultValue: 0,
},
atr: {
label: "Average True Range",
step: [20, 15, 10, 5, 3, 1],
category: "Technical Analysis",
defaultCondition: "over",
defaultValue: 10,
},
sma20: {
label: "SMA20",
step: [
"Stock Price > SMA20",
"SMA20 > SMA50",
"SMA20 > SMA100",
"SMA20 > SMA200",
],
category: "Technical Analysis",
defaultValue: "any",
},
sma50: {
label: "SMA50",
step: [
"Stock Price > SMA50",
"SMA50 > SMA20",
"SMA50 > SMA100",
"SMA50 > SMA200",
],
category: "Technical Analysis",
defaultValue: "any",
},
sma100: {
label: "SMA100",
step: [
"Stock Price > SMA100",
"SMA100 > SMA20",
"SMA100 > SMA50",
"SMA100 > SMA200",
],
category: "Technical Analysis",
defaultValue: "any",
},
sma200: {
label: "SMA200",
step: [
"Stock Price > SMA200",
"SMA200 > SMA20",
"SMA200 > SMA50",
"SMA200 > SMA100",
],
category: "Technical Analysis",
defaultValue: "any",
},
ema20: {
label: "EMA20",
step: [
"Stock Price > EMA20",
"EMA20 > EMA50",
"EMA20 > EMA100",
"EMA20 > EMA200",
],
category: "Technical Analysis",
defaultValue: "any",
},
ema50: {
label: "EMA50",
step: [
"Stock Price > EMA50",
"EMA50 > EMA20",
"EMA50 > EMA100",
"EMA50 > EMA200",
],
category: "Technical Analysis",
defaultValue: "any",
},
ema100: {
label: "EMA100",
step: [
"Stock Price > EMA100",
"EMA100 > EMA20",
"EMA100 > EMA50",
"EMA100 > EMA200",
],
category: "Technical Analysis",
defaultValue: "any",
},
ema200: {
label: "EMA200",
step: [
"Stock Price > EMA200",
"EMA200 > EMA20",
"EMA200 > EMA50",
"EMA200 > EMA100",
],
category: "Technical Analysis",
defaultValue: "any",
},
grahamNumber: {
label: "Graham Number",
step: ["Price > Graham Number", "Price < Graham Number"],
defaultValue: "any",
},
price: {
label: "Stock Price",
step: [1000, 500, 400, 300, 200, 150, 100, 80, 60, 50, 20, 10, 5, 1],
defaultCondition: "over",
defaultValue: 10,
category: "Price & Volume",
},
change1W: {
label: "Price Change 1W",
step: ["20%", "10%", "5%", "1%", "-1%", "-5%", "-10%", "-20%"],
defaultCondition: "over",
defaultValue: "any",
category: "Performance",
},
change1M: {
label: "Price Change 1M",
step: [
"100%",
"50%",
"20%",
"10%",
"5%",
"1%",
"-1%",
"-5%",
"-10%",
"-20%",
"-50%",
],
defaultCondition: "over",
defaultValue: "any",
category: "Performance",
},
change3M: {
label: "Price Change 3M",
step: [
"100%",
"50%",
"20%",
"10%",
"5%",
"1%",
"-1%",
"-5%",
"-10%",
"-20%",
"-50%",
],
defaultCondition: "over",
defaultValue: "any",
category: "Performance",
},
change6M: {
label: "Price Change 6M",
step: [
"100%",
"50%",
"20%",
"10%",
"5%",
"1%",
"-1%",
"-5%",
"-10%",
"-20%",
"-50%",
],
defaultCondition: "over",
defaultValue: "any",
category: "Performance",
},
change1Y: {
label: "Price Change 1Y",
step: [
"100%",
"50%",
"20%",
"10%",
"5%",
"1%",
"-1%",
"-5%",
"-10%",
"-20%",
"-50%",
],
defaultCondition: "over",
defaultValue: "any",
category: "Performance",
},
change3Y: {
label: "Price Change 3Y",
step: [
"100%",
"50%",
"20%",
"10%",
"5%",
"1%",
"-1%",
"-5%",
"-10%",
"-20%",
"-50%",
],
defaultCondition: "over",
defaultValue: "any",
category: "Performance",
},
marketCap: {
label: "Market Cap",
step: ["100B", "50B", "10B", "1B", "300M", "100M", "10M"],
defaultCondition: "over",
defaultValue: "10M",
category: ["Most Popular", "Valuation & Ratios"],
},
workingCapital: {
label: "Working Capital",
step: ["20B", "10B", "5B", "1B", "500M", "100M", "50M", "10M", "1M", "0"],
defaultCondition: "over",
defaultValue: "any",
},
totalAssets: {
label: "Total Assets",
step: ["500B", "200B", "100B", "50B", "10B", "1B", "100M", "10M"],
defaultCondition: "over",
defaultValue: "any",
},
tangibleAssetValue: {
label: "Tangible Assets",
step: ["500B", "200B", "100B", "50B", "10B", "1B", "100M", "10M"],
defaultCondition: "over",
defaultValue: "any",
},
revenue: {
label: "Revenue",
step: ["100B", "50B", "10B", "1B", "300M", "100M", "10M"],
defaultCondition: "over",
defaultValue: "10M",
category: "Revenue / Sales",
},
revenueGrowthYears: {
label: "Revenue Growth Years",
step: ["10", "5", "3", "1"],
defaultCondition: "over",
defaultValue: "any",
category: "Revenue / Sales",
},
epsGrowthYears: {
label: "EPS Growth Years",
step: ["10", "5", "3", "1"],
defaultCondition: "over",
defaultValue: "any",
category: "Valuation & Ratios",
},
netIncomeGrowthYears: {
label: "Net Income Growth Years",
step: ["10", "5", "3", "1"],
defaultCondition: "over",
defaultValue: "any",
category: "Net Income",
},
grossProfitGrowthYears: {
label: "Gross Profit Growth Years",
step: ["10", "5", "3", "1"],
defaultCondition: "over",
defaultValue: "any",
category: "Other Profits",
},
growthRevenue: {
label: "Revenue Growth",
step: ["200%", "100%", "50%", "20%", "10%", "5%", "1%"],
defaultCondition: "over",
defaultValue: "1%",
category: ["Most Popular", "Revenue / Sales"],
},
costOfRevenue: {
label: "Cost of Revenue",
step: ["100B", "50B", "10B", "1B", "300M", "100M", "10M"],
defaultCondition: "over",
defaultValue: "10M",
},
growthCostOfRevenue: {
label: "Cost of Revenue Growth",
step: ["200%", "100%", "50%", "20%", "10%", "5%", "1%"],
defaultCondition: "over",
defaultValue: "1%",
},
costAndExpenses: {
label: "Cost & Expenses",
step: ["100B", "50B", "10B", "1B", "300M", "100M", "10M"],
defaultCondition: "over",
defaultValue: "10M",
},
growthCostAndExpenses: {
label: "Cost & Expenses Growth",
step: ["200%", "100%", "50%", "20%", "10%", "5%", "1%"],
defaultCondition: "over",
defaultValue: "1%",
},
netIncome: {
label: "Net Income",
step: ["100B", "50B", "10B", "1B", "300M", "100M", "10M"],
defaultCondition: "over",
defaultValue: "10M",
category: "Net Income",
},
growthNetIncome: {
label: "Net Income Growth",
step: ["200%", "100%", "50%", "20%", "10%", "5%", "1%"],
defaultCondition: "over",
defaultValue: "1%",
category: "Net Income",
},
grossProfit: {
label: "Gross Profit",
step: ["100B", "50B", "10B", "1B", "300M", "100M", "10M"],
defaultCondition: "over",
defaultValue: "10M",
category: "Other Profits",
},
growthGrossProfit: {
label: "Gross Profit Growth",
step: ["200%", "100%", "50%", "20%", "10%", "5%", "1%"],
defaultCondition: "over",
defaultValue: "1%",
category: "Other Profits",
},
researchAndDevelopmentExpenses: {
label: "Research & Development",
step: ["10B", "1B", "100M", "10M", "1M", 0],
defaultCondition: "over",
defaultValue: 0,
},
growthResearchAndDevelopmentExpenses: {
label: "R&D Growth",
step: ["200%", "100%", "50%", "20%", "10%", "5%", "1%"],
defaultCondition: "over",
defaultValue: "1%",
},
payoutRatio: {
label: "Payout Ratio",
step: ["100%", "80%", "60%", "40%", "20%", "0%", "-20%", "-40%"],
defaultCondition: "over",
defaultValue: "0%",
category: "Dividends",
},
dividendYield: {
label: "Dividend Yield",
step: ["50%", "20%", "10%", "5%", "1%"],
defaultCondition: "over",
defaultValue: "1%",
category: ["Most Popular", "Dividends"],
},
annualDividend: {
label: "Annual Dividend",
step: [10, 5, 3, 2, 1, 0],
defaultCondition: "over",
defaultValue: "0",
category: "Dividends",
},
dividendGrowth: {
label: "Dividend Growth",
step: ["50%", "20%", "10%", "5%", "3%", "2%", "1%", "0%"],
defaultCondition: "over",
defaultValue: 0,
category: "Dividends",
},
eps: {
label: "EPS",
step: [20, 15, 10, 5, 3, 2, 1, 0],
defaultCondition: "over",
defaultValue: 0,
category: "Valuation & Ratios",
},
growthEPS: {
label: "EPS Growth",
step: ["200%", "100%", "50%", "20%", "10%", "5%", "1%"],
defaultCondition: "over",
defaultValue: "1%",
category: "Valuation & Ratios",
},
interestIncome: {
label: "Interest Income",
step: ["100B", "50B", "10B", "1B", "300M", "100M", "10M"],
defaultCondition: "over",
defaultValue: "10M",
},
interestExpense: {
label: "Interest Expenses",
step: ["100B", "50B", "10B", "1B", "300M", "100M", "10M"],
defaultCondition: "over",
defaultValue: "10M",
},
growthInterestExpense: {
label: "Interest Expenses Growth",
step: ["200%", "100%", "50%", "20%", "10%", "5%", "1%"],
defaultCondition: "over",
defaultValue: "1%",
},
operatingExpenses: {
label: "Operating Expenses",
step: ["100B", "50B", "10B", "1B", "300M", "100M", "10M"],
defaultCondition: "over",
defaultValue: "10M",
},
growthOperatingExpenses: {
label: "Operating Expenses Growth",
step: ["200%", "100%", "50%", "20%", "10%", "5%", "1%"],
defaultCondition: "over",
defaultValue: "1%",
},
ebit: {
label: "EBIT",
step: ["100B", "50B", "10B", "1B", "300M", "100M", "10M"],
defaultCondition: "over",
defaultValue: "10M",
},
operatingIncome: {
label: "Operating Income",
step: ["100B", "50B", "10B", "1B", "300M", "100M", "10M"],
defaultCondition: "over",
defaultValue: "10M",
category: "Other Profits",
},
growthOperatingIncome: {
label: "Operating Income Growth",
step: ["200%", "100%", "50%", "20%", "10%", "5%", "1%"],
defaultCondition: "over",
defaultValue: "1%",
category: "Other Profits",
},
growthFreeCashFlow: {
label: "Free Cash Flow Growth",
step: ["200%", "100%", "50%", "20%", "10%", "5%", "1%"],
defaultCondition: "over",
defaultValue: "1%",
category: "Cash Flow",
},
growthOperatingCashFlow: {
label: "Operating Cash Flow Growth",
step: ["200%", "100%", "50%", "20%", "10%", "5%", "1%"],
defaultCondition: "over",
defaultValue: "1%",
category: "Cash Flow",
},
growthStockBasedCompensation: {
label: "Stock-Based Compensation Growth",
step: ["200%", "100%", "50%", "20%", "10%", "5%", "1%"],
defaultCondition: "over",
defaultValue: "1%",
},
growthTotalLiabilities: {
label: "Total Liabilities Growth",
step: ["200%", "100%", "50%", "20%", "10%", "5%", "1%"],
defaultCondition: "over",
defaultValue: "1%",
category: "Debt",
},
growthTotalDebt: {
label: "Total Debt Growth",
step: ["200%", "100%", "50%", "20%", "10%", "5%", "1%"],
defaultCondition: "over",
defaultValue: "1%",
category: "Debt",
},
growthTotalStockholdersEquity: {
label: "Shareholders Equity Growth",
step: ["200%", "100%", "50%", "20%", "10%", "5%", "1%"],
defaultCondition: "over",
defaultValue: "1%",
category: "Shares Statistics",
},
researchDevelopmentRevenueRatio: {
label: "R&D / Revenue",
step: ["20%", "10%", "5%", "1%", "0%"],
defaultCondition: "over",
defaultValue: "1%",
category: "Revenue / Sales",
},
cagr3YearRevenue: {
label: "Revenue CAGR 3Y",
step: ["200%", "100%", "50%", "20%", "10%", "5%", "1%"],
defaultCondition: "over",
defaultValue: "1%",
category: "Revenue / Sales",
},
cagr5YearRevenue: {
label: "Revenue CAGR 5Y",
step: ["200%", "100%", "50%", "20%", "10%", "5%", "1%"],
defaultCondition: "over",
defaultValue: "1%",
category: "Revenue / Sales",
},
cagr3YearEPS: {
label: "EPS CAGR 3Y",
step: ["200%", "100%", "50%", "20%", "10%", "5%", "1%"],
defaultCondition: "over",
defaultValue: "1%",
category: "Valuation & Ratios",
},
cagr5YearEPS: {
label: "EPS CAGR 5Y",
step: ["200%", "100%", "50%", "20%", "10%", "5%", "1%"],
defaultCondition: "over",
defaultValue: "1%",
category: "Valuation & Ratios",
},
returnOnInvestedCapital: {
label: "Return On Invested Capital",
step: ["100%", "50%", "20%", "10%", "5%", "1%"],
defaultCondition: "over",
defaultValue: "1%",
},
relativeVolume: {
label: "Relative Volume",
step: ["500%", "200%", "100%", "50%", "10%", "0%"],
defaultCondition: "over",
defaultValue: "50%",
category: "Price & Volume",
},
institutionalOwnership: {
label: "Institutional Ownership",
step: ["90%", "80%", "70%", "60%", "50%", "40%", "30%", "20%", "10%"],
defaultCondition: "over",
defaultValue: "10%",
category: "Shares Statistics",
},
pe: {
label: "Price / Earnings (ttm)",
step: [50, 40, 30, 20, 10, 5, 1],
defaultCondition: "over",
defaultValue: 1,
category: "Valuation & Ratios",
},
peg: {
label: "PEG Ratio",
step: [100, 10, 5, 3, 1, 0.5, 0],
defaultCondition: "over",
defaultValue: 0,
category: "Valuation & Ratios",
},
forwardPE: {
label: "Forward PE",
step: [50, 20, 10, 5, 1, 0, -1, -5, -10, -20, -50],
defaultCondition: "over",
defaultValue: 0,
category: "Valuation & Ratios",
},
forwardPS: {
label: "Forward PS",
step: [50, 20, 10, 5, 1, 0],
defaultCondition: "over",
defaultValue: 5,
category: "Valuation & Ratios",
},
priceToBookRatio: {
label: "PB Ratio",
step: [50, 40, 30, 20, 10, 5, 1],
defaultCondition: "over",
defaultValue: 1,
category: "Valuation & Ratios",
},
priceToSalesRatio: {
label: "PS Ratio",
step: [50, 40, 30, 20, 10, 5, 1],
defaultCondition: "over",
defaultValue: 1,
category: "Valuation & Ratios",
},
beta: {
label: "Beta",
step: [10, 5, 1, -5, -10],
defaultCondition: "over",
defaultValue: 1,
category: "Price & Volume",
},
ebitda: {
label: "EBITDA",
step: ["100B", "50B", "10B", "1B", "300M", "100M", "10M"],
defaultCondition: "over",
defaultValue: "10M",
},
growthEBITDA: {
label: "EBITDA Growth",
step: ["200%", "100%", "50%", "20%", "10%", "5%", "1%"],
defaultCondition: "over",
defaultValue: "1%",
},
var: {
label: "Value-at-Risk",
step: ["-1%", "-5%", "-10%", "-15%", "-20%"],
defaultCondition: "over",
defaultValue: "-5%",
category: "Performance",
},
//trendAnalysis: { label: 'AI Trend Analysis', step: ['80%','70%','60%','50%'], category: 'ai', defaultCondition: 'over', defaultValue: '50%' },
//fundamentalAnalysis: { label: 'AI Fundamental Analysis', step: ['80%','70%','60%','50%'], category: 'ai', defaultCondition: 'over', defaultValue: '50%' },
currentRatio: {
label: "Current Ratio",
step: [50, 40, 30, 20, 10, 5, 1],
defaultCondition: "over",
defaultValue: 1,
category: "Valuation & Ratios",
},
quickRatio: {
label: "Quick Ratio",
step: [50, 40, 30, 20, 10, 5, 1],
defaultCondition: "over",
defaultValue: 1,
},
debtEquityRatio: {
label: "Debt / Equity",
step: [50, 40, 30, 20, 10, 5, 1],
defaultCondition: "over",
defaultValue: 1,
category: "Debt",
},
debtRatio: {
label: "Debt Ratio",
step: [1, 0.5, 0, -0.5, -1],
defaultCondition: "over",
defaultValue: -0.5,
category: "Debt",
},
inventoryTurnover: {
label: "Inventory Turnover",
step: [200, 100, 50, 20, 10, 5, 3, 2, 1, 0],
defaultCondition: "over",
defaultValue: "0",
},
returnOnAssets: {
label: "Return on Assets",
step: [10, 8, 6, 4, 2, 1, 0, -2, -4, -6, -8, -10],
defaultCondition: "over",
defaultValue: "0",
},
returnOnEquity: {
label: "Return on Equity",
step: [10, 8, 6, 4, 2, 1, 0, -2, -4, -6, -8, -10],
defaultCondition: "over",
defaultValue: "0",
},
returnOnTangibleAssets: {
label: "Return on Tangible Assets",
step: [10, 8, 6, 4, 2, 1, 0, -2, -4, -6, -8, -10],
defaultCondition: "over",
defaultValue: "0",
},
enterpriseValue: {
label: "Enterprise Value",
step: ["100B", "50B", "10B", "1B", "300M", "100M", "10M"],
defaultCondition: "over",
defaultValue: "10M",
category: "Valuation & Ratios",
},
evSales: {
label: "EV / Sales",
step: [50, 20, 10, 5, 1, 0],
defaultCondition: "over",
defaultValue: 0,
category: "Valuation & Ratios",
},
evEarnings: {
label: "EV / Earnings",
step: [50, 20, 10, 5, 1, 0],
defaultCondition: "over",
defaultValue: 0,
category: "Valuation & Ratios",
},
evEBITDA: {
label: "EV / EBITDA",
step: [50, 20, 10, 5, 1, 0],
defaultCondition: "over",
defaultValue: 0,
category: "Valuation & Ratios",
},
evEBIT: {
label: "EV / EBIT",
step: [50, 20, 10, 5, 1, 0],
defaultCondition: "over",
defaultValue: 0,
category: "Valuation & Ratios",
},
evFCF: {
label: "EV / FCF",
step: [50, 20, 10, 5, 1, 0],
defaultCondition: "over",
defaultValue: 0,
category: "Valuation & Ratios",
},
freeCashFlowPerShare: {
label: "FCF / Share",
step: [10, 8, 6, 4, 2, 1, 0],
defaultCondition: "over",
defaultValue: "0",
category: "Cash Flow",
},
cashPerShare: {
label: "Cash / Share",
step: [50, 20, 10, 5, 1, 0, -1, -5, -10, -20, -50],
defaultCondition: "over",
defaultValue: "0",
category: "Valuation & Ratios",
},
priceToFreeCashFlowsRatio: {
label: "Price / FCF",
step: [50, 20, 10, 5, 1, 0, -1, -5, -10, -20, -50],
defaultCondition: "over",
defaultValue: "0",
category: "Cash Flow",
},
interestCoverage: {
label: "Interest Coverage",
step: [10, 5, 3, 2, 1, 0],
defaultCondition: "over",
defaultValue: "0",
},
sharesShort: {
label: "Short Interest",
step: ["50M", "20M", "10M", "5M", "1M", "500K"],
defaultCondition: "over",
defaultValue: "500K",
category: "Short Selling Statistics",
},
shortRatio: {
label: "Short Ratio",
step: [10, 5, 3, 2, 1, 0],
defaultCondition: "over",
defaultValue: "0",
category: "Short Selling Statistics",
},
shortFloatPercent: {
label: "Short % Float",
step: ["50%", "30%", "20%", "10%", "5%", "1%", "0%"],
defaultCondition: "over",
defaultValue: "0%",
category: "Short Selling Statistics",
},
shortOutStandingPercent: {
label: "Short % Shares",
step: ["50%", "30%", "20%", "10%", "5%", "1%", "0%"],
defaultCondition: "over",
defaultValue: "0%",
category: "Short Selling Statistics",
},
failToDeliver: {
label: "Fail to Deliver (FTD)",
step: ["1M", "500K", "200K", "100K", "50K", "10K", "1K"],
defaultCondition: "over",
defaultValue: "1K",
category: "Short Selling Statistics",
},
relativeFTD: {
label: "FTD / Avg. Volume",
step: ["300%", "200%", "100%", "50%", "20%", "10%", "0%"],
defaultCondition: "over",
defaultValue: "50%",
category: "Short Selling Statistics",
},
freeCashFlow: {
label: "Free Cash Flow",
step: ["50B", "10B", "1B", "100M", "10M", "1M", 0],
defaultCondition: "over",
defaultValue: "0",
category: "Cash Flow",
},
operatingCashFlow: {
label: "Operating Cash Flow",
step: ["50B", "10B", "1B", "100M", "10M", "1M", 0],
defaultCondition: "over",
defaultValue: "0",
category: "Cash Flow",
},
operatingCashFlowPerShare: {
label: "Operating Cash Flow / Share",
step: [50, 40, 30, 10, 5, 1],
defaultCondition: "over",
defaultValue: "1",
category: "Cash Flow",
},
revenuePerShare: {
label: "Revenue / Share",
step: [50, 40, 30, 10, 5, 1],
defaultCondition: "over",
defaultValue: "1",
category: "Valuation & Ratios",
},
netIncomePerShare: {
label: "Net Income / Share",
step: [50, 40, 30, 10, 5, 1],
defaultCondition: "over",
defaultValue: "1",
category: "Valuation & Ratios",
},
shareholdersEquityPerShare: {
label: "Shareholders Equity / Share",
step: [50, 40, 30, 10, 5, 1],
defaultCondition: "over",
defaultValue: "1",
category: "Valuation & Ratios",
},
interestDebtPerShare: {
label: "Interest Debt / Share",
step: [50, 40, 30, 10, 5, 1],
defaultCondition: "over",
defaultValue: "1",
category: "Debt",
},
capexPerShare: {
label: "CapEx / Share",
step: [50, 40, 30, 10, 5, 1],
defaultCondition: "over",
defaultValue: "1",
category: "Valuation & Ratios",
},
freeCashFlowMargin: {
label: "FCF Margin",
step: [
"80%",
"50%",
"20%",
"10%",
"5%",
"0%",
"-5%",
"-10%",
"-20%",
"-50%",
],
defaultCondition: "over",
defaultValue: "0%",
category: "Margins",
},
totalDebt: {
label: "Total Debt",
step: ["200B", "100B", "50B", "10B", "1B", "100M", "10M", "1M"],
defaultCondition: "over",
defaultValue: "1M",
category: "Debt",
},
cashFlowToDebtRatio: {
label: "Cash Flow / Debt",
step: [50, 40, 30, 20, 10, 5, 1],
defaultCondition: "over",
defaultValue: "1",
category: "Debt",
},
operatingCashFlowSalesRatio: {
label: "Operating Cash Flow / Sales",
step: [5, 3, 1, 0.5, 0],
defaultCondition: "over",
defaultValue: "0",
category: "Valuation & Ratios",
},
priceCashFlowRatio: {
label: "Price / Cash Flow",
step: [20, 15, 10, 5, 3, 1, 0],
defaultCondition: "over",
defaultValue: "0",
category: "Valuation & Ratios",
},
priceEarningsRatio: {
label: "Price / Earnings",
step: [100, 50, 20, 10, 5, 0],
defaultCondition: "over",
defaultValue: "0",
category: ["Most Popular", "Valuation & Ratios"],
},
priceEarningsToGrowthRatio: {
label: "Price / Earnings Growth",
step: [10, 5, 3, 2, 1, 0],
defaultCondition: "over",
defaultValue: "0",
category: "Valuation & Ratios",
},
stockBasedCompensation: {
label: "Stock-Based Compensation",
step: ["10B", "1B", "100M", "10M", "1M", 0],
defaultCondition: "over",
defaultValue: "0",
},
totalStockholdersEquity: {
label: "Shareholders Equity",
step: ["100B", "50B", "10B", "1B", "100M", "50M", "10M", "1M", 0],
defaultCondition: "over",
defaultValue: "0",
category: "Shares Statistics",
},
sharesQoQ: {
label: "Shares Change (QoQ)",
step: ["80%", "60%", "50%", "20%", "10%", "5%", "1%", "0.5%"],
defaultCondition: "over",
defaultValue: "1%",
category: "Shares Statistics",
},
sharesYoY: {
label: "Shares Change (YoY)",
step: ["80%", "60%", "50%", "20%", "10%", "5%", "1%", "0.5%"],
defaultCondition: "over",
defaultValue: "1%",
category: "Shares Statistics",
},
grossProfitMargin: {
label: "Gross Margin",
step: ["80%", "60%", "50%", "20%", "10%", "5%", "1%", "0.5%"],
defaultCondition: "over",
defaultValue: "1%",
category: "Margins",
},
netProfitMargin: {
label: "Profit Margin",
step: ["80%", "60%", "50%", "20%", "10%", "5%", "1%", "0.5%"],
defaultCondition: "over",
defaultValue: "1%",
category: "Margins",
},
pretaxProfitMargin: {
label: "Pretax Margin",
step: ["80%", "60%", "50%", "20%", "10%", "5%", "1%", "0.5%"],
defaultCondition: "over",
defaultValue: "1%",
category: "Margins",
},
ebitdaMargin: {
label: "EBITDA Margin",
step: ["80%", "60%", "50%", "20%", "10%", "5%", "1%", "0.5%"],
defaultCondition: "over",
defaultValue: "1%",
category: "Margins",
},
ebitMargin: {
label: "EBIT Margin",
step: ["80%", "60%", "50%", "20%", "10%", "5%", "1%", "0.5%"],
defaultCondition: "over",
defaultValue: "1%",
category: "Margins",
},
operatingMargin: {
label: "Operating Margin",
step: ["80%", "60%", "50%", "20%", "10%", "5%", "1%", "0.5%"],
defaultCondition: "over",
defaultValue: "1%",
category: "Margins",
},
longTermDebtToCapitalization: {
label: "Long Term Debt / Market Cap",
step: ["80%", "60%", "50%", "30%", "20%", "10%", "5%", "1%", "0.5%"],
defaultCondition: "over",
defaultValue: "1%",
category: "Debt",
},
shortTermDebtToCapitalization: {
label: "Short Term Debt / Market Cap",
step: ["80%", "60%", "50%", "30%", "20%", "10%", "5%", "1%", "0.5%"],
defaultCondition: "over",
defaultValue: "1%",
category: "Debt",
},
interestIncomeToCapitalization: {
label: "Interest Income / Market Cap",
step: ["80%", "60%", "50%", "30%", "20%", "10%", "5%", "1%", "0.5%"],
defaultCondition: "over",
defaultValue: "1%",
category: "Valuation & Ratios",
},
assetTurnover: {
label: "Asset Turnover",
step: [5, 3, 2, 1, 0],
defaultCondition: "over",
defaultValue: 0,
},
earningsYield: {
label: "Earnings Yield",
step: ["20%", "15%", "10%", "5%", "0%"],
defaultCondition: "over",
defaultValue: "0%",
category: "Valuation & Ratios",
},
freeCashFlowYield: {
label: "FCF Yield",
step: ["20%", "15%", "10%", "5%", "0%"],
defaultCondition: "over",
defaultValue: "0%",
category: "Valuation & Ratios",
},
effectiveTaxRate: {
label: "Effective Tax Rate",
step: ["20%", "15%", "10%", "5%", "0%"],
defaultCondition: "over",
defaultValue: "0%",
},
fixedAssetTurnover: {
label: "Fixed Asset Turnover",
step: [10, 5, 3, 2, 1, 0],
defaultCondition: "over",
defaultValue: "0",
},
sharesOutStanding: {
label: "Shares Outstanding",
step: ["10B", "5B", "1B", "100M", "50M", "10M", "1M"],
defaultCondition: "over",
defaultValue: "1M",
category: "Shares Statistics",
},
employees: {
label: "Employees",
step: ["500K", "300K", "200K", "100K", "10K", "1K", "100"],
defaultCondition: "over",
defaultValue: "100K",
category: "Company Info",
},
revenuePerEmployee: {
label: "Revenue Per Employee",
step: ["5M", "3M", "2M", "1M", "500K", "100K", 0],
defaultCondition: "over",
defaultValue: "0",
category: "Company Info",
},
profitPerEmployee: {
label: "Profit Per Employee",
step: ["5M", "3M", "2M", "1M", "500K", "100K", 0],
defaultCondition: "over",
defaultValue: "0",
category: "Company Info",
},
totalLiabilities: {
label: "Total Liabilities",
step: ["500B", "200B", "100B", "50B", "10B", "1B", "100M", "10M", "1M"],
defaultCondition: "over",
defaultValue: "1M",
category: "Debt",
},
altmanZScore: {
label: "Altman-Z-Score",
step: [10, 5, 3, 1],
defaultCondition: "over",
defaultValue: "any",
},
piotroskiScore: {
label: "Piotroski F-Score",
step: [9, 8, 7, 6, 5, 4, 3, 2, 1],
defaultCondition: "over",
defaultValue: "any",
},
analystRating: {
label: "Analyst Rating",
step: ["Strong Buy", "Buy", "Hold", "Sell", "Strong Sell"],
defaultCondition: "",
defaultValue: "any",
category: ["Most Popular", "Forecasts, Analysts & Price Targets"],
},
analystCounter: {
label: "Analyst Count",
step: ["40", "30", "20", "10", "5", "0"],
defaultCondition: "over",
defaultValue: "any",
category: "Forecasts, Analysts & Price Targets",
},
priceTarget: {
label: "Price Target",
step: ["1000", "500", "100", "10", "5", "1"],
defaultCondition: "over",
defaultValue: "any",
category: "Forecasts, Analysts & Price Targets",
},
upside: {
label: "Price Target Upside",
step: ["100%", "50%", "20%", "10%", "5%", "0%"],
defaultCondition: "over",
defaultValue: "any",
category: "Forecasts, Analysts & Price Targets",
},
topAnalystRating: {
label: "Top Analyst Rating",
step: ["Strong Buy", "Buy", "Hold", "Sell", "Strong Sell"],
defaultCondition: "",
defaultValue: "any",
category: "Forecasts, Analysts & Price Targets",
},
topAnalystCounter: {
label: "Top Analyst Count",
step: ["10", "5", "3", "1"],
defaultCondition: "over",
defaultValue: "any",
category: "Forecasts, Analysts & Price Targets",
},
topAnalystUpside: {
label: "Top Analyst Price Target Upside",
step: ["100%", "50%", "20%", "10%", "5%", "0%"],
defaultCondition: "over",
defaultValue: "any",
category: "Forecasts, Analysts & Price Targets",
},
topAnalystPriceTarget: {
label: "Top Analyst Price Target",
step: ["1000", "500", "100", "10", "5", "1"],
defaultCondition: "over",
defaultValue: "any",
category: "Forecasts, Analysts & Price Targets",
},
halalStocks: {
label: "Halal Stocks",
step: ["Compliant", "Non-Compliant"],
defaultCondition: "",
defaultValue: "any",
},
score: {
label: "AI Score",
step: ["Strong Buy", "Buy", "Hold", "Sell", "Strong Sell"],
defaultCondition: "",
defaultValue: "any",
category: ["Most Popular", "Forecasts, Analysts & Price Targets"],
},
sector: {
label: "Sector",
step: sectorList,
defaultCondition: "",
defaultValue: "any",
category: "Company Info",
},
industry: {
label: "Industry",
step: industryList,
defaultCondition: "",
defaultValue: "any",
category: "Company Info",
},
country: {
label: "Country",
step: listOfRelevantCountries,
defaultCondition: "",
defaultValue: "any",
category: "Company Info",
},
};
let filteredData = [];
let displayResults = [];
let isSaved = false;
// Generate allRows from allRules
$: allRows = Object?.entries(allRules)
?.sort(([, a], [, b]) => a.label.localeCompare(b.label)) // Sort by label
?.map(([ruleName, ruleProps]) => ({
rule: ruleName,
...ruleProps,
}));
let filteredGroupedRules;
let searchTerm = "";
let ruleName = "";
// Define your default values
let ruleCondition = {};
let valueMappings = {};
Object.keys(allRules).forEach((ruleName) => {
ruleCondition[ruleName] = allRules[ruleName].defaultCondition;
// 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
ruleOfList?.forEach((rule) => {
ruleCondition[rule.name] =
rule.condition || allRules[rule.name].defaultCondition;
valueMappings[rule.name] = rule.value || allRules[rule.name].defaultValue;
});
async function getInfoText(parameter, title) {
tooltipTitle = title;
const cachedData = getCache(parameter, "getInfoText");
if (cachedData) {
infoText = cachedData;
} else {
const postData = { parameter };
const response = await fetch("/api/info-text", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify(postData),
});
infoText = await response.json();
setCache(parameter, infoText, "getInfoText");
}
}
async function handleCreateStrategy() {
if (data?.user?.tier === "Pro") {
const closePopup = document.getElementById("addStrategy");
closePopup?.dispatchEvent(new MouseEvent("click"));
} else {
goto("/pricing");
}
}
async function handleDeleteStrategy() {
const postData = { strategyId: selectedStrategy };
const response = await fetch("/api/delete-strategy", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify(postData),
});
const output = await response.json();
if (output === "success") {
toast.success("Strategy deleted successfully!", {
style: "border-radius: 200px; background: #2A2E39; color: #fff;",
});
strategyList =
strategyList?.filter((item) => item?.id !== selectedStrategy) ?? [];
selectedStrategy = strategyList?.at(0)?.id ?? "";
ruleOfList =
strategyList?.find((item) => item.id === selectedStrategy)?.rules ?? [];
ruleOfList.forEach((rule) => {
ruleCondition[rule.name] =
rule.condition || allRules[rule.name].defaultCondition;
valueMappings[rule.name] =
rule.value || allRules[rule.name].defaultValue;
});
if (ruleOfList?.length === 0) {
filteredData = [];
displayResults = [];
}
await updateStockScreenerData();
checkedItems = new Map(
ruleOfList
?.filter((rule) =>
[
"analystRating",
"topAnalystRating",
"halalStocks",
"sector",
"country",
"score",
"industry",
"grahamNumber",
]?.includes(rule.name),
) // Only include specific rules
?.map((rule) => [rule.name, new Set(rule.value)]), // Create Map from filtered rules
);
} else if (output === "failure") {
toast.error("Something went wrong. Please try again", {
style: "border-radius: 200px; background: #2A2E39; color: #fff;",
});
}
}
async function createStrategy(event) {
event.preventDefault();
const formData = new FormData(event.target); // create a FormData object from the form
formData.append("user", data?.user?.id);
formData.append("rules", "[]");
const title = formData.get("title");
if (!title || title.length === 0) {
toast.error("Title cannot be empty!", {
style: "border-radius: 200px; background: #2A2E39; color: #fff;",
});
return;
}
if (title?.length > 100) {
toast.error("Title is too long. Keep it simple and concise bruv!", {
style: "border-radius: 200px; background: #2A2E39; color: #fff;",
});
return;
}
const postData = {};
// Iterate through the FormData entries and populate the object
for (const [key, value] of formData.entries()) {
postData[key] = value;
}
const response = await fetch("/api/create-strategy", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify(postData),
});
const output = await response?.json();
if (output?.id && output?.id?.length !== 0) {
toast.success("Strategy created successfully!", {
style: "border-radius: 200px; background: #2A2E39; color: #fff;",
});
const closePopup = document.getElementById("addStrategy");
closePopup?.dispatchEvent(new MouseEvent("click"));
selectedStrategy = output?.id;
if (strategyList?.length !== 0) {
ruleOfList = [];
}
strategyList?.unshift(output);
selectedPopularStrategy = "";
} else {
toast.error("Something went wrong. Please try again later!", {
style: "border-radius: 200px; background: #2A2E39; color: #fff;",
});
}
return output;
}
async function switchStrategy(item) {
displayTableTab = "general";
ruleName = "";
selectedPopularStrategy = "";
selectedStrategy = item?.id ?? "";
ruleOfList =
strategyList?.find((item) => item.id === selectedStrategy)?.rules ?? [];
ruleOfList.forEach((rule) => {
ruleCondition[rule.name] =
rule.condition || allRules[rule.name].defaultCondition;
valueMappings[rule.name] = rule.value || allRules[rule.name].defaultValue;
});
if (ruleOfList?.length === 0) {
filteredData = [];
displayResults = [];
}
await updateStockScreenerData();
checkedItems = new Map(
ruleOfList
?.filter((rule) =>
[
"analystRating",
"topAnalystRating",
"halalStocks",
"sector",
"country",
"score",
"industry",
"grahamNumber",
]?.includes(rule.name),
) // Only include specific rules
?.map((rule) => [rule.name, new Set(rule.value)]), // Create Map from filtered rules
);
}
function changeRule(state: string) {
if (data?.user?.tier !== "Pro" && ['topAnalystRating','topAnalystCounter','topAnalystPriceTarget','topAnalystUpside','score']?.includes(state)) {
goto("/pricing");
} else {
selectedPopularStrategy = "";
ruleName = state;
handleAddRule();
}
}
const handleMessage = (event) => {
displayRules = allRows?.filter((row) =>
ruleOfList?.some((rule) => rule.name === row.rule),
);
filteredData = event.data?.filteredData ?? [];
displayResults = filteredData?.slice(0, 50);
};
const handleScreenerMessage = (event) => {
stockScreenerData = event?.data?.stockScreenerData;
shouldLoadWorker.set(true);
};
const loadWorker = async () => {
if (['performance', 'analysts']?.includes(displayTableTab) || hoverStatus) {
syncWorker.postMessage({
stockScreenerData,
ruleOfList: [...ruleOfList, ...otherTabRules],
});
} else {
syncWorker.postMessage({
stockScreenerData,
ruleOfList,
});
}
};
const updateStockScreenerData = async () => {
if (["performance","analysts"]?.includes(displayTableTab) || hoverStatus) {
downloadWorker.postMessage({
ruleOfList: [...ruleOfList, ...otherTabRules],
});
} else {
downloadWorker.postMessage({ ruleOfList: ruleOfList });
}
};
function handleAddRule() {
if (ruleName === "") {
toast.error("Please select a rule", {
style: "border-radius: 200px; background: #2A2E39; color: #fff;",
});
return;
}
let newRule;
switch (ruleName) {
case "analystRating":
case "topAnalystRating":
case "halalStocks":
case "score":
case "sector":
case "industry":
case "country":
case "ema20":
case "ema50":
case "ema100":
case "ema200":
case "sma20":
case "grahamNumber":
case "sma50":
case "sma100":
case "sma200":
newRule = {
name: ruleName,
value: Array.isArray(valueMappings[ruleName])
? valueMappings[ruleName]
: [valueMappings[ruleName]],
}; // Ensure value is an array
break;
default:
newRule = {
name: ruleName,
condition: ruleCondition[ruleName],
value: valueMappings[ruleName],
};
break;
}
handleRule(newRule);
}
async function handleRule(newRule) {
const existingRuleIndex = ruleOfList.findIndex(
(rule) => rule.name === newRule.name,
);
if (existingRuleIndex !== -1) {
const existingRule = ruleOfList[existingRuleIndex];
if (existingRule.name === newRule.name) {
// Remove the rule instead of showing an error
ruleOfList.splice(existingRuleIndex, 1);
ruleOfList = [...ruleOfList]; // Trigger reactivity
} else {
ruleOfList[existingRuleIndex] = newRule;
ruleOfList = [...ruleOfList]; // Trigger reactivity
}
} else {
ruleOfList = [...ruleOfList, newRule];
/*
toast.success('Rule added', {
style: 'border-radius: 200px; background: #2A2E39; color: #fff;'
});
*/
await updateStockScreenerData();
}
}
async function handleResetAll() {
selectedPopularStrategy = "";
displayTableTab = "general";
ruleOfList = [];
Object?.keys(allRules)?.forEach((ruleName) => {
ruleCondition[ruleName] = allRules[ruleName].defaultCondition;
valueMappings[ruleName] = allRules[ruleName].defaultValue;
});
ruleName = "";
filteredData = [];
displayResults = [];
checkedItems = new Map();
ruleOfList = [...ruleOfList];
await updateStockScreenerData();
//await handleSave(false);
}
async function handleDeleteRule(state) {
selectedPopularStrategy = "";
// Find the index of the rule to be deleted
const index = ruleOfList?.findIndex((rule) => rule.name === state);
if (index !== -1) {
// Remove the element at the found index
ruleOfList?.splice(index, 1);
}
if (ruleOfList?.length === 0) {
ruleName = "";
filteredData = [];
displayResults = [];
} else if (state === ruleName) {
ruleName = "";
}
// Only create a new array if there are remaining rules
ruleOfList = [...ruleOfList];
await updateStockScreenerData();
//await handleSave(false);
}
async function handleScroll() {
const scrollThreshold = document.body.offsetHeight * 0.8; // 80% of the website height
const isBottom = window.innerHeight + window.scrollY >= scrollThreshold;
if (isBottom && displayResults?.length !== filteredData?.length) {
const nextIndex = displayResults?.length;
const filteredNewResults = filteredData?.slice(nextIndex, nextIndex + 30);
displayResults = [...displayResults, ...filteredNewResults];
}
}
/*
const handleKeyDown = (event) => {
if (event.ctrlKey && event.key === 's') {
event.preventDefault(); // prevent the browser's default save action
handleSave();
}
};
*/
let LoginPopup;
onMount(async () => {
if (!syncWorker) {
const SyncWorker = await import("./workers/filterWorker?worker");
syncWorker = new SyncWorker.default();
syncWorker.onmessage = handleMessage;
}
if (!downloadWorker) {
const DownloadWorker = await import("./workers/downloadWorker?worker");
downloadWorker = new DownloadWorker.default();
downloadWorker.onmessage = handleScreenerMessage;
}
if (!data?.user) {
LoginPopup = (await import("$lib/components/LoginPopup.svelte")).default;
}
shouldLoadWorker.subscribe(async (value) => {
if (value) {
isLoaded = false;
await loadWorker();
shouldLoadWorker.set(false); // Reset after worker is loaded
isLoaded = true;
}
});
groupedRules = groupScreenerRules(allRows);
});
onDestroy(() => {
syncWorker?.terminate();
syncWorker = undefined;
clearCache();
});
async function handleSave(printToast) {
if (data?.user) {
if (isSaved === false && strategyList?.length > 0) {
strategyList.find((item) => item.id === selectedStrategy).rules =
ruleOfList;
const postData = {
strategyId: selectedStrategy,
rules: ruleOfList,
};
const response = await fetch("/api/save-strategy", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify(postData),
});
if (printToast === true) {
toast.success("Strategy saved!", {
style: "border-radius: 200px; background: #2A2E39; color: #fff;",
});
}
//isSaved = true;
}
}
}
$: {
if (ruleOfList ) {
const ruleToUpdate = ruleOfList?.find((rule) => rule.name === ruleName);
if (ruleToUpdate) {
ruleToUpdate.value = valueMappings[ruleToUpdate.name];
ruleToUpdate.condition = ruleCondition[ruleToUpdate.name];
ruleOfList = [...ruleOfList];
}
shouldLoadWorker.set(true);
}
}
$: {
if (searchTerm?.length > 0) {
// Filter rows by search term
const filteredRows = allRows?.filter((row) =>
row?.label?.toLowerCase()?.includes(searchTerm?.toLowerCase()),
);
// Group the filtered rows by category
filteredGroupedRules = groupScreenerRules(filteredRows);
} else {
// If no search term, return all rows grouped by category
filteredGroupedRules = groupScreenerRules(allRows);
}
}
$: isSaved = !ruleOfList;
$: charNumber = $screenWidth < 640 ? 20 : 40;
function changeRuleCondition(name: string, state: string) {
ruleName = name;
if (
ruleCondition[ruleName] === "between" &&
["over", "under", "exactly"]?.includes(state?.toLowerCase())
) {
valueMappings[ruleName] = "";
}
ruleCondition[ruleName] = state?.toLowerCase();
}
let checkedItems = new Map(
ruleOfList
?.filter((rule) =>
[
"analystRating",
"topAnalystRating",
"halalStocks",
"sector",
"country",
"score",
"industry",
"grahamNumber",
]?.includes(rule.name),
) // Only include specific rules
?.map((rule) => [rule.name, new Set(rule.value)]), // Create Map from filtered rules
);
function isChecked(item, ruleName) {
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);
}
async function handleChangeValue(value, { shouldSort = true } = {}) {
if (checkedItems.has(ruleName)) {
const itemsSet = checkedItems.get(ruleName);
// Apply sorting only if shouldSort is true
const sortedValue =
shouldSort && Array.isArray(value) ? value.sort(customSort) : value;
const valueKey = Array.isArray(sortedValue)
? sortedValue.join("-")
: sortedValue;
if (itemsSet?.has(valueKey)) {
itemsSet?.delete(valueKey);
} else {
itemsSet?.add(valueKey);
}
} else {
// Apply sorting only if shouldSort is true
const sortedValue =
shouldSort && Array.isArray(value) ? value.sort(customSort) : value;
const valueKey = Array.isArray(sortedValue)
? sortedValue.join("-")
: sortedValue;
checkedItems?.set(ruleName, new Set([valueKey]));
}
if (
[
"sma20",
"sma50",
"sma100",
"sma200",
"ema20",
"ema50",
"ema100",
"ema200",
"grahamNumber",
"analystRating",
"topAnalystRating",
"halalStocks",
"score",
"sector",
"industry",
"country",
]?.includes(ruleName)
) {
searchQuery = "";
if (!Array.isArray(valueMappings[ruleName])) {
valueMappings[ruleName] = [];
}
// Apply sorting only if shouldSort is true
const sortedValue =
shouldSort && Array?.isArray(value) ? value?.sort(customSort) : value;
const valueKey = Array?.isArray(sortedValue)
? sortedValue.join("-")
: sortedValue;
const index = valueMappings[ruleName].indexOf(valueKey);
if (index === -1) {
valueMappings[ruleName].push(valueKey);
} else {
valueMappings[ruleName].splice(index, 1);
}
if (valueMappings[ruleName].length === 0) {
valueMappings[ruleName] = "any";
}
await updateStockScreenerData();
} else if (ruleName in valueMappings) {
if (ruleCondition[ruleName] === "between" && Array?.isArray(value)) {
// Apply sorting only if shouldSort is true
valueMappings[ruleName] = shouldSort ? value?.sort(customSort) : value;
} else {
valueMappings[ruleName] = value;
}
} else {
console.warn(`Unhandled rule: ${ruleName}`);
}
}
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, { shouldSort: false });
} else {
await handleChangeValue(newValue);
}
}
async function popularStrategy(state: string) {
ruleOfList = [];
const strategies = {
dividendGrowth: {
name: "Dividend Growth",
rules: [
{ condition: "over", name: "dividendGrowth", value: "5%" },
{ condition: "over", name: "dividendYield", value: "1%" },
{ condition: "under", name: "payoutRatio", value: "60%" },
{ condition: "over", name: "growthRevenue", value: "5%" },
],
},
topGainers1Y: {
name: "Top Gainers 1Y",
rules: [
{ condition: "over", name: "change1Y", value: "50%" },
{ condition: "over", name: "marketCap", value: "10B" },
{ condition: "over", name: "eps", value: 5 },
],
},
topShortedStocks: {
name: "Top Shorted Stocks",
rules: [
{ condition: "over", name: "shortFloatPercent", value: "20%" },
{ condition: "over", name: "shortRatio", value: 1 },
{ condition: "over", name: "shortOutStandingPercent", value: "10%" },
{ condition: "over", name: "sharesShort", value: "20M" },
{ condition: "over", name: "marketCap", value: "100M" },
],
},
momentumTAStocks: {
name: "Momentum TA Stocks",
rules: [
{ condition: "under", name: "rsi", value: 40 },
{ condition: "under", name: "stochRSI", value: 40 },
{ condition: "over", name: "marketCap", value: "1B" },
{ condition: "under", name: "mfi", value: 40 },
],
},
underValuedStocks: {
name: "Undervalued Stocks",
rules: [
{ condition: "under", name: "marketCap", value: "100M" },
{ condition: "over", name: "debtEquityRatio", value: 1 },
{ condition: "over", name: "debtRatio", value: -0.5 },
{ condition: "over", name: "eps", value: 0 },
],
},
strongCashFlow: {
// New Strategy Added
name: "Strong Cash Flow",
rules: [
{ condition: "over", name: "marketCap", value: "1B" },
{ condition: "over", name: "freeCashFlow", value: "50B" },
{ condition: "over", name: "operatingCashFlowPerShare", value: 5 },
{ condition: "over", name: "operatingCashFlow", value: "50B" },
{ condition: "over", name: "freeCashFlowPerShare", value: 2 },
{ condition: "over", name: "freeCashFlowMargin", value: "50%" },
],
},
};
const strategy = strategies[state];
if (strategy) {
selectedPopularStrategy = strategy.name;
ruleOfList = strategy?.rules;
ruleOfList?.forEach((row) => {
ruleName = row?.name;
ruleCondition[ruleName] = row?.condition;
handleChangeValue(row?.value);
});
await updateStockScreenerData();
}
}
function handleInput(event) {
const searchQuery = event.target.value?.toLowerCase() || "";
setTimeout(() => {
testList = [];
if (searchQuery.length > 0) {
const rawList =
ruleName === "country"
? listOfRelevantCountries
: ruleName === "sector"
? sectorList
: ruleName === "industry"
? industryList
: ['analystRating','topAnalystRating','score']?.includes(ruleName)
? ["Strong Buy", "Buy", "Hold", "Sell", "Strong Sell"]
: ["Compliant", "Non-Compliant"];
testList =
rawList?.filter((item) => {
const index = item?.toLowerCase();
// Check if country starts with searchQuery
return index?.startsWith(searchQuery);
}) || [];
}
}, 50);
}
const sortData = (key) => {
// Reset all other keys to 'none' except the current key
for (const k in sortOrders) {
if (k !== key) {
sortOrders[k].order = "none";
}
}
// Cycle through 'none', 'asc', 'desc' for the clicked key
const orderCycle = ["none", "asc", "desc"];
let originalData = filteredData;
const currentOrderIndex = orderCycle.indexOf(sortOrders[key].order);
sortOrders[key].order =
orderCycle[(currentOrderIndex + 1) % orderCycle.length];
const sortOrder = sortOrders[key].order;
// Reset to original data when 'none' and stop further sorting
if (sortOrder === "none") {
displayResults = [...originalData]?.slice(0, 50); // Reset to original data (spread to avoid mutation)
return;
}
// Define a generic comparison function
const compareValues = (a, b) => {
const { type } = sortOrders[key];
let valueA, valueB;
switch (type) {
case "date":
valueA = new Date(a[key]);
valueB = new Date(b[key]);
break;
case "string":
valueA = a[key].toUpperCase();
valueB = b[key].toUpperCase();
return sortOrder === "asc"
? valueA.localeCompare(valueB)
: valueB.localeCompare(valueA);
case "number":
default:
valueA = parseFloat(a[key]);
valueB = parseFloat(b[key]);
break;
}
if (sortOrder === "asc") {
return valueA < valueB ? -1 : valueA > valueB ? 1 : 0;
} else {
return valueA > valueB ? -1 : valueA < valueB ? 1 : 0;
}
};
// Sort using the generic comparison function
displayResults = [...originalData].sort(compareValues)?.slice(0, 50);
};
let columns;
let sortOrders;
// Initial columns and sort orders for the "general" tab
const generalColumns = [
{ key: "symbol", label: "Symbol", align: "left" },
{ key: "name", label: "Name", align: "left" },
{ key: "marketCap", label: "Market Cap", align: "right" },
{ key: "price", label: "Price", align: "right" },
{ key: "changesPercentage", label: "% Change", align: "right" },
{ key: "volume", label: "Volume", align: "right" },
{ key: "pe", label: "PE Ratio", align: "right" },
];
const generalSortOrders = {
symbol: { order: "none", type: "string" },
name: { order: "none", type: "string" },
marketCap: { order: "none", type: "number" },
changesPercentage: { order: "none", type: "number" },
price: { order: "none", type: "number" },
volume: { order: "none", type: "number" },
pe: { order: "none", type: "number" },
};
const stringTypeRules = [
"country",
"industry",
"score",
"sector",
"analystRating",
"topAnalystRating",
"halalStocks",
];
// Helper to determine the type based on stringTypeRules
const getType = (key) =>
stringTypeRules.includes(key) ? "string" : "number";
$: {
if (displayTableTab) {
const baseColumnsMap = {
performance: [
{ key: "symbol", label: "Symbol", align: "left" },
{ key: "name", label: "Name", align: "left" },
{ key: "marketCap", label: "Market Cap", align: "right" },
],
analysts: [
{ key: "symbol", label: "Symbol", align: "left" },
{ key: "name", label: "Name", align: "left" },
{ key: "marketCap", label: "Market Cap", align: "right" },
],
filters: [
{ key: "symbol", label: "Symbol", align: "left" },
{ key: "name", label: "Name", align: "left" },
{ key: "marketCap", label: "Market Cap", align: "right" },
],
};
const baseSortOrdersMap = {
performance: {
symbol: { order: "none", type: "string" },
name: { order: "none", type: "string" },
marketCap: { order: "none", type: "number" },
},
analysts: {
symbol: { order: "none", type: "string" },
name: { order: "none", type: "string" },
marketCap: { order: "none", type: "number" },
},
filters: {
symbol: { order: "none", type: "string" },
name: { order: "none", type: "string" },
marketCap: { order: "none", type: "number" },
},
};
if (displayTableTab === "general") {
columns = [...generalColumns];
sortOrders = { ...generalSortOrders };
} else {
columns = [...(baseColumnsMap[displayTableTab] || [])];
sortOrders = { ...(baseSortOrdersMap[displayTableTab] || {}) };
const rulesList =
["performance","analysts"]?.includes(displayTableTab) ? tabRuleList : displayRules;
rulesList?.forEach((rule) => {
if (rule.rule !== "marketCap") {
columns.push({
key: rule.rule,
label: rule.label,
align: "right",
});
sortOrders[rule.rule] = { order: "none", type: getType(rule.rule) };
}
});
}
}
}
let tabRuleList = [];
let hoverStatus = false;
async function changeTab(state) {
displayTableTab = state;
if (displayTableTab === "performance") {
hoverStatus = false;
otherTabRules = [
{ name: "marketCap", value: "any" },
{ name: "change1W", value: "any" },
{ name: "change1M", value: "any" },
{ name: "change3M", value: "any" },
{ name: "change1Y", value: "any" },
];
tabRuleList = otherTabRules
.map((rule) => allRows.find((row) => row.rule === rule.name))
.filter(Boolean);
await updateStockScreenerData();
} else if (displayTableTab === "analysts") {
hoverStatus = false;
otherTabRules = [
{ name: "marketCap", value: "any" },
{ name: "analystRating", value: "any" },
{ name: "analystCounter", value: "any" },
{ name: "priceTarget", value: "any" },
{ name: "upside", value: "any" },
];
tabRuleList = otherTabRules
.map((rule) => allRows.find((row) => row.rule === rule.name))
.filter(Boolean);
await updateStockScreenerData();
}
}
/*
async function handleMouseOver() {
if (displayTableTab !== "performance") {
hoverStatus = true;
otherTabRules = [
{ name: "marketCap", value: "any" },
{ name: "change1W", value: "any" },
{ name: "change1M", value: "any" },
{ name: "change3M", value: "any" },
{ name: "change1Y", value: "any" },
];
tabRuleList = otherTabRules
?.map((rule) => allRows.find((row) => row.rule === rule.name))
?.filter(Boolean);
await updateStockScreenerData();
} else if (displayTableTab !== "analysts") {
hoverStatus = true;
otherTabRules = [
{ name: "marketCap", value: "any" },
{ name: "analystRating", value: "any" },
{ name: "analystCounter", value: "any" },
{ name: "priceTarget", value: "any" },
{ name: "upside", value: "any" },
];
tabRuleList = otherTabRules
?.map((rule) => allRows.find((row) => row.rule === rule.name))
?.filter(Boolean);
await updateStockScreenerData();
}
}
*/
</script>
<svelte:head>
<meta charset="utf-8" />
<meta
name="viewport"
content="width=device-width, initial-scale=1.0, maximum-scale=1.0"
/>
<title>
{$numberOfUnreadNotification > 0 ? `(${$numberOfUnreadNotification})` : ""} Stock
Screener · Stocknear
</title>
<meta
name="description"
content={`Build your Stock Screener to find profitable stocks.`}
/>
<!-- Other meta tags -->
<meta property="og:title" content={`Stock Screener · Stocknear`} />
<meta
property="og:description"
content={`Build your Stock Screener to find profitable stocks.`}
/>
<meta property="og:type" content="website" />
<!-- Add more Open Graph meta tags as needed -->
<!-- Twitter specific meta tags -->
<meta name="twitter:card" content="summary_large_image" />
<meta name="twitter:title" content={`Stock Screener · Stocknear`} />
<meta
name="twitter:description"
content={`Build your Stock Screener to find profitable stocks.`}
/>
<!-- Add more Twitter meta tags as needed -->
</svelte:head>
<svelte:window on:scroll={handleScroll} />
<section
class="w-full max-w-3xl sm:max-w-screen-xl overflow-hidden min-h-screen pt-5 pb-40 px-5"
>
<div class="text-sm sm:text-[1rem] breadcrumbs">
<ul>
<li><a href="/" class="text-gray-300">Home</a></li>
<li><span class="text-gray-300">Stock Screener</span></li>
</ul>
</div>
<!--Start Build Strategy-->
<div class="mt-5 sm:rounded-md">
<div class="flex flex-col md:flex-row items-start md:items-center mb-5">
<div class="w-full flex flex-row items-center sm:mt-4">
<h1 class="text-white text-3xl font-semibold">Stock Screener</h1>
<span
class="inline-block text-xs sm:text-sm font-semibold text-white ml-2 mt-3"
>
{filteredData?.length} Matches Found
</span>
</div>
<div class="flex flex-row items-center w-full mt-5">
<div class="flex w-full sm:w-[50%] md:block md:w-auto sm:ml-auto">
<div
class="hidden text-sm sm:text-[1rem] font-semibold text-white md:block sm:mb-1"
>
Popular Screens
</div>
<div class="relative inline-block text-left grow">
<DropdownMenu.Root>
<DropdownMenu.Trigger asChild let:builder>
<Button
builders={[builder]}
class="w-full border-gray-600 border bg-[#09090B] sm:hover:bg-primary ease-out flex flex-row justify-between items-center px-3 py-2 text-white rounded-md truncate"
>
<span class="truncate text-white"
>{selectedPopularStrategy?.length !== 0
? selectedPopularStrategy
: "Select popular"}</span
>
<svg
class="-mr-1 ml-1 h-5 w-5 xs:ml-2 inline-block"
viewBox="0 0 20 20"
fill="currentColor"
style="max-width:40px"
aria-hidden="true"
>
<path
fill-rule="evenodd"
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"
clip-rule="evenodd"
></path>
</svg>
</Button>
</DropdownMenu.Trigger>
<DropdownMenu.Content
class="w-56 h-fit max-h-72 overflow-y-auto scroller"
>
<DropdownMenu.Label class="text-gray-400">
Popular Strategies
</DropdownMenu.Label>
<DropdownMenu.Separator />
<DropdownMenu.Group>
<DropdownMenu.Item
on:click={() => popularStrategy("dividendGrowth")}
class="cursor-pointer hover:bg-primary"
>
Dividend Growth
</DropdownMenu.Item>
<DropdownMenu.Item
on:click={() => popularStrategy("topGainers1Y")}
class="cursor-pointer hover:bg-primary"
>
Top Gainers 1Y
</DropdownMenu.Item>
<DropdownMenu.Item
on:click={() => popularStrategy("topShortedStocks")}
class="cursor-pointer hover:bg-primary"
>
Top Shorted Stocks
</DropdownMenu.Item>
<DropdownMenu.Item
on:click={() => popularStrategy("momentumTAStocks")}
class="cursor-pointer hover:bg-primary"
>
Momentum TA Stocks
</DropdownMenu.Item>
<DropdownMenu.Item
on:click={() => popularStrategy("underValuedStocks")}
class="cursor-pointer hover:bg-primary"
>
Undervalued Stocks
</DropdownMenu.Item>
<DropdownMenu.Item
on:click={() => popularStrategy("strongCashFlow")}
class="cursor-pointer hover:bg-primary"
>
Strong Cash Flow
</DropdownMenu.Item>
</DropdownMenu.Group>
</DropdownMenu.Content>
</DropdownMenu.Root>
</div>
</div>
<div class="flex w-full sm:w-[50%] sm:ml-3 md:block md:w-auto ml-3">
<div
class="hidden text-sm sm:text-[1rem] font-semibold text-white md:block sm:mb-1"
>
Saved Screens
</div>
<div class="relative inline-block text-left grow">
<DropdownMenu.Root>
<DropdownMenu.Trigger asChild let:builder>
<Button
builders={[builder]}
class="min-w-[110px] w-full border-gray-600 border bg-[#09090B] sm:hover:bg-primary ease-out flex flex-row justify-between items-center px-3 py-2 text-white rounded-md truncate"
>
<span class="truncate text-white"
>{selectedStrategy?.length !== 0
? strategyList?.find(
(item) => item.id === selectedStrategy,
)?.title
: "Select screen"}</span
>
<svg
class="-mr-1 ml-1 h-5 w-5 xs:ml-2 inline-block"
viewBox="0 0 20 20"
fill="currentColor"
style="max-width:40px"
aria-hidden="true"
>
<path
fill-rule="evenodd"
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"
clip-rule="evenodd"
></path>
</svg>
</Button>
</DropdownMenu.Trigger>
<DropdownMenu.Content
class="w-56 h-fit max-h-72 overflow-y-auto scroller"
>
<DropdownMenu.Label class="text-gray-400">
<DropdownMenu.Trigger asChild let:builder>
<Button
on:click={handleCreateStrategy}
builders={[builder]}
class="p-0 -mb-2 -mt-2 text-sm inline-flex cursor-pointer items-center justify-center space-x-1 whitespace-nowrap text-base text-white bg-[#0909B] focus:outline-none sm:text-smaller"
>
<svg
class="h-4 w-4"
viewBox="0 0 20 20"
fill="currentColor"
style="max-width:40px"
aria-hidden="true"
>
<path
fill-rule="evenodd"
d="M10 3a1 1 0 011 1v5h5a1 1 0 110 2h-5v5a1 1 0 11-2 0v-5H4a1 1 0 110-2h5V4a1 1 0 011-1z"
clip-rule="evenodd"
></path>
</svg>
<div class="text-sm text-start">New Screen</div>
</Button>
</DropdownMenu.Trigger>
</DropdownMenu.Label>
<DropdownMenu.Separator />
<DropdownMenu.Group>
{#each strategyList as item}
<DropdownMenu.Item
on:click={() => switchStrategy(item)}
class="{item?.id === selectedStrategy
? 'bg-primary'
: ''} cursor-pointer sm:hover:bg-primary"
>
{item?.title} ({item?.rules?.length})
</DropdownMenu.Item>
{/each}
</DropdownMenu.Group>
</DropdownMenu.Content>
</DropdownMenu.Root>
</div>
</div>
</div>
</div>
<div class="rounded-md border border-gray-700 bg-primary p-2">
<div class="items-end border-b border-gray-600">
<div
class="mr-1 flex items-center justify-between lg:mr-2 pb-1.5 border-b border-gray-600 mt-1.5"
>
<button
on:click={() => (showFilters = !showFilters)}
class="flex cursor-pointer items-center text-lg sm:text-xl font-semibold text-gray-200"
title="Hide Filter Area"
>
<svg
class="-mb-0.5 h-6 w-6 {showFilters ? '' : '-rotate-90'} "
viewBox="0 0 20 20"
fill="currentColor"
style="max-width:40px"
>
<path
fill-rule="evenodd"
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"
clip-rule="evenodd"
></path>
</svg>
{ruleOfList?.length} Filters
</button>
</div>
</div>
{#if showFilters}
<div class="mt-3 flex flex-col gap-y-2.5 sm:flex-row lg:gap-y-2">
<label
for="ruleModal"
class="inline-flex cursor-pointer items-center justify-center space-x-1 whitespace-nowrap rounded-md border border-transparent bg-blue-brand_light py-2 pl-3 pr-4 text-base font-semibold text-white shadow-sm bg-[#000] sm:hover:bg-[#09090B]/60 ease-out focus:outline-none focus:ring-2 focus:ring-blue-500 sm:text-smaller"
>
<svg
class="h-5 w-5"
viewBox="0 0 20 20"
fill="currentColor"
style="max-width:40px"
aria-hidden="true"
>
<path
fill-rule="evenodd"
d="M10 3a1 1 0 011 1v5h5a1 1 0 110 2h-5v5a1 1 0 11-2 0v-5H4a1 1 0 110-2h5V4a1 1 0 011-1z"
clip-rule="evenodd"
></path>
</svg>
<div>Add Filters</div>
</label>
{#if data?.user && selectedStrategy?.length !== 0}
<label
for={!data?.user ? "userLogin" : ""}
on:click={() => handleSave(true)}
class="sm:ml-3 cursor-pointer inline-flex items-center justify-center space-x-1 whitespace-nowrap rounded-md border border-transparent bg-blue-brand_light py-2 pl-3 pr-4 text-base font-semibold text-white shadow-sm bg-[#000] sm:hover:bg-[#09090B]/60 ease-out focus:outline-none focus:ring-2 focus:ring-blue-500 sm:text-smaller"
>
<svg
class="h-5 w-5"
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 32 32"
><path
fill="#fff"
d="M5 5v22h22V9.594l-.281-.313l-4-4L22.406 5zm2 2h3v6h12V7.437l3 3V25h-2v-9H9v9H7zm5 0h4v2h2V7h2v4h-8zm-1 11h10v7H11z"
/></svg
>
<div>Save</div>
</label>
<label
for="deleteStrategy"
class="sm:ml-3 cursor-pointer inline-flex items-center justify-center space-x-1 whitespace-nowrap rounded-md border border-transparent bg-blue-brand_light py-2 pl-3 pr-4 text-base font-semibold text-white shadow-sm bg-[#000] sm:hover:bg-[#09090B]/60 ease-out sm:hover:text-red-500 focus:outline-none focus:ring-2 focus:ring-blue-500 sm:text-smaller"
>
<svg
class="h-5 w-5"
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 1024 1024"
><path
fill="currentColor"
d="M360 184h-8c4.4 0 8-3.6 8-8zh304v-8c0 4.4 3.6 8 8 8h-8v72h72v-80c0-35.3-28.7-64-64-64H352c-35.3 0-64 28.7-64 64v80h72zm504 72H160c-17.7 0-32 14.3-32 32v32c0 4.4 3.6 8 8 8h60.4l24.7 523c1.6 34.1 29.8 61 63.9 61h454c34.2 0 62.3-26.8 63.9-61l24.7-523H888c4.4 0 8-3.6 8-8v-32c0-17.7-14.3-32-32-32M731.3 840H292.7l-24.2-512h487z"
/></svg
>
<div>Delete</div>
</label>
{/if}
{#if ruleOfList?.length !== 0}
<label
on:click={handleResetAll}
class="sm:ml-3 cursor-pointer inline-flex items-center justify-center space-x-1 whitespace-nowrap rounded-md border border-transparent bg-blue-brand_light py-2 pl-3 pr-4 text-base font-semibold text-white shadow-sm bg-[#000] sm:hover:text-red-500 ease-out focus:outline-none focus:ring-2 focus:ring-blue-500 sm:text-smaller"
>
<svg
class="h-4 w-4"
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 21 21"
><g
fill="none"
fill-rule="evenodd"
stroke="currentColor"
stroke-linecap="round"
stroke-linejoin="round"
><path d="M3.578 6.487A8 8 0 1 1 2.5 10.5" /><path
d="M7.5 6.5h-4v-4"
/></g
></svg
>
<div>Reset All</div>
</label>
{/if}
<!--
<div class="relative sm:ml-2">
<div class="absolute inset-y-0 left-0 flex items-center pl-2.5">
<svg class="h-4 w-4 text-gray-400 xs:h-5 xs:w-5" fill="none" stroke-linecap="round" stroke-linejoin="round" stroke-width="3" stroke="currentColor" viewBox="0 0 24 24" style="max-width: 40px" aria-hidden="true">
<path d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"></path>
</svg>
</div>
<input type="text" placeholder="Search {allRows?.length} filters..." class="controls-input rounded-md w-full py-2 pl-10 placeholder:text-gray-300 bg-secondary sm:w-72">
<div class="absolute inset-y-0 right-0 flex items-center pr-2"></div>
</div>
-->
</div>
<div
class="sm:grid sm:gap-x-2.5 md:grid-cols-2 lg:grid-cols-3 w-full mt-3 border-t border-b border-gray-600"
>
{#each displayRules as row (row?.rule)}
<!--Start Added Rules-->
<div
class="flex items-center justify-between space-x-2 px-1 py-1.5 text-smaller leading-tight text-default"
>
<div class="hide-scroll text-white">
{row?.label?.length > 20
? row?.label?.slice(0, 20)?.replace("[%]", "") + "..."
: row?.label?.replace("[%]", "")}
<span class="relative" role="tooltip"
><label
for="mobileTooltip"
on:click={() =>
getInfoText(row?.rule, row?.label?.replace("[%]", ""))}
class="relative"
role="tooltip"
>
<span
class="absolute -right-[15px] -top-[3px] cursor-pointer p-1 text-gray-300 sm:hover:text-white"
>
<svg
class="h-[10.5px] w-[10.5px]"
viewBox="0 0 4 16"
fill="currentColor"
style="max-width:20px"
><path
d="M0 6h4v10h-4v-10zm2-6c1.1 0 2 .9 2 2s-.9 2-2 2-2-.9-2-2 .9-2 2-2z"
></path></svg
>
</span>
</label></span
>
</div>
<div class="flex items-center">
<button
on:click={() => handleDeleteRule(row?.rule)}
class="mr-1.5 cursor-pointer text-gray-300 sm:hover:text-red-500 focus:outline-none"
title="Remove filter"
>
<svg
class="w-6 h-6"
fill="none"
viewBox="0 0 24 24"
stroke="CurrentColor"
style="max-width:40px"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M10 14l2-2m0 0l2-2m-2 2l-2-2m2 2l2 2m7-2a9 9 0 11-18 0 9 9 0 0118 0z"
></path>
</svg>
</button>
<div class="relative inline-block text-left">
<div on:click={() => (ruleName = row?.rule)}>
<DropdownMenu.Root>
<DropdownMenu.Trigger asChild let:builder>
<Button
builders={[builder]}
class="bg-[#000] h-[40px] flex flex-row justify-between items-center w-[150px] xs:w-[140px] sm:w-[150px] px-3 text-white rounded-md truncate"
>
<span class="truncate ml-2 text-sm sm:text-[1rem]">
{#if valueMappings[row?.rule] === "any"}
Any
{:else if ruleCondition[row?.rule] === "between"}
{Array.isArray(valueMappings[row?.rule])
? `${valueMappings[row?.rule][0]}-${valueMappings[row?.rule][1] ?? "Any"}`
: "Any"}
{:else}
{ruleCondition[row?.rule]
?.replace("under", "Under")
?.replace("over", "Over")
?.replace("exactly", "Exactly") ?? ""}
{valueMappings[row?.rule]}
{/if}
</span>
<svg
class=" ml-1 h-6 w-6 xs:ml-2 inline-block"
viewBox="0 0 20 20"
fill="currentColor"
style="max-width:40px"
aria-hidden="true"
>
<path
fill-rule="evenodd"
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"
clip-rule="evenodd"
></path>
</svg>
</Button>
</DropdownMenu.Trigger>
<DropdownMenu.Content
class="w-64 min-h-auto max-h-72 overflow-y-auto scroller"
>
{#if !["sma20", "sma50", "sma100", "sma200", "ema20", "ema50", "ema100", "ema200", "grahamNumber", "analystRating", "topAnalystRating", "halalStocks", "score", "sector", "industry", "country"]?.includes(row?.rule)}
<DropdownMenu.Label
class="absolute mt-2 h-11 border-gray-800 border-b -top-1 z-20 fixed sticky bg-[#09090B]"
>
<div
class="flex items-center justify-start gap-x-1"
>
<!--Start Dropdown for Condition-->
<div
class="-ml-2 relative inline-block text-left"
>
<DropdownMenu.Root>
<DropdownMenu.Trigger asChild let:builder
><Button
builders={[builder]}
class="w-fit -mt-1 -ml-2 bg-[#09090B] flex flex-row justify-between items-center text-white"
>
<span
class="truncate ml-2 text-sm sm:text-[1rem]"
>
{ruleCondition[ruleName]
?.replace("under", "Under")
?.replace("over", "Over")
?.replace("between", "Between")
?.replace("exactly", "Exactly")}
</span>
<svg
class="mt-1 -mr-1 ml-1 h-5 w-5 xs:ml-2 !ml-0 sm:ml-0 inline-block"
viewBox="0 0 20 20"
fill="currentColor"
style="max-width:40px"
aria-hidden="true"
><path
fill-rule="evenodd"
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"
clip-rule="evenodd"
></path></svg
>
</Button>
</DropdownMenu.Trigger>
<DropdownMenu.Content>
<DropdownMenu.Group>
{#each ["Over", "Under", "Between", "Exactly"] as item}
<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>
{#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]}
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", "exactly"]?.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>
</DropdownMenu.Label>
{:else}
<div
class="relative sticky z-40 focus:outline-none -top-1"
tabindex="0"
role="menu"
style=""
>
<input
bind:value={searchQuery}
on:input={handleInput}
autocomplete="off"
class="{![
'analystRating',
"topAnalystRating",
'halalStocks',
'score',
'sector',
'industry',
'country',
]?.includes(row?.rule)
? 'hidden'
: ''} absolute fixed sticky w-full border-0 bg-[#09090B] border-b border-gray-200
focus:border-gray-200 focus:ring-0 text-white placeholder:text-gray-300"
type="search"
placeholder="Search..."
/>
</div>
{/if}
<DropdownMenu.Group class="min-h-10 mt-2">
{#if !["sma20", "sma50", "sma100", "sma200", "ema20", "ema50", "ema100", "ema200", "grahamNumber", "analystRating", "topAnalystRating","halalStocks", "score", "sector", "industry", "country"]?.includes(row?.rule)}
{#each row?.step as newValue, index}
{#if ruleCondition[row?.rule] === "between"}
{#if newValue && row?.step[index + 1]}
<DropdownMenu.Item
class="sm:hover:bg-primary"
>
<button
on:click={() => {
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">
<button
on:click={() => {
handleChangeValue(newValue);
}}
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("under", "Under")
?.replace("over", "Over")
?.replace("exactly", "Exactly")}
{newValue}
</button>
</DropdownMenu.Item>
{/if}
{/each}
{:else if ["sma20", "sma50", "sma100", "sma200", "ema20", "ema50", "ema100", "ema200", "grahamNumber"]?.includes(row?.rule)}
{#each row?.step as item}
<DropdownMenu.Item class="sm:hover:bg-primary">
<div
class="flex items-center"
on:click|capture={(event) =>
event.preventDefault()}
>
<label
on:click={() => {
handleChangeValue(item);
}}
class="cursor-pointer text-white"
for={item}
>
<input
type="checkbox"
class="rounded"
checked={isChecked(item, row?.rule)}
/>
<span class="ml-2">{item}</span>
</label>
</div>
</DropdownMenu.Item>
{/each}
{:else}
{#each testList.length > 0 && searchQuery?.length > 0 ? testList : searchQuery?.length > 0 && testList?.length === 0 ? [] : row?.rule === "country" ? listOfRelevantCountries : row?.rule === "sector" ? sectorList : row?.rule === "industry" ? industryList : ['analystRating','topAnalystRating','score']?.includes(ruleName) ? ["Strong Buy", "Buy", "Hold", "Sell", "Strong Sell"] : ["Compliant", "Non-Compliant"] as item}
<DropdownMenu.Item class="sm:hover:bg-primary">
<div
class="flex items-center"
on:click|capture={(event) =>
event.preventDefault()}
>
<label
on:click={() => {
handleChangeValue(item);
}}
class="cursor-pointer text-white"
for={item}
>
<input
type="checkbox"
class="rounded"
checked={isChecked(item, row?.rule)}
/>
<span class="ml-2">{item}</span>
</label>
</div>
</DropdownMenu.Item>
{/each}
{/if}
</DropdownMenu.Group>
</DropdownMenu.Content>
</DropdownMenu.Root>
</div>
</div>
</div>
</div>
<!--End Added Rules-->
{/each}
</div>
{/if}
</div>
<!--End Adding Rules-->
</div>
<!--End Build Strategy-->
<div
class="mt-10 sm:mt-6 grid-cols-2 items-center sm:grid lg:flex lg:space-x-1 lg:overflow-visible lg:px-1 lg:py-2"
>
<h2
class="mb-1 whitespace-nowrap text-xl font-semibold text-white bp:text-[1.3rem] sm:mb-0"
>
{filteredData?.length} Stocks
</h2>
<div
class="col-span-2 flex flex-row items-center border-t border-gray-600 lg:order-2 lg:grow lg:border-0 lg:pl-1 xl:pl-3"
>
<nav class="w-full flex flex-row items-center py-2.5 sm:py-3 lg:py-1">
<ul
class="flex flex-row overflow-x-auto items-center space-x-2 whitespace-nowrap text-base"
>
<li>
<button
on:click={() => (displayTableTab = "general")}
class="text-[1rem] sm:text-lg block text-white rounded-md px-2 py-1 focus:outline-none sm:hover:bg-primary {displayTableTab ===
'general'
? 'font-semibold bg-primary'
: ''}"
>
General
</button>
</li>
<li>
<button
on:click={() => (displayTableTab = "filters")}
class="text-[1rem] sm:text-lg flex flex-row items-center relative block rounded-md px-2 py-1 sm:hover:bg-primary {displayTableTab ===
'filters'
? 'font-semibold bg-primary'
: ''} focus:outline-none"
>
<span class="text-white">Filters</span>
<span
class="ml-2 rounded-full avatar w-5 h-5 text-xs font-semibold text-white text-center flex-shrink-0
flex items-center justify-center {ruleOfList?.length !==
0
? 'bg-red-500'
: 'bg-gray-600'}"
>
{ruleOfList?.length}
</span>
</button>
</li>
<li>
<button
on:click={() => changeTab("performance")}
class="text-[1rem] sm:text-lg block text-white rounded-md px-2 py-1 focus:outline-none sm:hover:bg-primary {displayTableTab ===
'performance'
? 'font-semibold bg-primary'
: ''}"
>
Performance
</button>
</li>
<li>
<button
on:click={() => changeTab("analysts")}
class="text-[1rem] sm:text-lg block text-white rounded-md px-2 py-1 focus:outline-none sm:hover:bg-primary {displayTableTab ===
'analysts'
? 'font-semibold bg-primary'
: ''}"
>
Analysts
</button>
</li>
</ul>
<div class="w-fit ml-auto hidden sm:inline-block">
<DownloadData
{data}
rawData={filteredData}
title={"stock_screener_data"}
/>
</div>
</nav>
</div>
</div>
<!--Start Matching Preview-->
{#if isLoaded}
{#if filteredData?.length !== 0}
{#if displayTableTab === "general"}
<div class="w-full rounded-md overflow-x-scroll">
<table
class="table table-sm table-compact w-full bg-[#09090B] border-bg-[#09090B]"
>
<thead>
<TableHeader {columns} {sortOrders} {sortData} />
</thead>
<tbody>
{#each displayResults as item}
<tr
class="sm:hover:bg-[#245073] sm:hover:bg-opacity-[0.2] bg-[#09090B] border-b-[#09090B] odd:bg-secondary"
>
<td class="border-b-[#09090B] whitespace-nowrap">
<a
href={"/stocks/" + item?.symbol}
class="sm:hover:text-white text-blue-400 text-sm sm:text-[1rem]"
>{item?.symbol}</a
>
</td>
<td
class="whitespace-nowrap text-[1rem] text-white border-b-[#09090B]"
>
{item?.name?.length > charNumber
? item?.name?.slice(0, charNumber) + "..."
: item?.name}
</td>
<td
class="text-white text-sm sm:text-[1rem] text-end border-b-[#09090B]"
>
{item?.marketCap < 100
? "< 100"
: abbreviateNumber(item?.marketCap)}
</td>
<td
class="text-white text-sm sm:text-[1rem] text-end border-b-[#09090B]"
>
{item?.price < 0.01 ? "< 0.01" : item?.price?.toFixed(2)}
</td>
<td
class="text-white text-end text-sm sm:text-[1rem] font-medium border-b-[#09090B]"
>
{#if item?.changesPercentage >= 0}
<span class="text-[#00FC50]"
>+{item?.changesPercentage >= 1000
? abbreviateNumber(item?.changesPercentage)
: item?.changesPercentage?.toFixed(2)}%</span
>
{:else}
<span class="text-[#FF2F1F]"
>{item?.changesPercentage <= -1000
? abbreviateNumber(item?.changesPercentage)
: item?.changesPercentage?.toFixed(2)}%
</span>
{/if}
</td>
<td
class="text-white text-sm sm:text-[1rem] text-end border-b-[#09090B]"
>
{item?.volume === 0 ? "-" : abbreviateNumber(item?.volume)}
</td>
<td
class="text-white text-sm sm:text-[1rem] text-end border-b-[#09090B]"
>
{item?.pe}
</td>
</tr>
{/each}
</tbody>
</table>
</div>
{:else if displayTableTab === "filters"}
<div class="w-full rounded-md overflow-x-scroll">
<table
class="table table-sm table-compact w-full bg-[#09090B] border-bg-[#09090B]"
>
<thead>
<TableHeader {columns} {sortOrders} {sortData} />
</thead>
<tbody>
{#each displayResults as item (item?.symbol)}
<tr
class="sm:hover:bg-[#245073] sm:hover:bg-opacity-[0.2] bg-[#09090B] border-b-[#09090B] odd:bg-secondary"
>
<td class="border-b-[#09090B] whitespace-nowrap">
<a
href={"/stocks/" + item?.symbol}
class="sm:hover:text-white text-blue-400 text-sm sm:text-[1rem]"
>{item?.symbol}</a
>
</td>
<td
class=" whitespace-nowrap text-[1rem] text-white border-b-[#09090B]"
>
{item?.name?.length > charNumber
? item?.name?.slice(0, charNumber) + "..."
: item?.name}
</td>
<td
class="whitespace-nowrap text-sm sm:text-[1rem] text-end text-white border-b-[#09090B]"
>
{abbreviateNumber(item?.marketCap)}
</td>
{#each displayRules as row (row?.rule)}
{#if row?.rule !== "marketCap"}
<td
class="whitespace-nowrap text-sm sm:text-[1rem] text-end text-white border-b-[#09090B]"
>
{#if ["ema20", "ema50", "ema100", "ema200", "analystRating", "topAnalystRating", "halalStocks", "score", "sector", "industry", "country"]?.includes(row?.rule)}
{item[row?.rule]}
{:else}
{abbreviateNumber(item[row?.rule])}
{/if}
</td>
{/if}
{/each}
</tr>
{/each}
</tbody>
</table>
</div>
{:else if displayTableTab === "performance"}
<div class="w-full rounded-md overflow-x-scroll">
<table
class="table table-sm table-compact w-full bg-[#09090B] border-bg-[#09090B]"
>
<thead>
<TableHeader {columns} {sortOrders} {sortData} />
</thead>
<tbody>
{#each displayResults as item (item?.symbol)}
<tr
class="sm:hover:bg-[#245073] sm:hover:bg-opacity-[0.2] bg-[#09090B] border-b-[#09090B] odd:bg-secondary"
>
<td class="border-b-[#09090B] whitespace-nowrap">
<a
href={"/stocks/" + item?.symbol}
class="sm:hover:text-white text-blue-400 text-sm sm:text-[1rem]"
>{item?.symbol}</a
>
</td>
<td
class="whitespace-nowrap text-[1rem] text-white border-b-[#09090B]"
>
{item?.name?.length > charNumber
? item?.name?.slice(0, charNumber) + "..."
: item?.name}
</td>
{#each tabRuleList as row (row?.rule)}
<td
class="whitespace-nowrap text-sm sm:text-[1rem] text-end text-white border-b-[#09090B]"
>
{#if row?.rule === "marketCap"}
{abbreviateNumber(item[row?.rule])}
{:else if item[row?.rule] > 0}
<span class="text-[#00FC50]"
>+{abbreviateNumber(
item[row?.rule]?.toFixed(2),
)}%</span
>
{:else if item[row?.rule] < 0}
<span class="text-[#FF2F1F]"
>{abbreviateNumber(
item[row?.rule]?.toFixed(2),
)}%</span
>
{:else}
<span class="text-[#fff]">-</span>
{/if}
</td>
{/each}
</tr>
{/each}
</tbody>
</table>
</div>
{:else if displayTableTab === "analysts"}
<div class="w-full rounded-md overflow-x-scroll">
<table
class="table table-sm table-compact w-full bg-[#09090B] border-bg-[#09090B]"
>
<thead>
<TableHeader {columns} {sortOrders} {sortData} />
</thead>
<tbody>
{#each displayResults as item (item?.symbol)}
<tr
class="sm:hover:bg-[#245073] sm:hover:bg-opacity-[0.2] bg-[#09090B] border-b-[#09090B] odd:bg-secondary"
>
<td class="border-b-[#09090B] whitespace-nowrap">
<a
href={"/stocks/" + item?.symbol}
class="sm:hover:text-white text-blue-400 text-sm sm:text-[1rem]"
>{item?.symbol}</a
>
</td>
<td
class="whitespace-nowrap text-[1rem] text-white border-b-[#09090B]"
>
{item?.name?.length > charNumber
? item?.name?.slice(0, charNumber) + "..."
: item?.name}
</td>
{#each tabRuleList as row (row?.rule)}
<td
class="whitespace-nowrap text-sm sm:text-[1rem] text-end text-white border-b-[#09090B]"
>
{#if row?.rule === "marketCap"}
{abbreviateNumber(item[row?.rule])}
{:else if ['analystCounter','priceTarget']?.includes(row?.rule)}
<span class="text-white"
>{abbreviateNumber(
item[row?.rule],
)}</span
>
{:else if row?.rule === 'upside'}
{#if item[row?.rule] > 0}
<span class="text-[#00FC50]"
>+{item[row?.rule]?.toFixed(2)}%</span
>
{:else if item[row?.rule] < 0}
<span class="text-[#FF2F1F]"
>{item[row?.rule]?.toFixed(2)}%</span
>
{:else}
<span class="text-[#fff]"
>n/a</span
>
{/if}
{:else if ['analystRating','topAnalystRating']?.includes(row?.rule)}
{#if ["Strong Buy", "Buy"].includes(item[row?.rule])}
<span class="text-[#00FC50]">{item[row?.rule]}</span>
{:else if ["Strong Sell", "Sell"].includes(item[row?.rule])}
<span class="text-[#FF2F1F]">{item[row?.rule]}</span>
{:else if item[row?.rule] === "Hold"}
<span class="text-[#FFA838]">{item[row?.rule]}</span>
{:else}
-
{/if}
{/if}
</td>
{/each}
</tr>
{/each}
</tbody>
</table>
</div>
{/if}
{:else}
<div
class="text-white p-3 sm:p-5 mb-10 rounded-md sm:flex sm:flex-row sm:items-center border border-gray-600 text-sm sm:text-[1rem]"
>
<svg
class="w-6 h-6 flex-shrink-0 inline-block sm:mr-2"
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 256 256"
><path
fill="#fff"
d="M128 24a104 104 0 1 0 104 104A104.11 104.11 0 0 0 128 24m-4 48a12 12 0 1 1-12 12a12 12 0 0 1 12-12m12 112a16 16 0 0 1-16-16v-40a8 8 0 0 1 0-16a16 16 0 0 1 16 16v40a8 8 0 0 1 0 16"
/></svg
>
Looks like your taste is one-of-a-kind! No matches found... yet!
</div>
{/if}
{:else}
<div class="flex justify-center items-center h-80">
<div class="relative">
<label
class="bg-primary rounded-xl h-14 w-14 flex justify-center items-center absolute top-1/2 left-1/2 transform -translate-x-1/2 -translate-y-1/2"
>
<span class="loading loading-spinner loading-md text-gray-400"></span>
</label>
</div>
</div>
{/if}
<!--End Matching Preview-->
</section>
<!--
<div class="tabs w-screen mb-5 ">
<label on:click={() => handleRuleTab('all')} class="tab mr-2 text-white font-medium transition duration-150 ease-out hover:ease-out rounded-md hover:bg-[#333333] {displayTab === 'all' ? 'bg-[#333333]' : ''}">
All
</label>
<label on:click={() => handleRuleTab('ta')} class="tab mr-2 text-white font-medium transition duration-150 ease-out hover:ease-out rounded-md hover:bg-[#333333] {displayTab === 'ta' ? 'bg-[#333333]' : ''}">
Technical Indicators
</label>
<label on:click={() => handleRuleTab('fund')} class="tab mr-2 text-white font-medium transition duration-150 ease-out hover:ease-out rounded-md hover:bg-[#333333] {displayTab === 'fund' ? 'bg-[#333333]' : ''}">
Fundamental Data
</label>
</div>
-->
<!--Start Choose Rule Modal-->
<input type="checkbox" id="ruleModal" class="modal-toggle" />
<dialog id="ruleModal" class="modal p-2 lg:p-0">
<label
id="ruleModal"
for="ruleModal"
on:click={() => (searchTerm = "")}
class="cursor-pointer modal-backdrop bg-[#000] bg-opacity-[0.8]"
></label>
<div
class="modal-box relative bg-primary z-20 mx-2 min-h-[30vh] h-[800px] rounded bg-default opacity-100 border border-gray-600 bp:mx-3 sm:mx-4 w-full max-w-6xl overflow-y-auto"
>
<div class="relative flex flex-col w-full">
<!-- Sticky Header -->
<div
class="fixed w-full h-fit sticky -top-6 z-40 bg-primary shadow-sm opacity-100 pb-6 pt-5 border-gray-600 border-b"
>
<div class="flex flex-row items-center justify-between mb-2">
<h2 class="text-white text-[1rem] sm:text-xl font-semibold">
Select screener filters ({allRows?.length} total)
</h2>
<label
for="ruleModal"
class="inline-block cursor-pointer absolute right-0 top-3 text-[1.3rem] sm:text-[1.8rem] text-white"
>
<svg
class="w-6 h-6 sm:w-8 sm:h-8"
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
><path
fill="white"
d="m6.4 18.308l-.708-.708l5.6-5.6l-5.6-5.6l.708-.708l5.6 5.6l5.6-5.6l.708.708l-5.6 5.6l5.6 5.6l-.708.708l-5.6-5.6z"
/></svg
>
</label>
</div>
<!-- Start Search bar -->
<form
class="w-full h-8"
on:keydown={(e) => (e?.key === "Enter" ? e.preventDefault() : "")}
>
<label for="search" class="text-sm font-medium text-gray-200 sr-only"
>Search</label
>
<div class="relative w-full max-w-sm">
<div
class="absolute inset-y-0 start-0 flex items-center ps-3 pointer-events-none"
>
<svg
class="w-4 h-4 text-gray-200"
aria-hidden="true"
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 20 20"
>
<path
stroke="currentColor"
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="m19 19-4-4m0-7A7 7 0 1 1 1 8a7 7 0 0 1 14 0Z"
/>
</svg>
</div>
<div
class="absolute inset-y-0 right-0 flex items-center pr-2 {searchTerm?.length >
0
? ''
: 'hidden'}"
>
<button
on:click={() => (searchTerm = "")}
class="cursor-pointer text-gray-200 sm:hover:text-white"
tabindex="0"
><svg
class="w-5 h-5"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
style="max-width:40px"
><path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M6 18L18 6M6 6l12 12"
></path></svg
></button
>
</div>
<input
autocomplete="off"
type="search"
id="search"
class="placeholder-gray-300 block w-full p-2 ps-10 text-sm text-gray-200 border border-gray-600 rounded-md bg-secondary border border-blue-500"
placeholder="Search"
bind:value={searchTerm}
/>
</div>
</form>
<!-- End Search bar -->
</div>
<!-- Content -->
<div class="text-white">
{#each searchTerm?.length !== 0 ? Object?.entries(filteredGroupedRules) : Object?.entries(groupedRules) as [category, rules]}
<h4 class="mb-1 font-semibold text-lg mt-5">{category}</h4>
<div class="flex flex-wrap">
{#each rules as row}
<div
class="flex w-full items-center space-x-1.5 py-1.5 md:w-1/2 lg:w-1/3 lg:py-1"
>
{#if ['topAnalystRating','topAnalystCounter','topAnalystPriceTarget','topAnalystUpside','score']?.includes(row?.rule) && data?.user?.tier !== "Pro"}
<label id={row?.rule} on:click={() => changeRule(row?.rule)}>
<svg
class="w-4 h-4 mb-1 inline-block text-[#A3A3A3] sm:hover:text-white cursor-pointer"
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
><path
fill="currentColor"
d="M17 9V7c0-2.8-2.2-5-5-5S7 4.2 7 7v2c-1.7 0-3 1.3-3 3v7c0 1.7 1.3 3 3 3h10c1.7 0 3-1.3 3-3v-7c0-1.7-1.3-3-3-3M9 7c0-1.7 1.3-3 3-3s3 1.3 3 3v2H9z"
/></svg
>
</label>
{:else}
<input
on:click={() => changeRule(row?.rule)}
id={row?.rule}
type="checkbox"
checked={ruleOfList?.find(
(rule) => rule?.name === row?.rule,
)}
class="h-[18px] w-[18px] rounded-sm ring-offset-0 lg:h-4 lg:w-4"
/>
{/if}
<div class="-mt-0.5">
<label for={row?.rule} class="cursor-pointer text-[1rem]"
>{row?.label}</label
>
</div>
</div>
{/each}
</div>
{/each}
{#if searchTerm?.length > 0 && Object?.entries(filteredGroupedRules)?.length === 0}
<div class="text-white mt-5 font-semibold text-[1rem] sm:text-lg">
Nothing found
</div>
{/if}
</div>
</div>
</div>
</dialog>
<!--End Choose Rule Modal-->
<!--Start Add Strategy Modal-->
<input type="checkbox" id="addStrategy" class="modal-toggle" />
<dialog id="addStrategy" class="modal modal-bottom sm:modal-middle">
<label
for="addStrategy"
class="cursor-pointer modal-backdrop bg-[#000] bg-opacity-[0.5]"
></label>
<div class="modal-box w-full bg-primary border border-gray-600">
<h1 class="text-white text-2xl font-bold">New Strategy</h1>
<form
on:submit={createStrategy}
method="POST"
class="space-y-2 pt-5 pb-10 sm:pb-5"
>
<Input
id="title"
type="text"
errors=""
label="Strategy Name"
required={true}
/>
<button
type="submit"
class="mt-10 btn bg-[#fff] sm:hover:bg-gray-300 duration-100 btn-md w-full rounded-md m-auto text-black font-semibold text-md"
>
Create Strategy
</button>
</form>
</div>
</dialog>
<!--End Add Strategy Modal-->
<!--Start Delete Strategy Modal-->
<input type="checkbox" id="deleteStrategy" class="modal-toggle" />
<dialog id="deleteStrategy" class="modal modal-bottom sm:modal-middle">
<label
for="deleteStrategy"
class="cursor-pointer modal-backdrop bg-[#000] bg-opacity-[0.5]"
></label>
<div
class="modal-box w-full bg-primary border border-gray-600 overflow-hidden"
>
<h3
class="font-bold text-md sm:text-lg flex justify-center items-center mt-10 text-white"
>
Are you sure you want to delete the strategy?
</h3>
<div
class="modal-action w-full m-auto p-5 flex flex-col sm:flex-row items-center"
>
<label
for="deleteStrategy"
on:click={handleDeleteStrategy}
class="mt-5 btn bg-[#fff] sm:hover:bg-gray-300 btn-md w-full rounded-md m-auto text-black font-semibold text-md"
>
Proceed
</label>
</div>
</div>
</dialog>
<!--End Delete Strategy Modal-->
<input type="checkbox" id="mobileTooltip" class="modal-toggle" />
<dialog id="mobileTooltip" class="modal p-3">
<label
for="mobileTooltip"
class="cursor-pointer modal-backdrop bg-[#000] bg-opacity-[0.8]"
></label>
<!-- Desktop modal content -->
<div
class="modal-box rounded-md border border-gray-600 w-full bg-secondary flex flex-col items-center"
>
<div class="text-white mb-5 text-center">
<h3 class="font-bold text-2xl mb-5">{tooltipTitle}</h3>
<span class="text-white text-[1rem] font-normal"
>{infoText?.text ?? "n/a"}</span
>
{#if infoText?.equation !== undefined}
<div class="w-5/6 m-auto mt-5"></div>
<div class="text-[1rem] w-full pt-3 pb-3 m-auto text-white">
{infoText?.equation}
</div>
{/if}
</div>
<div class="border-t border-gray-600 mt-2 w-full">
<label
for="mobileTooltip"
class="cursor-pointer mt-4 font-semibold text-white text-xl m-auto flex justify-center"
>
Close
</label>
</div>
</div>
</dialog>
<!--Start Login Modal-->
{#if LoginPopup}
<LoginPopup {form} />
{/if}
<!--End Login Modal-->
<style>
.scroller {
scrollbar-width: thin;
}
.scrollbar {
display: grid;
grid-gap: 90px;
grid-template-columns: repeat(auto-fill, minmax(80px, 1fr));
grid-auto-flow: column;
overflow-x: auto;
scrollbar-width: thin; /* Hide the default scrollbar in Firefox */
scrollbar-color: transparent transparent; /* Hide the default scrollbar in Firefox */
}
/* Custom scrollbar for Webkit (Chrome, Safari) */
.scrollbar::-webkit-scrollbar {
width: 0; /* Hide the width of the scrollbar */
height: 0; /* Hide the height of the scrollbar */
}
.scrollbar::-webkit-scrollbar-thumb {
background: transparent; /* Make the thumb transparent */
}
</style>