import {z} from 'zod' export type GetApiV1StocksTickerHolderMixArgs = {ticker: string, dimension?: string | undefined, periods?: number | undefined}; export type GetApiV1StocksTickerHolderMixOptions = {baseUrl?: string | undefined, ApiKey?: string | undefined, fetch?: typeof fetch | undefined}; export const listingNotice = z.object({status: z.string().nullable().optional(), ticker: z.string().nullable().optional(), delistedOn: z.string().nullable().optional(), successorTicker: z.string().nullable().optional()}); export const holderMixBucketDto = z.object({token: z.string().nullable().optional(), label: z.string().nullable().optional(), shares: z.number().int().optional(), shareOfTotal: z.number().nullable().optional(), value: z.number().int().optional(), filers: z.number().int().optional(), likeForLikeCurrentShares: z.number().int().nullable().optional(), likeForLikePreviousShares: z.number().int().nullable().optional(), likeForLikeChangePercent: z.number().nullable().optional(), excludedCurrentShares: z.number().int().optional(), excludedPreviousShares: z.number().int().optional(), newFilers: z.number().int().optional(), exitedFilers: z.number().int().optional(), callShares: z.number().int().optional(), callValue: z.number().int().optional(), putShares: z.number().int().optional(), putValue: z.number().int().optional()}); export const holderMixQuarterDto = z.object({reportDate: z.string().optional(), isCombinedQuarter: z.boolean().optional(), measuredQuarter: z.string().nullable().optional(), comparisonAvailable: z.boolean().optional(), coverageNote: z.string().nullable().optional(), total: holderMixBucketDto.optional(), activeHolders: holderMixBucketDto.optional(), buckets: z.array(holderMixBucketDto).nullable().optional()}); export const fundOwnershipBucketDto = z.object({token: z.string().nullable().optional(), label: z.string().nullable().optional(), shares: z.number().int().optional(), value: z.number().optional(), seriesCount: z.number().int().optional(), percentOfSharesOutstanding: z.number().nullable().optional(), oldestReportPeriod: z.string().nullable().optional(), newestReportPeriod: z.string().nullable().optional()}); export const fundOwnershipQuarterDto = z.object({reportDate: z.string().optional(), indexShares: z.number().int().optional(), indexPercentOfSharesOutstanding: z.number().nullable().optional(), splitUnresolved: z.boolean().optional(), total: fundOwnershipBucketDto.optional(), buckets: z.array(fundOwnershipBucketDto).nullable().optional()}); export const holderMixResponse = z.object({listingNotice: listingNotice.optional(), ticker: z.string().nullable().optional(), dimension: z.string().nullable().optional(), dimensionLabel: z.string().nullable().optional(), exactListing: z.boolean().optional(), carriedFrom: z.string().nullable().optional(), quarters: z.array(holderMixQuarterDto).nullable().optional(), fundQuarters: z.array(fundOwnershipQuarterDto).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 }; /** * A stock or ETF listing's 13F holders split by one dimension, quarter by quarter, oldest * first: each bucket's common shares, value and filers, its like-for-like change (only filers * with an observed 13F in both quarters, each kept in its bucket of the later quarter on both * sides), and new and exited filers. Every dimension's buckets sum to total. Holder type is each * filer's current regulatory profile, never its name; client base and private funds come from * the Form ADV roster current at the column's quarter end (rosters from December 2019, so an * earlier column reads Unknown); style, turnover, horizon and book size are measured from each * filer's 13F book in the newest measured quarter on or before the column. A company's primary listing reads its open quarter * as the combined view; an ETF series or sibling class reads its exact listing as filed. Shares * are restated to today's split basis; options are reported beside them and principal amounts * are left out. dimension=fundtype reads registered funds instead: each fund series' latest Form * NPORT-P report inside the quarter, bucketed by its Form N-CEN fund type (index fund, index * ETF, active ETF, active fund, unclassified), as shares and a percentage of shares * outstanding in fundQuarters. Funds sit inside their advisers' 13F reports, so those shares * are never added to 13F totals. * * GET /v1/stocks/{ticker}/holder-mix */ export const getApiV1StocksTickerHolderMix = async (args: GetApiV1StocksTickerHolderMixArgs, options?: GetApiV1StocksTickerHolderMixOptions) => { 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.dimension != null) query.append("dimension", String(args.dimension)) if (args.periods != null) query.append("periods", String(args.periods)) if (ApiKey !== undefined) headers.set('authorization', `Bearer ${ApiKey}`) const url = `${baseUrl.replace(/\/+$/, '')}/v1/stocks/${encodeURIComponent(String(args.ticker))}/holder-mix${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(holderMixResponse, await res.json()) };