@openfinclaw/findoo-datahub-plugin 2026.3.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,207 @@
1
+ import type { OHLCV, Ticker } from "./types.js";
2
+
3
+ /**
4
+ * DataHub REST API client.
5
+ * Single upstream for all financial data — routes internally to
6
+ * Tushare/yfinance/Polygon/CCXT/CoinGecko/DefiLlama/WorldBank.
7
+ *
8
+ * Auth: Basic admin:<apiKey>
9
+ * Response: { results: [...], provider: "...", warnings: null }
10
+ */
11
+ export class DataHubClient {
12
+ private authHeader: string;
13
+
14
+ constructor(
15
+ private baseUrl: string,
16
+ username: string,
17
+ password: string,
18
+ private timeoutMs: number,
19
+ ) {
20
+ this.authHeader = `Basic ${btoa(`${username}:${password}`)}`;
21
+ }
22
+
23
+ /* ============================================================
24
+ * Generic query — all category helpers delegate here
25
+ * ============================================================ */
26
+
27
+ async query(path: string, params?: Record<string, string>): Promise<unknown[]> {
28
+ const url = new URL(`${this.baseUrl}/api/v1/${path}`);
29
+ if (params) {
30
+ for (const [k, v] of Object.entries(params)) {
31
+ url.searchParams.set(k, v);
32
+ }
33
+ }
34
+
35
+ const resp = await fetch(url.toString(), {
36
+ headers: { Authorization: this.authHeader },
37
+ signal: AbortSignal.timeout(this.timeoutMs),
38
+ });
39
+
40
+ if (resp.status === 204) return [];
41
+
42
+ const text = await resp.text();
43
+ if (!resp.ok) {
44
+ throw new Error(`DataHub error (${resp.status}): ${text.slice(0, 300)}`);
45
+ }
46
+
47
+ let payload: { results?: unknown[]; error?: string; detail?: string };
48
+ try {
49
+ payload = JSON.parse(text);
50
+ } catch {
51
+ throw new Error(`DataHub returned non-JSON (${resp.status}): ${text.slice(0, 200)}`);
52
+ }
53
+
54
+ if (payload.detail) {
55
+ throw new Error(`DataHub: ${payload.detail}`);
56
+ }
57
+
58
+ return payload.results ?? [];
59
+ }
60
+
61
+ /* ============================================================
62
+ * 8 category helpers — thin wrappers over query()
63
+ * ============================================================ */
64
+
65
+ /** /api/v1/equity/* — A-share, HK, US equity data (83 endpoints) */
66
+ equity(endpoint: string, params?: Record<string, string>): Promise<unknown[]> {
67
+ return this.query(`equity/${endpoint}`, params);
68
+ }
69
+
70
+ /** /api/v1/crypto/* — CEX market data + DeFi + CoinGecko (23 endpoints) */
71
+ crypto(endpoint: string, params?: Record<string, string>): Promise<unknown[]> {
72
+ return this.query(`crypto/${endpoint}`, params);
73
+ }
74
+
75
+ /** /api/v1/economy/* — Macro, rates, FX, WorldBank (21 endpoints) */
76
+ economy(endpoint: string, params?: Record<string, string>): Promise<unknown[]> {
77
+ return this.query(`economy/${endpoint}`, params);
78
+ }
79
+
80
+ /** /api/v1/derivatives/* — Futures, options, convertible bonds (13 endpoints) */
81
+ derivatives(endpoint: string, params?: Record<string, string>): Promise<unknown[]> {
82
+ return this.query(`derivatives/${endpoint}`, params);
83
+ }
84
+
85
+ /** /api/v1/index/* — Index data, thematic indices (12 endpoints) */
86
+ index(endpoint: string, params?: Record<string, string>): Promise<unknown[]> {
87
+ return this.query(`index/${endpoint}`, params);
88
+ }
89
+
90
+ /** /api/v1/etf/* — ETF + Fund data (9 endpoints) */
91
+ etf(endpoint: string, params?: Record<string, string>): Promise<unknown[]> {
92
+ return this.query(`etf/${endpoint}`, params);
93
+ }
94
+
95
+ /** /api/v1/currency/* — FX historical, search, snapshots */
96
+ currency(endpoint: string, params?: Record<string, string>): Promise<unknown[]> {
97
+ return this.query(`currency/${endpoint}`, params);
98
+ }
99
+
100
+ /** /api/v1/coverage/* — Provider/endpoint metadata */
101
+ coverage(endpoint: string): Promise<unknown[]> {
102
+ return this.query(`coverage/${endpoint}`);
103
+ }
104
+
105
+ /* ============================================================
106
+ * Typed convenience methods (OHLCV + Ticker)
107
+ * ============================================================ */
108
+
109
+ async getOHLCV(params: {
110
+ symbol: string;
111
+ market: string;
112
+ timeframe: string;
113
+ since?: number;
114
+ limit?: number;
115
+ }): Promise<OHLCV[]> {
116
+ const { symbol, market } = params;
117
+ const queryParams: Record<string, string> = { symbol };
118
+
119
+ if (params.since) {
120
+ queryParams.start_date = new Date(params.since).toISOString().slice(0, 10);
121
+ }
122
+
123
+ if (market === "crypto") {
124
+ queryParams.provider = "ccxt";
125
+ const results = await this.crypto("price/historical", queryParams);
126
+ return this.normalizeOHLCV(results, params.limit);
127
+ }
128
+
129
+ if (market === "equity") {
130
+ queryParams.provider = detectEquityProvider(symbol);
131
+ const results = await this.equity("price/historical", queryParams);
132
+ return this.normalizeOHLCV(results, params.limit);
133
+ }
134
+
135
+ throw new Error(`DataHub: unsupported market "${market}" for OHLCV`);
136
+ }
137
+
138
+ async getTicker(symbol: string, market: string): Promise<Ticker> {
139
+ const queryParams: Record<string, string> = { symbol };
140
+
141
+ if (market === "crypto") {
142
+ queryParams.provider = "ccxt";
143
+ const results = await this.crypto("price/historical", queryParams);
144
+ const last = results[results.length - 1] as Record<string, unknown> | undefined;
145
+ return {
146
+ symbol,
147
+ market: "crypto",
148
+ last: Number(last?.close ?? 0),
149
+ timestamp: last?.date ? new Date(String(last.date)).getTime() : Date.now(),
150
+ };
151
+ }
152
+
153
+ // Equity ticker — fetch latest bar
154
+ queryParams.provider = detectEquityProvider(symbol);
155
+ queryParams.limit = "1";
156
+ const results = await this.equity("price/historical", queryParams);
157
+ const last = results[results.length - 1] as Record<string, unknown> | undefined;
158
+ if (!last) throw new Error(`No ticker data for ${symbol}`);
159
+
160
+ return {
161
+ symbol,
162
+ market: "equity",
163
+ last: Number(last.close ?? 0),
164
+ volume24h: Number(last.volume ?? 0) || undefined,
165
+ timestamp: last.date ? new Date(String(last.date)).getTime() : Date.now(),
166
+ };
167
+ }
168
+
169
+ /* ============================================================
170
+ * Internal helpers
171
+ * ============================================================ */
172
+
173
+ private normalizeOHLCV(results: unknown[], limit?: number): OHLCV[] {
174
+ const rows = (results as Array<Record<string, unknown>>)
175
+ .map((r) => {
176
+ const ts = r.date ?? r.trade_date ?? r.timestamp;
177
+ if (!ts) return null;
178
+ return {
179
+ timestamp: typeof ts === "number" ? ts : new Date(String(ts)).getTime(),
180
+ open: Number(r.open) || 0,
181
+ high: Number(r.high) || 0,
182
+ low: Number(r.low) || 0,
183
+ close: Number(r.close) || 0,
184
+ volume: Number(r.volume ?? r.vol) || 0,
185
+ };
186
+ })
187
+ .filter((r): r is OHLCV => r !== null)
188
+ .sort((a, b) => a.timestamp - b.timestamp);
189
+
190
+ return limit ? rows.slice(-limit) : rows;
191
+ }
192
+ }
193
+
194
+ /** Detect the best DataHub provider for an equity symbol. */
195
+ function detectEquityProvider(symbol: string): string {
196
+ const upper = symbol.toUpperCase();
197
+ if (
198
+ upper.endsWith(".SH") ||
199
+ upper.endsWith(".SZ") ||
200
+ upper.endsWith(".BJ") ||
201
+ upper.endsWith(".HK") ||
202
+ /^\d{6}/.test(upper)
203
+ ) {
204
+ return "tushare";
205
+ }
206
+ return "yfinance";
207
+ }