3815 lines
125 KiB
Svelte
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>
|