@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.
- package/DESIGN.md +234 -0
- package/index.ts +608 -0
- package/openclaw.plugin.json +34 -0
- package/package.json +32 -0
- package/skills/crypto-defi/skill.md +69 -0
- package/skills/data-query/skill.md +56 -0
- package/skills/derivatives/skill.md +53 -0
- package/skills/equity/skill.md +64 -0
- package/skills/macro/skill.md +60 -0
- package/skills/market-radar/skill.md +47 -0
- package/src/adapters/crypto-adapter.ts +103 -0
- package/src/adapters/equity-adapter.ts +11 -0
- package/src/adapters/yahoo-adapter.ts +110 -0
- package/src/config.ts +51 -0
- package/src/datahub-client.test.ts +544 -0
- package/src/datahub-client.ts +207 -0
- package/src/integration.live.test.ts +589 -0
- package/src/ohlcv-cache.ts +118 -0
- package/src/regime-detector.ts +73 -0
- package/src/types.ts +31 -0
- package/src/unified-provider.ts +123 -0
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import type { MarketRegime, OHLCV } from "./types.js";
|
|
2
|
+
|
|
3
|
+
/** Simple Moving Average over `period` values. */
|
|
4
|
+
function sma(values: number[], period: number): number[] {
|
|
5
|
+
const result: number[] = [];
|
|
6
|
+
if (values.length < period) return result;
|
|
7
|
+
let sum = 0;
|
|
8
|
+
for (let i = 0; i < period; i++) {
|
|
9
|
+
sum += values[i]!;
|
|
10
|
+
}
|
|
11
|
+
result.push(sum / period);
|
|
12
|
+
for (let i = period; i < values.length; i++) {
|
|
13
|
+
sum += values[i]! - values[i - period]!;
|
|
14
|
+
result.push(sum / period);
|
|
15
|
+
}
|
|
16
|
+
return result;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/** Average True Range over `period` bars. Returns one ATR value per bar after the first `period` bars. */
|
|
20
|
+
function atr(bars: OHLCV[], period: number): number[] {
|
|
21
|
+
if (bars.length < 2) return [];
|
|
22
|
+
|
|
23
|
+
// Calculate True Range for each bar (starting from index 1)
|
|
24
|
+
const trValues: number[] = [];
|
|
25
|
+
for (let i = 1; i < bars.length; i++) {
|
|
26
|
+
const high = bars[i]!.high;
|
|
27
|
+
const low = bars[i]!.low;
|
|
28
|
+
const prevClose = bars[i - 1]!.close;
|
|
29
|
+
const tr = Math.max(high - low, Math.abs(high - prevClose), Math.abs(low - prevClose));
|
|
30
|
+
trValues.push(tr);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
return sma(trValues, period);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export class RegimeDetector {
|
|
37
|
+
detect(ohlcv: OHLCV[]): MarketRegime {
|
|
38
|
+
if (ohlcv.length < 200) return "sideways";
|
|
39
|
+
|
|
40
|
+
const closes = ohlcv.map((b) => b.close);
|
|
41
|
+
const currentClose = closes[closes.length - 1]!;
|
|
42
|
+
|
|
43
|
+
// 1. Drawdown from peak
|
|
44
|
+
let peak = -Infinity;
|
|
45
|
+
for (const c of closes) {
|
|
46
|
+
if (c > peak) peak = c;
|
|
47
|
+
}
|
|
48
|
+
const drawdownPct = ((peak - currentClose) / peak) * 100;
|
|
49
|
+
if (drawdownPct > 30) return "crisis";
|
|
50
|
+
|
|
51
|
+
// 2. ATR% = ATR(14) / close * 100
|
|
52
|
+
const atrValues = atr(ohlcv, 14);
|
|
53
|
+
if (atrValues.length > 0) {
|
|
54
|
+
const latestAtr = atrValues[atrValues.length - 1]!;
|
|
55
|
+
const atrPct = (latestAtr / currentClose) * 100;
|
|
56
|
+
if (atrPct > 4) return "volatile";
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// 3. SMA crossover
|
|
60
|
+
const sma50 = sma(closes, 50);
|
|
61
|
+
const sma200 = sma(closes, 200);
|
|
62
|
+
|
|
63
|
+
if (sma50.length === 0 || sma200.length === 0) return "sideways";
|
|
64
|
+
|
|
65
|
+
const latestSma50 = sma50[sma50.length - 1]!;
|
|
66
|
+
const latestSma200 = sma200[sma200.length - 1]!;
|
|
67
|
+
|
|
68
|
+
if (latestSma50 > latestSma200 && currentClose > latestSma50) return "bull";
|
|
69
|
+
if (latestSma50 < latestSma200 && currentClose < latestSma50) return "bear";
|
|
70
|
+
|
|
71
|
+
return "sideways";
|
|
72
|
+
}
|
|
73
|
+
}
|
package/src/types.ts
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
// Inlined from @openfinclaw/fin-shared-types so this plugin is fully self-contained.
|
|
2
|
+
|
|
3
|
+
export interface OHLCV {
|
|
4
|
+
timestamp: number; // Unix ms
|
|
5
|
+
open: number;
|
|
6
|
+
high: number;
|
|
7
|
+
low: number;
|
|
8
|
+
close: number;
|
|
9
|
+
volume: number;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export type MarketType = "crypto" | "equity" | "commodity";
|
|
13
|
+
|
|
14
|
+
export type MarketRegime = "bull" | "bear" | "sideways" | "volatile" | "crisis";
|
|
15
|
+
|
|
16
|
+
export interface Ticker {
|
|
17
|
+
symbol: string;
|
|
18
|
+
market: MarketType;
|
|
19
|
+
last: number;
|
|
20
|
+
bid?: number;
|
|
21
|
+
ask?: number;
|
|
22
|
+
volume24h?: number;
|
|
23
|
+
changePct24h?: number;
|
|
24
|
+
timestamp: number;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export interface MarketInfo {
|
|
28
|
+
market: MarketType;
|
|
29
|
+
symbols: string[];
|
|
30
|
+
available: boolean;
|
|
31
|
+
}
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
import type { CryptoAdapter } from "./adapters/crypto-adapter.js";
|
|
2
|
+
import type { EquityAdapter } from "./adapters/equity-adapter.js";
|
|
3
|
+
import { DataHubClient } from "./datahub-client.js";
|
|
4
|
+
import type { OHLCVCache } from "./ohlcv-cache.js";
|
|
5
|
+
import type { RegimeDetector } from "./regime-detector.js";
|
|
6
|
+
import type { MarketInfo, MarketRegime, MarketType, OHLCV, Ticker } from "./types.js";
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Unified data provider that routes requests:
|
|
10
|
+
* - Has DataHub API key → DataHub REST (all markets)
|
|
11
|
+
* - No key + crypto → CryptoAdapter (CCXT)
|
|
12
|
+
* - No key + equity → Yahoo Finance adapter
|
|
13
|
+
*/
|
|
14
|
+
export class UnifiedDataProvider {
|
|
15
|
+
constructor(
|
|
16
|
+
private datahubClient: DataHubClient | null,
|
|
17
|
+
private cryptoAdapter: CryptoAdapter,
|
|
18
|
+
private regimeDetector: RegimeDetector,
|
|
19
|
+
private cache: OHLCVCache,
|
|
20
|
+
private yahooAdapter?: EquityAdapter,
|
|
21
|
+
) {}
|
|
22
|
+
|
|
23
|
+
async getOHLCV(params: {
|
|
24
|
+
symbol: string;
|
|
25
|
+
market: MarketType;
|
|
26
|
+
timeframe: string;
|
|
27
|
+
since?: number;
|
|
28
|
+
limit?: number;
|
|
29
|
+
}): Promise<OHLCV[]> {
|
|
30
|
+
// Route 1: DataHub REST (has API key)
|
|
31
|
+
if (this.datahubClient) {
|
|
32
|
+
// Check cache first
|
|
33
|
+
const range = this.cache.getRange(params.symbol, params.market, params.timeframe);
|
|
34
|
+
if (range && params.since != null && params.limit != null) {
|
|
35
|
+
const cached = this.cache.query(
|
|
36
|
+
params.symbol,
|
|
37
|
+
params.market,
|
|
38
|
+
params.timeframe,
|
|
39
|
+
params.since,
|
|
40
|
+
);
|
|
41
|
+
if (cached.length >= params.limit) {
|
|
42
|
+
return cached.slice(0, params.limit);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const rows = await this.datahubClient.getOHLCV(params);
|
|
47
|
+
if (rows.length > 0) {
|
|
48
|
+
this.cache.upsertBatch(params.symbol, params.market, params.timeframe, rows);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// Return from cache for consistency
|
|
52
|
+
if (range || rows.length > 0) {
|
|
53
|
+
const all = this.cache.query(params.symbol, params.market, params.timeframe, params.since);
|
|
54
|
+
return params.limit ? all.slice(0, params.limit) : all;
|
|
55
|
+
}
|
|
56
|
+
return rows;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// Route 2: Free adapters (no API key)
|
|
60
|
+
if (params.market === "crypto") {
|
|
61
|
+
return this.cryptoAdapter.getOHLCV(params);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
if (params.market === "equity") {
|
|
65
|
+
if (!this.yahooAdapter) {
|
|
66
|
+
throw new Error(
|
|
67
|
+
"Equity data unavailable. Set DATAHUB_API_KEY for full access, or install yahoo-finance2 for free US equity data.",
|
|
68
|
+
);
|
|
69
|
+
}
|
|
70
|
+
return this.yahooAdapter.getOHLCV(params);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
throw new Error(
|
|
74
|
+
`Market "${params.market}" not yet supported in free mode. Set DATAHUB_API_KEY for full access.`,
|
|
75
|
+
);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
async getTicker(symbol: string, market: MarketType): Promise<Ticker> {
|
|
79
|
+
// Route 1: DataHub REST
|
|
80
|
+
if (this.datahubClient) {
|
|
81
|
+
return this.datahubClient.getTicker(symbol, market);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// Route 2: Free adapters
|
|
85
|
+
if (market === "crypto") {
|
|
86
|
+
return this.cryptoAdapter.getTicker(symbol);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
if (market === "equity") {
|
|
90
|
+
if (!this.yahooAdapter) {
|
|
91
|
+
throw new Error(
|
|
92
|
+
"Equity ticker unavailable. Set DATAHUB_API_KEY or install yahoo-finance2.",
|
|
93
|
+
);
|
|
94
|
+
}
|
|
95
|
+
return this.yahooAdapter.getTicker(symbol);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
throw new Error(`Market "${market}" not yet supported in free mode.`);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
async detectRegime(params: {
|
|
102
|
+
symbol: string;
|
|
103
|
+
market: MarketType;
|
|
104
|
+
timeframe: string;
|
|
105
|
+
}): Promise<MarketRegime> {
|
|
106
|
+
const ohlcv = await this.getOHLCV({
|
|
107
|
+
symbol: params.symbol,
|
|
108
|
+
market: params.market,
|
|
109
|
+
timeframe: params.timeframe,
|
|
110
|
+
limit: 300,
|
|
111
|
+
});
|
|
112
|
+
return this.regimeDetector.detect(ohlcv);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
getSupportedMarkets(): MarketInfo[] {
|
|
116
|
+
const hasFullAccess = !!this.datahubClient;
|
|
117
|
+
return [
|
|
118
|
+
{ market: "crypto", symbols: [], available: true },
|
|
119
|
+
{ market: "equity", symbols: [], available: hasFullAccess || !!this.yahooAdapter },
|
|
120
|
+
{ market: "commodity", symbols: [], available: hasFullAccess },
|
|
121
|
+
];
|
|
122
|
+
}
|
|
123
|
+
}
|