// FinData OS client (https://findata.spboucher.ai/api/v1) — bearer auth from FINDATA_API_KEY / FINDATA_API_KEY_FILE, // TLS via libcurl, redacted logs, token-bucket rate limiting, bounded concurrency, retries with exponential backoff // honouring Retry-After, cursor pagination, 31-day chunking for 1m bars, asynchronous exports with polling/download, // on-disk cache keyed by dataset version, SHA-256 of every downloaded artifact, and provenance (request ids, versions). #pragma once #include #include #include #include #include #include #include #include #include "raid/core/types.hpp" #include "raid/core/estimate.hpp" #include "raid/data/bar_series.hpp" namespace raid::findata { struct Config { std::string base_url{"https://findata.spboucher.ai/api/v1"}; std::string api_key; // empty → FINDATA_API_KEY, then FINDATA_API_KEY_FILE std::string cache_dir; // empty → $RAID_CACHE_DIR or ./datasets/cache double requests_per_second{5.0}; // token bucket rate (600/min limit → 5/s leaves headroom) double burst{10.0}; int max_retries{5}; long timeout_seconds{120}; unsigned max_concurrency{4}; bool verify_tls{true}; bool offline{false}; // only serve from cache; any network call throws (for CI) static Config from_env(); }; /// Provenance of one HTTP exchange (no secrets). struct Exchange { std::string method, path; // query string without the key (the key is a header) long status{0}; std::string request_id, dataset_id; // X-Request-ID, X-Dataset-Id i64 dataset_version{-1}; // X-Dataset-Version std::string next_cursor; // X-Next-Cursor (arrow pages) double seconds{0}; usize bytes{0}; int attempts{1}; }; class TokenBucket { public: TokenBucket(double rate, double burst) : rate_(rate), capacity_(burst), tokens_(burst), last_(std::chrono::steady_clock::now()) {} /// Blocks until a token is available; returns the wait in seconds. double acquire(); private: double rate_, capacity_, tokens_; std::chrono::steady_clock::time_point last_; std::mutex m_; }; /// Splits [start, end] (inclusive dates) into chunks of at most `max_days` calendar days (API constraint: 1m ≤ 31 d, /// 5m ≤ 186 d, 1d ≤ 10 y; exports ≤ 12 y). std::vector> chunk_dates(Date start, Date end, int max_days); int max_days_per_call(Interval iv); struct BarsRequest { std::string symbol; Interval interval{Interval::m1}; Date start, end; std::string adjustment{"raw"}; usize page_limit{10000}; std::optional version; }; struct BarsResult { BarSeries bars; std::vector exchanges; i64 dataset_version{-1}; std::string dataset_id; usize duplicates_dropped{0}; std::string cache_path; bool from_cache{false}; }; struct ExportRequest { std::string dataset_id{"market_bars.stock.1m.raw"}; std::vector symbols; Date start, end; std::string adjustment{"raw"}; }; struct ExportResult { std::string export_id, path, sha256; i64 dataset_version{-1}; usize rows{0}; usize bytes{0}; std::vector exchanges; bool from_cache{false}; double seconds_total{0}; }; class Client { public: explicit Client(Config cfg = Config::from_env()); ~Client(); const Config& config() const { return cfg_; } std::string key_fingerprint() const; // first 12 hex of sha256(key): identifies the key without revealing it nlohmann::json health(); nlohmann::json openapi(); nlohmann::json catalog_datasets(); nlohmann::json dataset(const std::string& id); nlohmann::json sessions(const std::string& symbol, Interval iv, Date start, Date end); nlohmann::json coverage_symbols(const std::string& dataset_id, int min_sessions, usize limit = 200, const std::string& cursor = ""); /// All symbols with ≥ min_sessions sessions (follows cursors); returns {symbol, sessions, d0, d1, bars}. nlohmann::json coverage_symbols_all(const std::string& dataset_id, int min_sessions, usize max_symbols = 100000); nlohmann::json fundamentals(const std::string& entity, const std::string& as_of_iso, const std::vector& metrics = {}, const std::string& period_type = ""); /// Bounded read-only SQL (server: 30 s, ≤ 100 000 rows). Use only for universe/coverage questions. nlohmann::json query(const std::string& sql, usize max_rows = 1000); /// Bars via GET /market/bars?format=arrow with cursor pagination and ≤ max_days chunking; cached per dataset version. BarsResult bars(const BarsRequest& req); /// Asynchronous export (POST /exports → poll → download) with ≤ 10-year chunking handled by the caller; cached. ExportResult export_bars(const ExportRequest& req, const std::string& out_path = ""); /// Convenience: export, then read into BarSeries per symbol (requires Arrow build). std::map export_and_read(const ExportRequest& req); const std::vector& log() const { return log_; } Provenance provenance(const std::string& dataset_id) const; struct Response { long status{0}; std::string body; std::map headers; double seconds{0}; int attempts{1}; }; 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 = ""); private: 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); std::string cache_file(const std::string& kind, const std::string& key_material, const std::string& ext) const; Config cfg_; std::string key_; TokenBucket bucket_; std::vector log_; std::mutex log_m_; void* curl_share_{nullptr}; }; /// `raid data …` subcommands (returns exit code). int cli_main(const std::vector& args); } // namespace raid::findata