SPB Git forge

spb/hfmarketdata

Public

Open high-frequency market data platform — FirstRate full-history downloader, DuckDB/Parquet lake, open REST API and React docs platform (www.hfmarketdata.io)

127commits 1branches 0releases
24.7 MBsize
maindefault branch
11 days agolast push
JavaScript 53.7% Python 38.3% CSS 4.6% TypeScript 3.1%
5.6 KB · 137 lines typescript
Raw Blame History
1/**2 * Turns API payloads into compact, LLM-friendly text while keeping tool outputs bounded.3 *4 * Tabular payloads are rendered as `{"columns":[…],"rows":[[…]]}` (about half the tokens of an5 * array of objects). Above `fullRowsThreshold` rows only the head/tail are kept, together with the6 * total count, the columns, and a hint to narrow the request.7 */8import { describeRate, type ApiResult, type RateInfo } from './client.js';910export const DEFAULT_LIMIT = 500;11export const MAX_LIMIT = 5000;12export const FULL_ROWS_THRESHOLD = 200;13export const HEAD_TAIL = 15;1415export type Row = Record<string, unknown>;1617export interface Extracted {18  rows: Row[] | null;19  meta: Record<string, unknown>;20  /** Anything that is not the rows nor the meta (e.g. v1 `tickers` list). */21  rest: Record<string, unknown>;22}2324/** Understand both envelopes: v1 `{count,data}` and v2 `{data,meta}`, plus bare arrays. */25export function extract(body: unknown): Extracted {26  if (Array.isArray(body)) return { rows: body.every(isRow) ? (body as Row[]) : null, meta: {}, rest: body.every(isRow) ? {} : { data: body } };27  if (!body || typeof body !== 'object') return { rows: null, meta: {}, rest: { value: body } };28  const b = body as Record<string, unknown>;29  const meta: Record<string, unknown> = typeof b.meta === 'object' && b.meta ? { ...(b.meta as Record<string, unknown>) } : {};30  if (typeof b.count === 'number' && meta.count === undefined) meta.count = b.count;31  const rest: Record<string, unknown> = {};32  for (const [k, v] of Object.entries(b)) if (k !== 'data' && k !== 'meta' && k !== 'count') rest[k] = v;33  const data = b.data;34  if (Array.isArray(data)) {35    if (data.every(isRow)) return { rows: data as Row[], meta, rest };36    return { rows: null, meta, rest: { ...rest, data } };37  }38  if (data && typeof data === 'object') return { rows: null, meta, rest: { ...rest, data } };39  return { rows: null, meta, rest };40}4142function isRow(x: unknown): x is Row {43  return !!x && typeof x === 'object' && !Array.isArray(x);44}4546export function columnsOf(rows: Row[]): string[] {47  const cols: string[] = [];48  const seen = new Set<string>();49  for (const r of rows.slice(0, 50)) for (const k of Object.keys(r)) if (!seen.has(k)) { seen.add(k); cols.push(k); }50  return cols;51}5253function cell(v: unknown): unknown {54  if (typeof v === 'number' && !Number.isInteger(v)) return Number(v.toFixed(6));55  return v === undefined ? null : v;56}5758export function toTable(rows: Row[], columns = columnsOf(rows)): { columns: string[]; rows: unknown[][] } {59  return { columns, rows: rows.map((r) => columns.map((c) => cell(r[c]))) };60}6162export interface RenderOptions {63  /** Rows above which the output is summarised (head + tail). */64  fullRowsThreshold?: number;65  headTail?: number;66  title?: string;67  hint?: string;68}6970/** Render a full API result as text. */71export function renderResult(result: ApiResult, opts: RenderOptions = {}): string {72  const { rows, meta, rest } = extract(result.body);73  const lines: string[] = [];74  if (opts.title) lines.push(opts.title);75  const out: Record<string, unknown> = {};76  if (rows) {77    const threshold = opts.fullRowsThreshold ?? FULL_ROWS_THRESHOLD;78    const ht = opts.headTail ?? HEAD_TAIL;79    const total = rows.length;80    const columns = columnsOf(rows);81    out.row_count = total;82    if (typeof meta.count === 'number' && meta.count !== total) out.count_reported = meta.count;83    out.columns = columns;84    if (total <= threshold) {85      out.rows = toTable(rows, columns).rows;86    } else {87      out.truncated = true;88      out.first_rows = toTable(rows.slice(0, ht), columns).rows;89      out.last_rows = toTable(rows.slice(-ht), columns).rows;90      out.numeric_summary = numericSummary(rows, columns);91      out.hint = opts.hint ?? `Showing ${ht} first and ${ht} last rows of ${total}. Narrow the date range, use a coarser timeframe, or lower \`limit\` to see every row.`;92    }93    if (meta.next_cursor) out.next_cursor = meta.next_cursor;94  }95  const metaRest = { ...meta };96  delete metaRest.count;97  if (Object.keys(metaRest).length) out.meta = metaRest;98  for (const [k, v] of Object.entries(rest)) out[k] = shrink(v);99  lines.push(JSON.stringify(out));100  const rate = describeRate(result.rate);101  if (rate) lines.push(rate);102  return lines.join('\n');103}104105/** Long arrays inside non-tabular payloads (e.g. 7 000 tickers) get truncated with a count. */106export function shrink(v: unknown, max = 400): unknown {107  if (Array.isArray(v) && v.length > max) return { count: v.length, first: v.slice(0, max), truncated: true };108  if (v && typeof v === 'object' && !Array.isArray(v)) {109    const o: Record<string, unknown> = {};110    for (const [k, x] of Object.entries(v as Record<string, unknown>)) o[k] = shrink(x, max);111    return o;112  }113  return v;114}115116export function numericSummary(rows: Row[], columns: string[]): Record<string, { min: number; max: number; first: number | null; last: number | null }> {117  const s: Record<string, { min: number; max: number; first: number | null; last: number | null }> = {};118  for (const c of columns) {119    let min = Infinity, max = -Infinity, first: number | null = null, last: number | null = null, n = 0;120    for (const r of rows) {121      const v = r[c];122      if (typeof v !== 'number' || !Number.isFinite(v)) continue;123      n++;124      if (first === null) first = v;125      last = v;126      if (v < min) min = v;127      if (v > max) max = v;128    }129    if (n >= 2 && c !== 'datetime') s[c] = { min: Number(min.toFixed(6)), max: Number(max.toFixed(6)), first, last };130  }131  return s;132}133134export function rateLine(rate: RateInfo | undefined): string {135  return describeRate(rate) ?? 'Rate limit: headers not provided by the server';136}137