1#!/bin/bash2# fdq — quick FinData OS client for shell sessions (Claude Code or human).3# Reads the API key from ~/Desktop/Cluster/secrets/findata/api-key-claude-code.txt (or $FINDATA_API_KEY).4# fdq health # status + alerts5# fdq datasets # catalog (id, status, rows)6# fdq bars AAPL 1m 2024-03-01 2024-03-31 [raw|split|split_div] # JSON page (<= 10 000 rows); use export for more7# fdq sql "SELECT ... FROM market_bars_stock_1m_raw WHERE symbol='AAPL' AND year=2024 ..." [max_rows]8# fdq fund AAPL [as_of_iso] [metrics,comma] # point-in-time fundamentals9# fdq export "AAPL,MSFT" 2024-01-01 2024-12-31 [dataset_id] [out.parquet] # async Parquet export -> file10# fdq jobs | fdq enqueue '{"kind":"incremental","params":{"type":"stock","period":"day"}}' # admin11# fdq get /api/v1/... | fdq post /api/v1/... '{json}' # raw12set -euo pipefail13BASE="${FINDATA_URL:-https://findata.spboucher.ai}"14KEY="${FINDATA_API_KEY:-$(cat "$HOME/Desktop/Cluster/secrets/findata/api-key-claude-code.txt")}"15export BASE KEY16j() { python3 -c 'import sys,json; d=json.load(sys.stdin); print(json.dumps(d, indent=1, ensure_ascii=False))'; }17cmd="${1:-help}"; shift || true18case "$cmd" in19 health) curl -s -H "Authorization: Bearer $KEY" "$BASE/api/v1/health" | j ;;20 datasets) curl -s -H "Authorization: Bearer $KEY" "$BASE/api/v1/catalog/datasets" | python3 -c 'import sys,json21for d in json.load(sys.stdin)["data"]:22 print(f"{d[\"id\"]:<44} {d[\"status\"]:<10} rows={(d.get(\"row_count\") or 0):>14,} {d.get(\"first_session\") or \"\"} -> {d.get(\"last_session\") or \"\"}")' ;;23 bars) curl -s -H "Authorization: Bearer $KEY" --get "$BASE/api/v1/market/bars" --data-urlencode "symbol=$1" --data-urlencode "interval=$2" --data-urlencode "start=$3" --data-urlencode "end=$4" --data-urlencode "adjustment=${5:-raw}" --data-urlencode "limit=${LIMIT:-10000}" | j ;;24 sql) SQL="$1" MAXROWS="${2:-1000}" python3 -c '25import os, sys, json, urllib.request26req = urllib.request.Request(os.environ["BASE"] + "/api/v1/query", data=json.dumps({"sql": os.environ["SQL"], "max_rows": int(os.environ["MAXROWS"])}).encode(),27 headers={"Authorization": "Bearer " + os.environ["KEY"], "Content-Type": "application/json"})28try:29 with urllib.request.urlopen(req, timeout=180) as r: d = json.load(r)30except urllib.error.HTTPError as e:31 sys.exit(e.read().decode()[:800])32print(json.dumps({"meta": d["meta"], "columns": [c["name"] for c in d["columns"]]}, indent=1))33for row in d["rows"]: print(json.dumps(row, ensure_ascii=False))' ;;34 fund) curl -s -H "Authorization: Bearer $KEY" --get "$BASE/api/v1/fundamentals/$1" ${2:+--data-urlencode "as_of=$2"} ${3:+--data-urlencode "metrics=$3"} | j ;;35 export) SYMS="$1" START="$2" END="$3" DS="${4:-market_bars.stock.1m.raw}" OUT="${5:-findata-export.parquet}" python3 -c '36import os, sys, json, time, urllib.request37base, key = os.environ["BASE"], os.environ["KEY"]38hdr = {"Authorization": "Bearer " + key, "Content-Type": "application/json"}39body = {"dataset_id": os.environ["DS"], "symbols": os.environ["SYMS"].split(","), "start": os.environ["START"], "end": os.environ["END"]}40r = json.load(urllib.request.urlopen(urllib.request.Request(base + "/api/v1/exports", data=json.dumps(body).encode(), headers=hdr)))41eid = r["id"]42while True:43 st = json.load(urllib.request.urlopen(urllib.request.Request(base + "/api/v1/exports/" + eid, headers=hdr)))44 if st["status"] == "ready": break45 if st["status"] in ("failed", "expired"): sys.exit("export " + st["status"] + ": " + str(st.get("error")))46 time.sleep(2)47with urllib.request.urlopen(urllib.request.Request(base + "/api/v1/exports/" + eid + "/download", headers=hdr)) as resp, open(os.environ["OUT"], "wb") as f:48 f.write(resp.read())49print(os.environ["OUT"] + ": " + str(st["row_count"]) + " rows, " + str(st["bytes"]) + " bytes, dataset v" + str(st["snapshot"].get("version")))' ;;50 jobs) curl -s -H "Authorization: Bearer $KEY" "$BASE/api/v1/admin/jobs?limit=${LIMIT:-30}" | python3 -c 'import sys,json51for j in json.load(sys.stdin)["data"]:52 print(f"{j[\"id\"][:8]} {j[\"kind\"]:<12} {str(j[\"dataset_id\"]):<36} {j[\"status\"]:<9} {str(j[\"created_at\"])[:16]} {str(j.get(\"last_error\") or \"\")[:60]}")' ;;53 enqueue) curl -s -H "Authorization: Bearer $KEY" -H 'Content-Type: application/json' -d "$1" "$BASE/api/v1/admin/jobs" | j ;;54 get) curl -s -H "Authorization: Bearer $KEY" "$BASE$1" | j ;;55 post) curl -s -H "Authorization: Bearer $KEY" -H 'Content-Type: application/json' -d "${2:-{}}" "$BASE$1" | j ;;56 *) sed -n '2,12p' "$0" ;;57esac58