import {z} from 'zod' export type GetApiV1ScreenerStocksArgs = {minMarketCap?: number | undefined, maxMarketCap?: number | undefined, minPrice?: number | undefined, maxPrice?: number | undefined, minFilerCount?: number | undefined, maxFilerCount?: number | undefined, minFilerDelta?: number | undefined, maxFilerDelta?: number | undefined, minShortInterestPercent?: number | undefined, maxShortInterestPercent?: number | undefined, minDaysToCover?: number | undefined, maxDaysToCover?: number | undefined, minSqueezeScore?: number | undefined, maxSqueezeScore?: number | undefined, minInsiderSentiment?: number | undefined, maxInsiderSentiment?: number | undefined, minNetInsiderBuy?: number | undefined, maxNetInsiderBuy?: number | undefined, hasGoingConcernDoubt?: boolean | undefined, sector?: string | undefined, industry?: string | undefined, index?: string | undefined, market?: string | undefined, minPe?: number | undefined, maxPe?: number | undefined, minDividendYield?: number | undefined, maxDividendYield?: number | undefined, minRevenueGrowth?: number | undefined, maxRevenueGrowth?: number | undefined, minGrossMargin?: number | undefined, maxGrossMargin?: number | undefined, minDollarVolume?: number | undefined, maxDollarVolume?: number | undefined, minNetIncome?: number | undefined, maxNetIncome?: number | undefined, minInstitutionalOwnership?: number | undefined, maxInstitutionalOwnership?: number | undefined, minConvictionShare?: number | undefined, maxConvictionShare?: number | undefined, minCrowding?: number | undefined, maxCrowding?: number | undefined, minPrivateFundOwnership?: number | undefined, maxPrivateFundOwnership?: number | undefined, minActiveHolderChange?: number | undefined, maxActiveHolderChange?: number | undefined, minHolderTenure?: number | undefined, maxHolderTenure?: number | undefined, minEstimatedCostGap?: number | undefined, maxEstimatedCostGap?: number | undefined, sortBy?: string | undefined, sortAscending?: boolean | undefined, limit?: number | undefined, page?: number | undefined, tickers?: string | undefined, format?: string | undefined, cols?: string | undefined}; export type GetApiV1ScreenerStocksOptions = {baseUrl?: string | undefined, ApiKey?: string | undefined, fetch?: typeof fetch | undefined}; export const screenerVintagesDto = z.object({shortInterestSettlementDate: z.string().nullable().optional(), thirteenFQuarterEnd: z.string().nullable().optional(), holderSignalsQuarterEnd: z.string().nullable().optional()}); export const screenerRowDto = z.object({ticker: z.string().nullable().optional(), name: z.string().nullable().optional(), market: z.string().nullable().optional(), currency: z.string().nullable().optional(), localPrice: z.number().nullable().optional(), sector: z.string().nullable().optional(), industry: z.string().nullable().optional(), marketCap: z.number().optional(), price: z.number().nullable().optional(), priceDate: z.string().nullable().optional(), filerCount: z.number().int().nullable().optional(), filerDelta: z.number().int().nullable().optional(), shortInterestPercent: z.number().nullable().optional(), daysToCover: z.number().nullable().optional(), squeezeScore: z.number().nullable().optional(), insiderSentimentScore: z.number().nullable().optional(), netInsiderBuy: z.number().nullable().optional(), hasGoingConcernDoubt: z.boolean().optional(), priceToEarnings: z.number().nullable().optional(), dividendYieldPercent: z.number().nullable().optional(), revenueGrowthYoYPercent: z.number().nullable().optional(), grossMarginPercent: z.number().nullable().optional(), averageDollarVolume3Month: z.number().nullable().optional(), netIncomeTtm: z.number().nullable().optional(), institutionalOwnershipPercent: z.number().nullable().optional(), convictionSharePercent: z.number().nullable().optional(), crowdingPercent: z.number().nullable().optional(), privateFundOwnershipPercent: z.number().nullable().optional(), activeHolderChangePercent: z.number().nullable().optional(), holderTenureQuarters: z.number().nullable().optional(), estimatedCostGapPercent: z.number().nullable().optional()}); export const screenerResponse = z.object({totalCount: z.number().int().optional(), count: z.number().int().optional(), page: z.number().int().optional(), totalPages: z.number().int().optional(), vintages: screenerVintagesDto.optional(), notFound: z.array(z.string()).nullable().optional(), data: z.array(screenerRowDto).nullable().optional()}); export const validateResponse = async ( schema: { readonly '~standard': { readonly validate: (value: unknown) => ({ readonly value: Output; readonly issues?: undefined } | { readonly issues: ReadonlyArray<{ readonly message: string; readonly path?: ReadonlyArray | undefined }> }) | Promise<{ readonly value: Output; readonly issues?: undefined } | { readonly issues: ReadonlyArray<{ readonly message: string; readonly path?: ReadonlyArray | undefined }> }> } }, value: unknown ): Promise => { const result = await schema['~standard'].validate(value) if (result.issues !== undefined) { const detail = result.issues .map(({ message, path }) => path?.length ? `${path.map((segment) => String(typeof segment === 'object' ? segment.key : segment)).join('.')}: ${message}` : message ) .join('; ') throw new Error(`Response does not match the schema: ${detail}`, { cause: { issues: result.issues } }) } return result.value }; /** * Screen the stock universe by combining optional range filters across datasets: market cap and price, sector and industry, 13F filer count and its change, institutional ownership and the 13F holder signals of the newest computed closed quarter (conviction share, crowding, private-fund adviser ownership, active holders' share change, holder tenure, and price versus the holders' estimated average cost), short interest and days to cover, composite short-squeeze and insider-sentiment scores, net insider buying (90d), the going-concern doubt flag, and daily-refreshed derived metrics (trailing P/E, dividend yield from split-restated TTM cash dividends per share divided by share price, revenue growth YoY, gross margin, average daily dollar volume, TTM net income). A bound on a metric excludes stocks with no data on that axis. Sorted by the chosen metric (default market cap, descending); page walks past the first page and vintages state each dataset's freshness. * * GET /v1/screener/stocks */ export const getApiV1ScreenerStocks = async (args: GetApiV1ScreenerStocksArgs, options?: GetApiV1ScreenerStocksOptions) => { const {baseUrl = "https://api.equibles.com", fetch: fetchFn = fetch, ApiKey} = options ?? {} const query = new URLSearchParams() const headers = new Headers() headers.set('accept', "application/json") if (args.minMarketCap != null) query.append("minMarketCap", String(args.minMarketCap)) if (args.maxMarketCap != null) query.append("maxMarketCap", String(args.maxMarketCap)) if (args.minPrice != null) query.append("minPrice", String(args.minPrice)) if (args.maxPrice != null) query.append("maxPrice", String(args.maxPrice)) if (args.minFilerCount != null) query.append("minFilerCount", String(args.minFilerCount)) if (args.maxFilerCount != null) query.append("maxFilerCount", String(args.maxFilerCount)) if (args.minFilerDelta != null) query.append("minFilerDelta", String(args.minFilerDelta)) if (args.maxFilerDelta != null) query.append("maxFilerDelta", String(args.maxFilerDelta)) if (args.minShortInterestPercent != null) query.append("minShortInterestPercent", String(args.minShortInterestPercent)) if (args.maxShortInterestPercent != null) query.append("maxShortInterestPercent", String(args.maxShortInterestPercent)) if (args.minDaysToCover != null) query.append("minDaysToCover", String(args.minDaysToCover)) if (args.maxDaysToCover != null) query.append("maxDaysToCover", String(args.maxDaysToCover)) if (args.minSqueezeScore != null) query.append("minSqueezeScore", String(args.minSqueezeScore)) if (args.maxSqueezeScore != null) query.append("maxSqueezeScore", String(args.maxSqueezeScore)) if (args.minInsiderSentiment != null) query.append("minInsiderSentiment", String(args.minInsiderSentiment)) if (args.maxInsiderSentiment != null) query.append("maxInsiderSentiment", String(args.maxInsiderSentiment)) if (args.minNetInsiderBuy != null) query.append("minNetInsiderBuy", String(args.minNetInsiderBuy)) if (args.maxNetInsiderBuy != null) query.append("maxNetInsiderBuy", String(args.maxNetInsiderBuy)) if (args.hasGoingConcernDoubt != null) query.append("hasGoingConcernDoubt", String(args.hasGoingConcernDoubt)) if (args.sector != null) query.append("sector", String(args.sector)) if (args.industry != null) query.append("industry", String(args.industry)) if (args.index != null) query.append("index", String(args.index)) if (args.market != null) query.append("market", String(args.market)) if (args.minPe != null) query.append("minPe", String(args.minPe)) if (args.maxPe != null) query.append("maxPe", String(args.maxPe)) if (args.minDividendYield != null) query.append("minDividendYield", String(args.minDividendYield)) if (args.maxDividendYield != null) query.append("maxDividendYield", String(args.maxDividendYield)) if (args.minRevenueGrowth != null) query.append("minRevenueGrowth", String(args.minRevenueGrowth)) if (args.maxRevenueGrowth != null) query.append("maxRevenueGrowth", String(args.maxRevenueGrowth)) if (args.minGrossMargin != null) query.append("minGrossMargin", String(args.minGrossMargin)) if (args.maxGrossMargin != null) query.append("maxGrossMargin", String(args.maxGrossMargin)) if (args.minDollarVolume != null) query.append("minDollarVolume", String(args.minDollarVolume)) if (args.maxDollarVolume != null) query.append("maxDollarVolume", String(args.maxDollarVolume)) if (args.minNetIncome != null) query.append("minNetIncome", String(args.minNetIncome)) if (args.maxNetIncome != null) query.append("maxNetIncome", String(args.maxNetIncome)) if (args.minInstitutionalOwnership != null) query.append("minInstitutionalOwnership", String(args.minInstitutionalOwnership)) if (args.maxInstitutionalOwnership != null) query.append("maxInstitutionalOwnership", String(args.maxInstitutionalOwnership)) if (args.minConvictionShare != null) query.append("minConvictionShare", String(args.minConvictionShare)) if (args.maxConvictionShare != null) query.append("maxConvictionShare", String(args.maxConvictionShare)) if (args.minCrowding != null) query.append("minCrowding", String(args.minCrowding)) if (args.maxCrowding != null) query.append("maxCrowding", String(args.maxCrowding)) if (args.minPrivateFundOwnership != null) query.append("minPrivateFundOwnership", String(args.minPrivateFundOwnership)) if (args.maxPrivateFundOwnership != null) query.append("maxPrivateFundOwnership", String(args.maxPrivateFundOwnership)) if (args.minActiveHolderChange != null) query.append("minActiveHolderChange", String(args.minActiveHolderChange)) if (args.maxActiveHolderChange != null) query.append("maxActiveHolderChange", String(args.maxActiveHolderChange)) if (args.minHolderTenure != null) query.append("minHolderTenure", String(args.minHolderTenure)) if (args.maxHolderTenure != null) query.append("maxHolderTenure", String(args.maxHolderTenure)) if (args.minEstimatedCostGap != null) query.append("minEstimatedCostGap", String(args.minEstimatedCostGap)) if (args.maxEstimatedCostGap != null) query.append("maxEstimatedCostGap", String(args.maxEstimatedCostGap)) if (args.sortBy != null) query.append("sortBy", String(args.sortBy)) if (args.sortAscending != null) query.append("sortAscending", String(args.sortAscending)) if (args.limit != null) query.append("limit", String(args.limit)) if (args.page != null) query.append("page", String(args.page)) if (args.tickers != null) query.append("tickers", String(args.tickers)) if (args.format != null) query.append("format", String(args.format)) if (args.cols != null) query.append("cols", String(args.cols)) if (ApiKey !== undefined) headers.set('authorization', `Bearer ${ApiKey}`) const url = `${baseUrl.replace(/\/+$/, '')}/v1/screener/stocks${query.size > 0 ? `?${query}` : ''}` const res = await fetchFn(url, {method: "GET", headers}) if (!res.ok) throw new Error(`Request failed with status ${res.status}`, {cause: {status: res.status, body: await res.text()}}) return validateResponse(screenerResponse, await res.json()) };