1// FinData OS client (https://findata.spboucher.ai/api/v1) — bearer auth from FINDATA_API_KEY / FINDATA_API_KEY_FILE,2// TLS via libcurl, redacted logs, token-bucket rate limiting, bounded concurrency, retries with exponential backoff3// honouring Retry-After, cursor pagination, 31-day chunking for 1m bars, asynchronous exports with polling/download,4// on-disk cache keyed by dataset version, SHA-256 of every downloaded artifact, and provenance (request ids, versions).5#pragma once6#include <string>7#include <vector>8#include <map>9#include <optional>10#include <mutex>11#include <chrono>12#include <functional>13#include <nlohmann/json.hpp>14#include "raid/core/types.hpp"15#include "raid/core/estimate.hpp"16#include "raid/data/bar_series.hpp"1718namespace raid::findata {1920struct Config {21 std::string base_url{"https://findata.spboucher.ai/api/v1"};22 std::string api_key; // empty → FINDATA_API_KEY, then FINDATA_API_KEY_FILE23 std::string cache_dir; // empty → $RAID_CACHE_DIR or ./datasets/cache24 double requests_per_second{5.0}; // token bucket rate (600/min limit → 5/s leaves headroom)25 double burst{10.0};26 int max_retries{5};27 long timeout_seconds{120};28 unsigned max_concurrency{4};29 bool verify_tls{true};30 bool offline{false}; // only serve from cache; any network call throws (for CI)31 static Config from_env();32};3334/// Provenance of one HTTP exchange (no secrets).35struct Exchange {36 std::string method, path; // query string without the key (the key is a header)37 long status{0};38 std::string request_id, dataset_id; // X-Request-ID, X-Dataset-Id39 i64 dataset_version{-1}; // X-Dataset-Version40 std::string next_cursor; // X-Next-Cursor (arrow pages)41 double seconds{0};42 usize bytes{0};43 int attempts{1};44};4546class TokenBucket {47 public:48 TokenBucket(double rate, double burst) : rate_(rate), capacity_(burst), tokens_(burst), last_(std::chrono::steady_clock::now()) {}49 /// Blocks until a token is available; returns the wait in seconds.50 double acquire();51 private:52 double rate_, capacity_, tokens_; std::chrono::steady_clock::time_point last_; std::mutex m_;53};5455/// Splits [start, end] (inclusive dates) into chunks of at most `max_days` calendar days (API constraint: 1m ≤ 31 d,56/// 5m ≤ 186 d, 1d ≤ 10 y; exports ≤ 12 y).57std::vector<std::pair<Date, Date>> chunk_dates(Date start, Date end, int max_days);58int max_days_per_call(Interval iv);5960struct BarsRequest {61 std::string symbol; Interval interval{Interval::m1}; Date start, end; std::string adjustment{"raw"}; usize page_limit{10000}; std::optional<i64> version;62};63struct BarsResult { BarSeries bars; std::vector<Exchange> exchanges; i64 dataset_version{-1}; std::string dataset_id; usize duplicates_dropped{0}; std::string cache_path; bool from_cache{false}; };6465struct ExportRequest { std::string dataset_id{"market_bars.stock.1m.raw"}; std::vector<std::string> symbols; Date start, end; std::string adjustment{"raw"}; };66struct ExportResult { std::string export_id, path, sha256; i64 dataset_version{-1}; usize rows{0}; usize bytes{0}; std::vector<Exchange> exchanges; bool from_cache{false}; double seconds_total{0}; };6768class Client {69 public:70 explicit Client(Config cfg = Config::from_env());71 ~Client();72 const Config& config() const { return cfg_; }73 std::string key_fingerprint() const; // first 12 hex of sha256(key): identifies the key without revealing it7475 nlohmann::json health();76 nlohmann::json openapi();77 nlohmann::json catalog_datasets();78 nlohmann::json dataset(const std::string& id);79 nlohmann::json sessions(const std::string& symbol, Interval iv, Date start, Date end);80 nlohmann::json coverage_symbols(const std::string& dataset_id, int min_sessions, usize limit = 200, const std::string& cursor = "");81 /// All symbols with ≥ min_sessions sessions (follows cursors); returns {symbol, sessions, d0, d1, bars}.82 nlohmann::json coverage_symbols_all(const std::string& dataset_id, int min_sessions, usize max_symbols = 100000);83 nlohmann::json fundamentals(const std::string& entity, const std::string& as_of_iso, const std::vector<std::string>& metrics = {}, const std::string& period_type = "");84 /// Bounded read-only SQL (server: 30 s, ≤ 100 000 rows). Use only for universe/coverage questions.85 nlohmann::json query(const std::string& sql, usize max_rows = 1000);8687 /// Bars via GET /market/bars?format=arrow with cursor pagination and ≤ max_days chunking; cached per dataset version.88 BarsResult bars(const BarsRequest& req);89 /// Asynchronous export (POST /exports → poll → download) with ≤ 10-year chunking handled by the caller; cached.90 ExportResult export_bars(const ExportRequest& req, const std::string& out_path = "");91 /// Convenience: export, then read into BarSeries per symbol (requires Arrow build).92 std::map<std::string, BarSeries> export_and_read(const ExportRequest& req);9394 const std::vector<Exchange>& log() const { return log_; }95 Provenance provenance(const std::string& dataset_id) const;9697 struct Response { long status{0}; std::string body; std::map<std::string, std::string> headers; double seconds{0}; int attempts{1}; };98 Response request(const std::string& method, const std::string& path_and_query, const std::string& body = "", const std::string& content_type = "application/json", const std::string& download_to = "");99100 private:101 Response request_once(const std::string& method, const std::string& url, const std::string& body, const std::string& content_type, const std::string& download_to);102 std::string cache_file(const std::string& kind, const std::string& key_material, const std::string& ext) const;103 Config cfg_; std::string key_; TokenBucket bucket_; std::vector<Exchange> log_; std::mutex log_m_;104 void* curl_share_{nullptr};105};106107/// `raid data …` subcommands (returns exit code).108int cli_main(const std::vector<std::string>& args);109110} // namespace raid::findata111