@blockrun/franklin 3.8.8 → 3.8.10
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/dist/agent/error-classifier.js +1 -0
- package/dist/agent/llm.d.ts +7 -0
- package/dist/agent/llm.js +48 -7
- package/dist/agent/loop.js +66 -3
- package/dist/agent/permissions.js +2 -2
- package/dist/agent/types.d.ts +7 -0
- package/dist/banner.js +15 -0
- package/dist/commands/start.d.ts +4 -0
- package/dist/commands/start.js +72 -2
- package/dist/index.js +11 -3
- package/dist/panel/html.js +111 -21
- package/dist/panel/server.js +15 -4
- package/dist/tools/activate.d.ts +29 -0
- package/dist/tools/activate.js +96 -0
- package/dist/tools/index.js +2 -0
- package/dist/tools/tool-categories.d.ts +22 -0
- package/dist/tools/tool-categories.js +44 -0
- package/dist/tools/trading-execute.d.ts +11 -21
- package/dist/tools/trading-execute.js +43 -130
- package/dist/tools/trading-views.d.ts +64 -0
- package/dist/tools/trading-views.js +115 -0
- package/dist/tools/trading.js +86 -7
- package/dist/tools/webhook.d.ts +18 -0
- package/dist/tools/webhook.js +185 -0
- package/dist/trading/data.d.ts +24 -1
- package/dist/trading/data.js +67 -102
- package/dist/trading/providers/blockrun/client.d.ts +48 -0
- package/dist/trading/providers/blockrun/client.js +253 -0
- package/dist/trading/providers/blockrun/price.d.ts +24 -0
- package/dist/trading/providers/blockrun/price.js +110 -0
- package/dist/trading/providers/coingecko/client.d.ts +20 -0
- package/dist/trading/providers/coingecko/client.js +87 -0
- package/dist/trading/providers/coingecko/markets.d.ts +3 -0
- package/dist/trading/providers/coingecko/markets.js +25 -0
- package/dist/trading/providers/coingecko/ohlcv.d.ts +3 -0
- package/dist/trading/providers/coingecko/ohlcv.js +29 -0
- package/dist/trading/providers/coingecko/price.d.ts +11 -0
- package/dist/trading/providers/coingecko/price.js +41 -0
- package/dist/trading/providers/coingecko/trending.d.ts +3 -0
- package/dist/trading/providers/coingecko/trending.js +22 -0
- package/dist/trading/providers/fetcher.d.ts +43 -0
- package/dist/trading/providers/fetcher.js +45 -0
- package/dist/trading/providers/registry.d.ts +45 -0
- package/dist/trading/providers/registry.js +82 -0
- package/dist/trading/providers/standard-models.d.ts +94 -0
- package/dist/trading/providers/standard-models.js +21 -0
- package/dist/trading/providers/telemetry.d.ts +51 -0
- package/dist/trading/providers/telemetry.js +115 -0
- package/dist/ui/app.js +28 -2
- package/package.json +1 -1
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared CoinGecko HTTP client + short-TTL cache.
|
|
3
|
+
*
|
|
4
|
+
* Carved out of the original `src/trading/data.ts` so every CoinGecko
|
|
5
|
+
* fetcher (price, ohlcv, trending, markets) shares the same rate-limit
|
|
6
|
+
* cooldown, user-agent, timeout, and in-memory cache.
|
|
7
|
+
*/
|
|
8
|
+
import { recordFetch } from '../telemetry.js';
|
|
9
|
+
const BASE = 'https://api.coingecko.com/api/v3';
|
|
10
|
+
const UA = 'franklin/3.8.9 (trading)';
|
|
11
|
+
const TIMEOUT_MS = 10_000;
|
|
12
|
+
// Ticker → CoinGecko slug. Not exhaustive; unknown tickers fall through to
|
|
13
|
+
// lowercase and let CoinGecko either accept the slug or 404.
|
|
14
|
+
export const TICKER_TO_ID = {
|
|
15
|
+
BTC: 'bitcoin', ETH: 'ethereum', SOL: 'solana', BNB: 'binancecoin', XRP: 'ripple',
|
|
16
|
+
ADA: 'cardano', DOGE: 'dogecoin', AVAX: 'avalanche-2', DOT: 'polkadot', MATIC: 'matic-network',
|
|
17
|
+
LINK: 'chainlink', UNI: 'uniswap', ATOM: 'cosmos', LTC: 'litecoin', NEAR: 'near',
|
|
18
|
+
APT: 'aptos', ARB: 'arbitrum', OP: 'optimism', SUI: 'sui', SEI: 'sei-network',
|
|
19
|
+
FIL: 'filecoin', AAVE: 'aave', MKR: 'maker', SNX: 'synthetix-network-token',
|
|
20
|
+
COMP: 'compound-governance-token', INJ: 'injective-protocol', TIA: 'celestia',
|
|
21
|
+
PEPE: 'pepe', WIF: 'dogwifcoin', RENDER: 'render-token',
|
|
22
|
+
};
|
|
23
|
+
export function resolveProviderId(ticker) {
|
|
24
|
+
// Accept both "BTC" and "BTC-USD" — Pyth-style callers may pass the pair
|
|
25
|
+
// form even when the registry routes them to CoinGecko.
|
|
26
|
+
const normalized = ticker.toUpperCase().replace(/-USD$/, '').replace(/USDT?$/, '');
|
|
27
|
+
return TICKER_TO_ID[normalized] ?? TICKER_TO_ID[ticker.toUpperCase()] ?? normalized.toLowerCase();
|
|
28
|
+
}
|
|
29
|
+
const cache = new Map();
|
|
30
|
+
export async function cached(key, ttlMs, fn) {
|
|
31
|
+
const hit = cache.get(key);
|
|
32
|
+
if (hit && hit.expiry > Date.now())
|
|
33
|
+
return hit.data;
|
|
34
|
+
const data = await fn();
|
|
35
|
+
cache.set(key, { data, expiry: Date.now() + ttlMs });
|
|
36
|
+
return data;
|
|
37
|
+
}
|
|
38
|
+
/** For tests: wipe every cached entry. */
|
|
39
|
+
export function clearCache() {
|
|
40
|
+
cache.clear();
|
|
41
|
+
}
|
|
42
|
+
export async function coingeckoGet(path) {
|
|
43
|
+
const ctrl = new AbortController();
|
|
44
|
+
const timer = setTimeout(() => ctrl.abort(), TIMEOUT_MS);
|
|
45
|
+
const endpoint = path.split('?')[0];
|
|
46
|
+
const startedAt = Date.now();
|
|
47
|
+
try {
|
|
48
|
+
const res = await fetch(`${BASE}${path}`, {
|
|
49
|
+
headers: { 'User-Agent': UA },
|
|
50
|
+
signal: ctrl.signal,
|
|
51
|
+
});
|
|
52
|
+
const latencyMs = Date.now() - startedAt;
|
|
53
|
+
if (res.status === 429) {
|
|
54
|
+
recordFetch({ provider: 'coingecko', endpoint, ok: false, latencyMs });
|
|
55
|
+
return { kind: 'rate-limited', message: 'CoinGecko rate-limited this request (HTTP 429). Retry in a minute.' };
|
|
56
|
+
}
|
|
57
|
+
if (res.status === 404) {
|
|
58
|
+
recordFetch({ provider: 'coingecko', endpoint, ok: false, latencyMs });
|
|
59
|
+
return { kind: 'not-found', message: `CoinGecko returned 404 for path ${path}` };
|
|
60
|
+
}
|
|
61
|
+
if (!res.ok) {
|
|
62
|
+
recordFetch({ provider: 'coingecko', endpoint, ok: false, latencyMs });
|
|
63
|
+
return { kind: 'upstream-error', message: `CoinGecko HTTP ${res.status}` };
|
|
64
|
+
}
|
|
65
|
+
recordFetch({ provider: 'coingecko', endpoint, ok: true, latencyMs });
|
|
66
|
+
return await res.json();
|
|
67
|
+
}
|
|
68
|
+
catch (e) {
|
|
69
|
+
const latencyMs = Date.now() - startedAt;
|
|
70
|
+
if (e instanceof DOMException && e.name === 'AbortError') {
|
|
71
|
+
recordFetch({ provider: 'coingecko', endpoint, ok: false, latencyMs });
|
|
72
|
+
return { kind: 'timeout', message: `CoinGecko request timed out after ${TIMEOUT_MS}ms` };
|
|
73
|
+
}
|
|
74
|
+
recordFetch({ provider: 'coingecko', endpoint, ok: false, latencyMs });
|
|
75
|
+
return { kind: 'unknown', message: String(e) };
|
|
76
|
+
}
|
|
77
|
+
finally {
|
|
78
|
+
clearTimeout(timer);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
// TTLs for cache reuse across fetchers.
|
|
82
|
+
export const TTL = {
|
|
83
|
+
price: 5 * 60_000,
|
|
84
|
+
ohlcv: 60 * 60_000,
|
|
85
|
+
trending: 15 * 60_000,
|
|
86
|
+
markets: 15 * 60_000,
|
|
87
|
+
};
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { cached, coingeckoGet, TTL } from './client.js';
|
|
2
|
+
export const coingeckoMarketsFetcher = {
|
|
3
|
+
providerName: 'coingecko',
|
|
4
|
+
transformQuery(input) {
|
|
5
|
+
const limit = Math.max(1, Math.min(100, Math.round(Number(input.limit ?? 20))));
|
|
6
|
+
return { limit };
|
|
7
|
+
},
|
|
8
|
+
async fetchData(query) {
|
|
9
|
+
return cached(`markets:${query.limit}`, TTL.markets, async () => coingeckoGet(`/coins/markets?vs_currency=usd&order=market_cap_desc&per_page=${query.limit}&page=1`));
|
|
10
|
+
},
|
|
11
|
+
transformData(raw, _query) {
|
|
12
|
+
if (!Array.isArray(raw)) {
|
|
13
|
+
return { kind: 'upstream-error', message: 'CoinGecko /coins/markets returned unexpected shape' };
|
|
14
|
+
}
|
|
15
|
+
return raw.map(c => ({
|
|
16
|
+
providerId: c.id,
|
|
17
|
+
ticker: c.symbol.toUpperCase(),
|
|
18
|
+
name: c.name,
|
|
19
|
+
priceUsd: c.current_price,
|
|
20
|
+
change24hPct: c.price_change_percentage_24h,
|
|
21
|
+
marketCapUsd: c.market_cap,
|
|
22
|
+
volume24hUsd: c.total_volume,
|
|
23
|
+
}));
|
|
24
|
+
},
|
|
25
|
+
};
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { cached, coingeckoGet, resolveProviderId, TTL } from './client.js';
|
|
2
|
+
export const coingeckoOHLCVFetcher = {
|
|
3
|
+
providerName: 'coingecko',
|
|
4
|
+
transformQuery(input) {
|
|
5
|
+
const ticker = String(input.ticker ?? '').trim().toUpperCase();
|
|
6
|
+
if (!ticker)
|
|
7
|
+
throw new Error('OHLCVQueryParams.ticker is required');
|
|
8
|
+
const days = Math.max(1, Math.min(365, Math.round(Number(input.days ?? 30))));
|
|
9
|
+
return { ticker, days };
|
|
10
|
+
},
|
|
11
|
+
async fetchData(query) {
|
|
12
|
+
const id = resolveProviderId(query.ticker);
|
|
13
|
+
return cached(`ohlcv:${id}:${query.days}`, TTL.ohlcv, async () => {
|
|
14
|
+
return coingeckoGet(`/coins/${id}/market_chart?vs_currency=usd&days=${query.days}&interval=daily`);
|
|
15
|
+
});
|
|
16
|
+
},
|
|
17
|
+
transformData(raw, query) {
|
|
18
|
+
const payload = raw;
|
|
19
|
+
const prices = payload?.prices;
|
|
20
|
+
if (!Array.isArray(prices) || prices.length === 0) {
|
|
21
|
+
return { kind: 'not-found', message: `No OHLCV data for ${query.ticker} (${query.days}d)` };
|
|
22
|
+
}
|
|
23
|
+
return {
|
|
24
|
+
ticker: query.ticker,
|
|
25
|
+
timestamps: prices.map(p => p[0]),
|
|
26
|
+
closes: prices.map(p => p[1]),
|
|
27
|
+
};
|
|
28
|
+
},
|
|
29
|
+
};
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CoinGecko implementation of Fetcher<PriceQueryParams, PriceData>.
|
|
3
|
+
*
|
|
4
|
+
* Transforms: uppercase ticker → CoinGecko slug lookup.
|
|
5
|
+
* Fetches: /simple/price with 24h change/cap/volume flags.
|
|
6
|
+
* Coerces: the nested `{ [id]: { usd: ..., usd_24h_change: ... } }`
|
|
7
|
+
* response into the standard PriceData shape.
|
|
8
|
+
*/
|
|
9
|
+
import type { Fetcher } from '../fetcher.js';
|
|
10
|
+
import type { PriceData, PriceQueryParams } from '../standard-models.js';
|
|
11
|
+
export declare const coingeckoPriceFetcher: Fetcher<PriceQueryParams, PriceData>;
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CoinGecko implementation of Fetcher<PriceQueryParams, PriceData>.
|
|
3
|
+
*
|
|
4
|
+
* Transforms: uppercase ticker → CoinGecko slug lookup.
|
|
5
|
+
* Fetches: /simple/price with 24h change/cap/volume flags.
|
|
6
|
+
* Coerces: the nested `{ [id]: { usd: ..., usd_24h_change: ... } }`
|
|
7
|
+
* response into the standard PriceData shape.
|
|
8
|
+
*/
|
|
9
|
+
import { cached, coingeckoGet, resolveProviderId, TTL } from './client.js';
|
|
10
|
+
export const coingeckoPriceFetcher = {
|
|
11
|
+
providerName: 'coingecko',
|
|
12
|
+
transformQuery(input) {
|
|
13
|
+
const ticker = String(input.ticker ?? '').trim().toUpperCase();
|
|
14
|
+
if (!ticker) {
|
|
15
|
+
throw new Error('PriceQueryParams.ticker is required');
|
|
16
|
+
}
|
|
17
|
+
return { ticker };
|
|
18
|
+
},
|
|
19
|
+
async fetchData(query) {
|
|
20
|
+
const id = resolveProviderId(query.ticker);
|
|
21
|
+
return cached(`price:${id}`, TTL.price, async () => {
|
|
22
|
+
return coingeckoGet(`/simple/price?ids=${id}` +
|
|
23
|
+
`&vs_currencies=usd&include_24hr_change=true` +
|
|
24
|
+
`&include_market_cap=true&include_24hr_vol=true`);
|
|
25
|
+
});
|
|
26
|
+
},
|
|
27
|
+
transformData(raw, query) {
|
|
28
|
+
const id = resolveProviderId(query.ticker);
|
|
29
|
+
const entry = raw[id];
|
|
30
|
+
if (!entry) {
|
|
31
|
+
return { kind: 'not-found', message: `No CoinGecko data for ${query.ticker}` };
|
|
32
|
+
}
|
|
33
|
+
return {
|
|
34
|
+
ticker: query.ticker,
|
|
35
|
+
priceUsd: entry.usd,
|
|
36
|
+
change24hPct: entry.usd_24h_change,
|
|
37
|
+
volume24hUsd: entry.usd_24h_vol,
|
|
38
|
+
marketCapUsd: entry.usd_market_cap,
|
|
39
|
+
};
|
|
40
|
+
},
|
|
41
|
+
};
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { cached, coingeckoGet, TTL } from './client.js';
|
|
2
|
+
export const coingeckoTrendingFetcher = {
|
|
3
|
+
providerName: 'coingecko',
|
|
4
|
+
transformQuery(_input) {
|
|
5
|
+
return {};
|
|
6
|
+
},
|
|
7
|
+
async fetchData(_query) {
|
|
8
|
+
return cached('trending', TTL.trending, async () => coingeckoGet('/search/trending'));
|
|
9
|
+
},
|
|
10
|
+
transformData(raw, _query) {
|
|
11
|
+
const payload = raw;
|
|
12
|
+
if (!payload || !Array.isArray(payload.coins)) {
|
|
13
|
+
return { kind: 'upstream-error', message: 'CoinGecko /search/trending returned unexpected shape' };
|
|
14
|
+
}
|
|
15
|
+
return payload.coins.map(c => ({
|
|
16
|
+
providerId: c.item.id,
|
|
17
|
+
name: c.item.name,
|
|
18
|
+
symbol: c.item.symbol,
|
|
19
|
+
marketCapRank: c.item.market_cap_rank,
|
|
20
|
+
}));
|
|
21
|
+
},
|
|
22
|
+
};
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Fetcher<Query, Data> — the three-step Transform/Extract/Transform (TET)
|
|
3
|
+
* contract every trading data provider implements.
|
|
4
|
+
*
|
|
5
|
+
* Named after OpenBB's provider abstraction but reduced to the shape that
|
|
6
|
+
* actually pays rent in a single-language TypeScript codebase:
|
|
7
|
+
*
|
|
8
|
+
* 1. `transformQuery(input)` — normalize caller input (ticker casing,
|
|
9
|
+
* default values, clamping) into the provider's expected query.
|
|
10
|
+
* 2. `fetchData(query)` — hit the provider's API, return raw payload or a
|
|
11
|
+
* ProviderError.
|
|
12
|
+
* 3. `transformData(raw, query)` — coerce the raw payload into the
|
|
13
|
+
* standard data type the rest of the codebase consumes.
|
|
14
|
+
*
|
|
15
|
+
* Keeping these three steps separate makes providers testable in isolation:
|
|
16
|
+
* you can feed a canned raw payload into `transformData` without mocking
|
|
17
|
+
* HTTP. It also keeps the provider code free of formatting concerns —
|
|
18
|
+
* rendering is views' job, not fetchers'.
|
|
19
|
+
*/
|
|
20
|
+
import type { ProviderError } from './standard-models.js';
|
|
21
|
+
export interface Fetcher<Query, Data, Raw = unknown> {
|
|
22
|
+
/** Human-readable provider id — "coingecko", "binance", etc. */
|
|
23
|
+
readonly providerName: string;
|
|
24
|
+
/** Lowercase the ticker, clamp limits, fill defaults. */
|
|
25
|
+
transformQuery(input: Partial<Query>): Query;
|
|
26
|
+
/** Hit the upstream API. Must not throw — return ProviderError on failure. */
|
|
27
|
+
fetchData(query: Query): Promise<Raw | ProviderError>;
|
|
28
|
+
/** Coerce raw → standard data shape. Returns ProviderError if the shape
|
|
29
|
+
* doesn't map (e.g., provider returned an empty object for a ticker). */
|
|
30
|
+
transformData(raw: Raw, query: Query): Data | ProviderError;
|
|
31
|
+
/**
|
|
32
|
+
* Convenience: end-to-end run. Default implementation composes the three
|
|
33
|
+
* steps; providers can override when a single-step optimization exists.
|
|
34
|
+
*/
|
|
35
|
+
run?(input: Partial<Query>): Promise<Data | ProviderError>;
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Helper: run a fetcher end-to-end with the default composition. Providers
|
|
39
|
+
* that want caching, retries, or parallel fan-out can skip this and write
|
|
40
|
+
* their own `run`. Kept as a plain function so callers don't depend on
|
|
41
|
+
* object-oriented glue.
|
|
42
|
+
*/
|
|
43
|
+
export declare function runFetcher<Q, D, R>(fetcher: Fetcher<Q, D, R>, input: Partial<Q>): Promise<D | ProviderError>;
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Fetcher<Query, Data> — the three-step Transform/Extract/Transform (TET)
|
|
3
|
+
* contract every trading data provider implements.
|
|
4
|
+
*
|
|
5
|
+
* Named after OpenBB's provider abstraction but reduced to the shape that
|
|
6
|
+
* actually pays rent in a single-language TypeScript codebase:
|
|
7
|
+
*
|
|
8
|
+
* 1. `transformQuery(input)` — normalize caller input (ticker casing,
|
|
9
|
+
* default values, clamping) into the provider's expected query.
|
|
10
|
+
* 2. `fetchData(query)` — hit the provider's API, return raw payload or a
|
|
11
|
+
* ProviderError.
|
|
12
|
+
* 3. `transformData(raw, query)` — coerce the raw payload into the
|
|
13
|
+
* standard data type the rest of the codebase consumes.
|
|
14
|
+
*
|
|
15
|
+
* Keeping these three steps separate makes providers testable in isolation:
|
|
16
|
+
* you can feed a canned raw payload into `transformData` without mocking
|
|
17
|
+
* HTTP. It also keeps the provider code free of formatting concerns —
|
|
18
|
+
* rendering is views' job, not fetchers'.
|
|
19
|
+
*/
|
|
20
|
+
/**
|
|
21
|
+
* Helper: run a fetcher end-to-end with the default composition. Providers
|
|
22
|
+
* that want caching, retries, or parallel fan-out can skip this and write
|
|
23
|
+
* their own `run`. Kept as a plain function so callers don't depend on
|
|
24
|
+
* object-oriented glue.
|
|
25
|
+
*/
|
|
26
|
+
export async function runFetcher(fetcher, input) {
|
|
27
|
+
try {
|
|
28
|
+
if (fetcher.run)
|
|
29
|
+
return fetcher.run(input);
|
|
30
|
+
const query = fetcher.transformQuery(input);
|
|
31
|
+
const raw = await fetcher.fetchData(query);
|
|
32
|
+
if (isProviderErrorLike(raw))
|
|
33
|
+
return raw;
|
|
34
|
+
return fetcher.transformData(raw, query);
|
|
35
|
+
}
|
|
36
|
+
catch (err) {
|
|
37
|
+
return {
|
|
38
|
+
kind: 'unknown',
|
|
39
|
+
message: err instanceof Error ? err.message : String(err),
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
function isProviderErrorLike(v) {
|
|
44
|
+
return typeof v === 'object' && v !== null && 'kind' in v && 'message' in v;
|
|
45
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Provider registry.
|
|
3
|
+
*
|
|
4
|
+
* Single source of truth for which Fetcher implementation a given standard
|
|
5
|
+
* query should route to. One named slot per data kind — plus a keyed map
|
|
6
|
+
* for `price` so we can route per asset class (crypto → CoinGecko free tier,
|
|
7
|
+
* fx/commodity/stock → BlockRun Gateway / Pyth) without the call sites
|
|
8
|
+
* knowing which provider is active.
|
|
9
|
+
*
|
|
10
|
+
* Design note: this is intentionally not a dependency-injection framework.
|
|
11
|
+
* Tests that need to stub a provider should call `setProvider*()` before
|
|
12
|
+
* acting and reset with `resetProviders()` in a teardown. No magic.
|
|
13
|
+
*/
|
|
14
|
+
import type { Fetcher } from './fetcher.js';
|
|
15
|
+
import type { AssetClass, MarketCoinData, MarketOverviewQueryParams, OHLCVData, OHLCVQueryParams, PriceData, PriceQueryParams, TrendingCoinData, TrendingQueryParams } from './standard-models.js';
|
|
16
|
+
export type PriceFetcher = Fetcher<PriceQueryParams, PriceData>;
|
|
17
|
+
export interface TradingProviders {
|
|
18
|
+
/** Per-asset-class price fetcher. `getPriceProvider(assetClass)` reads this. */
|
|
19
|
+
price: Record<AssetClass, PriceFetcher>;
|
|
20
|
+
ohlcv: Fetcher<OHLCVQueryParams, OHLCVData>;
|
|
21
|
+
trending: Fetcher<TrendingQueryParams, TrendingCoinData[]>;
|
|
22
|
+
markets: Fetcher<MarketOverviewQueryParams, MarketCoinData[]>;
|
|
23
|
+
}
|
|
24
|
+
/** Read the active fetcher for a singleton data kind (not price). */
|
|
25
|
+
export declare function getProvider<K extends Exclude<keyof TradingProviders, 'price'>>(kind: K): TradingProviders[K];
|
|
26
|
+
/** Read the active price fetcher for a given asset class. Defaults to crypto. */
|
|
27
|
+
export declare function getPriceProvider(assetClass?: AssetClass): PriceFetcher;
|
|
28
|
+
/** Replace one singleton fetcher. */
|
|
29
|
+
export declare function setProvider<K extends Exclude<keyof TradingProviders, 'price'>>(kind: K, fetcher: TradingProviders[K]): void;
|
|
30
|
+
/** Replace one asset-class price fetcher. */
|
|
31
|
+
export declare function setPriceProvider(assetClass: AssetClass, fetcher: PriceFetcher): void;
|
|
32
|
+
/** Restore the default wiring — primarily for test isolation. */
|
|
33
|
+
export declare function resetProviders(): void;
|
|
34
|
+
/**
|
|
35
|
+
* Describe the active wiring for introspection (Panel Markets page, debug).
|
|
36
|
+
* Returns a per-asset-class listing plus the singleton kinds.
|
|
37
|
+
*/
|
|
38
|
+
export interface ProviderWiringRow {
|
|
39
|
+
kind: string;
|
|
40
|
+
assetClass?: AssetClass;
|
|
41
|
+
provider: string;
|
|
42
|
+
endpoint: string;
|
|
43
|
+
paid: boolean;
|
|
44
|
+
}
|
|
45
|
+
export declare function describeWiring(): ProviderWiringRow[];
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Provider registry.
|
|
3
|
+
*
|
|
4
|
+
* Single source of truth for which Fetcher implementation a given standard
|
|
5
|
+
* query should route to. One named slot per data kind — plus a keyed map
|
|
6
|
+
* for `price` so we can route per asset class (crypto → CoinGecko free tier,
|
|
7
|
+
* fx/commodity/stock → BlockRun Gateway / Pyth) without the call sites
|
|
8
|
+
* knowing which provider is active.
|
|
9
|
+
*
|
|
10
|
+
* Design note: this is intentionally not a dependency-injection framework.
|
|
11
|
+
* Tests that need to stub a provider should call `setProvider*()` before
|
|
12
|
+
* acting and reset with `resetProviders()` in a teardown. No magic.
|
|
13
|
+
*/
|
|
14
|
+
import { coingeckoPriceFetcher } from './coingecko/price.js';
|
|
15
|
+
import { coingeckoOHLCVFetcher } from './coingecko/ohlcv.js';
|
|
16
|
+
import { coingeckoTrendingFetcher } from './coingecko/trending.js';
|
|
17
|
+
import { coingeckoMarketsFetcher } from './coingecko/markets.js';
|
|
18
|
+
import { blockrunPriceFetcher } from './blockrun/price.js';
|
|
19
|
+
const DEFAULT_PROVIDERS = {
|
|
20
|
+
price: {
|
|
21
|
+
crypto: coingeckoPriceFetcher,
|
|
22
|
+
fx: blockrunPriceFetcher,
|
|
23
|
+
commodity: blockrunPriceFetcher,
|
|
24
|
+
stock: blockrunPriceFetcher,
|
|
25
|
+
},
|
|
26
|
+
ohlcv: coingeckoOHLCVFetcher,
|
|
27
|
+
trending: coingeckoTrendingFetcher,
|
|
28
|
+
markets: coingeckoMarketsFetcher,
|
|
29
|
+
};
|
|
30
|
+
let current = {
|
|
31
|
+
...DEFAULT_PROVIDERS,
|
|
32
|
+
price: { ...DEFAULT_PROVIDERS.price },
|
|
33
|
+
};
|
|
34
|
+
/** Read the active fetcher for a singleton data kind (not price). */
|
|
35
|
+
export function getProvider(kind) {
|
|
36
|
+
return current[kind];
|
|
37
|
+
}
|
|
38
|
+
/** Read the active price fetcher for a given asset class. Defaults to crypto. */
|
|
39
|
+
export function getPriceProvider(assetClass = 'crypto') {
|
|
40
|
+
return current.price[assetClass];
|
|
41
|
+
}
|
|
42
|
+
/** Replace one singleton fetcher. */
|
|
43
|
+
export function setProvider(kind, fetcher) {
|
|
44
|
+
current[kind] = fetcher;
|
|
45
|
+
}
|
|
46
|
+
/** Replace one asset-class price fetcher. */
|
|
47
|
+
export function setPriceProvider(assetClass, fetcher) {
|
|
48
|
+
current.price[assetClass] = fetcher;
|
|
49
|
+
}
|
|
50
|
+
/** Restore the default wiring — primarily for test isolation. */
|
|
51
|
+
export function resetProviders() {
|
|
52
|
+
current = {
|
|
53
|
+
...DEFAULT_PROVIDERS,
|
|
54
|
+
price: { ...DEFAULT_PROVIDERS.price },
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
export function describeWiring() {
|
|
58
|
+
const rows = [];
|
|
59
|
+
const priceEndpoints = {
|
|
60
|
+
crypto: { endpoint: 'coingecko /simple/price · blockrun /api/v1/crypto/price', paid: false },
|
|
61
|
+
fx: { endpoint: '/api/v1/fx/price', paid: false },
|
|
62
|
+
commodity: { endpoint: '/api/v1/commodity/price', paid: false },
|
|
63
|
+
stock: { endpoint: '/api/v1/stocks/{market}/price', paid: true },
|
|
64
|
+
};
|
|
65
|
+
for (const ac of ['crypto', 'fx', 'commodity', 'stock']) {
|
|
66
|
+
const f = current.price[ac];
|
|
67
|
+
const meta = priceEndpoints[ac];
|
|
68
|
+
rows.push({
|
|
69
|
+
kind: 'price',
|
|
70
|
+
assetClass: ac,
|
|
71
|
+
provider: f.providerName,
|
|
72
|
+
endpoint: ac === 'crypto' && f.providerName === 'coingecko'
|
|
73
|
+
? 'coingecko /simple/price'
|
|
74
|
+
: meta.endpoint.split(' · ').pop() || meta.endpoint,
|
|
75
|
+
paid: meta.paid,
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
rows.push({ kind: 'ohlcv', provider: current.ohlcv.providerName, endpoint: '/coins/{id}/market_chart', paid: false });
|
|
79
|
+
rows.push({ kind: 'trending', provider: current.trending.providerName, endpoint: '/search/trending', paid: false });
|
|
80
|
+
rows.push({ kind: 'markets', provider: current.markets.providerName, endpoint: '/coins/markets', paid: false });
|
|
81
|
+
return rows;
|
|
82
|
+
}
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Provider-agnostic trading data contracts.
|
|
3
|
+
*
|
|
4
|
+
* Every trading data source (CoinGecko today, Binance/CoinMarketCap/etc.
|
|
5
|
+
* tomorrow) must produce these types. Downstream code — the trading engine,
|
|
6
|
+
* the `trading-market` tool, the `trading-signal` tool — depends on these
|
|
7
|
+
* types, not on provider-specific response shapes. That's the whole point:
|
|
8
|
+
* swapping providers becomes a registry change, not a codebase change.
|
|
9
|
+
*
|
|
10
|
+
* Pattern: standard query params + standard data shape, both serializable,
|
|
11
|
+
* both strictly typed. Providers are free to accept extra fields in their
|
|
12
|
+
* own query shapes (time zone, cache bust, etc.) but their output must
|
|
13
|
+
* coerce to the standard data type before returning.
|
|
14
|
+
*/
|
|
15
|
+
/**
|
|
16
|
+
* Asset classes Franklin recognizes. Crypto is today; the rest unlock the
|
|
17
|
+
* moment a provider with non-crypto coverage (BlockRun Gateway / Pyth) is
|
|
18
|
+
* wired into the registry. Fetchers inspect this to pick the correct
|
|
19
|
+
* upstream endpoint.
|
|
20
|
+
*/
|
|
21
|
+
export type AssetClass = 'crypto' | 'fx' | 'commodity' | 'stock';
|
|
22
|
+
/**
|
|
23
|
+
* Stock exchange market codes BlockRun Gateway understands. Only meaningful
|
|
24
|
+
* when `assetClass === 'stock'`; ignored otherwise.
|
|
25
|
+
*/
|
|
26
|
+
export type MarketCode = 'us' | 'hk' | 'jp' | 'kr' | 'gb' | 'de' | 'fr' | 'nl' | 'ie' | 'lu' | 'cn' | 'ca';
|
|
27
|
+
/** Common to every query: a ticker the provider will resolve itself. */
|
|
28
|
+
export interface PriceQueryParams {
|
|
29
|
+
ticker: string;
|
|
30
|
+
/** Defaults to 'crypto' so existing callers keep working without churn. */
|
|
31
|
+
assetClass?: AssetClass;
|
|
32
|
+
/** Required when assetClass === 'stock'. Ignored otherwise. */
|
|
33
|
+
market?: MarketCode;
|
|
34
|
+
}
|
|
35
|
+
export interface PriceData {
|
|
36
|
+
ticker: string;
|
|
37
|
+
priceUsd: number;
|
|
38
|
+
change24hPct: number;
|
|
39
|
+
volume24hUsd: number;
|
|
40
|
+
marketCapUsd: number;
|
|
41
|
+
}
|
|
42
|
+
export interface OHLCVQueryParams {
|
|
43
|
+
ticker: string;
|
|
44
|
+
/** Number of calendar days of history, typically 1-365. */
|
|
45
|
+
days: number;
|
|
46
|
+
}
|
|
47
|
+
export interface OHLCVData {
|
|
48
|
+
ticker: string;
|
|
49
|
+
/** Epoch-ms timestamps aligned with closes. */
|
|
50
|
+
timestamps: number[];
|
|
51
|
+
/** Daily closing prices in USD. */
|
|
52
|
+
closes: number[];
|
|
53
|
+
}
|
|
54
|
+
export interface TrendingQueryParams {
|
|
55
|
+
_?: never;
|
|
56
|
+
}
|
|
57
|
+
export interface TrendingCoinData {
|
|
58
|
+
providerId: string;
|
|
59
|
+
symbol: string;
|
|
60
|
+
name: string;
|
|
61
|
+
marketCapRank: number | null;
|
|
62
|
+
}
|
|
63
|
+
export interface MarketOverviewQueryParams {
|
|
64
|
+
/** Top N by market cap. Providers may cap this internally. */
|
|
65
|
+
limit: number;
|
|
66
|
+
}
|
|
67
|
+
export interface MarketCoinData {
|
|
68
|
+
providerId: string;
|
|
69
|
+
ticker: string;
|
|
70
|
+
name: string;
|
|
71
|
+
priceUsd: number;
|
|
72
|
+
change24hPct: number;
|
|
73
|
+
marketCapUsd: number;
|
|
74
|
+
volume24hUsd: number;
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* Error convention: providers return a `ProviderError` (plain object) rather
|
|
78
|
+
* than throwing, so the caller can render it inline without try/catch at
|
|
79
|
+
* every tool handler. This mirrors the legacy `string | PriceData` return
|
|
80
|
+
* type but is structured — the `kind` field lets the UI color-code without
|
|
81
|
+
* string parsing.
|
|
82
|
+
*/
|
|
83
|
+
export interface ProviderError {
|
|
84
|
+
kind: 'rate-limited' | 'timeout' | 'not-found' | 'upstream-error' | 'unknown';
|
|
85
|
+
message: string;
|
|
86
|
+
/**
|
|
87
|
+
* Optional provider- or endpoint-specific subcode. Populated when the
|
|
88
|
+
* caller can take a different action than a generic error allows
|
|
89
|
+
* (e.g. "fund your wallet" vs "retry later"). Consumers should render
|
|
90
|
+
* `message` when `code` is absent.
|
|
91
|
+
*/
|
|
92
|
+
code?: 'insufficient-funds' | 'budget-exceeded' | 'schema-mismatch' | 'unsupported-asset-class' | 'missing-market-code';
|
|
93
|
+
}
|
|
94
|
+
export declare function isProviderError(v: unknown): v is ProviderError;
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Provider-agnostic trading data contracts.
|
|
3
|
+
*
|
|
4
|
+
* Every trading data source (CoinGecko today, Binance/CoinMarketCap/etc.
|
|
5
|
+
* tomorrow) must produce these types. Downstream code — the trading engine,
|
|
6
|
+
* the `trading-market` tool, the `trading-signal` tool — depends on these
|
|
7
|
+
* types, not on provider-specific response shapes. That's the whole point:
|
|
8
|
+
* swapping providers becomes a registry change, not a codebase change.
|
|
9
|
+
*
|
|
10
|
+
* Pattern: standard query params + standard data shape, both serializable,
|
|
11
|
+
* both strictly typed. Providers are free to accept extra fields in their
|
|
12
|
+
* own query shapes (time zone, cache bust, etc.) but their output must
|
|
13
|
+
* coerce to the standard data type before returning.
|
|
14
|
+
*/
|
|
15
|
+
export function isProviderError(v) {
|
|
16
|
+
return (typeof v === 'object' &&
|
|
17
|
+
v !== null &&
|
|
18
|
+
'kind' in v &&
|
|
19
|
+
'message' in v &&
|
|
20
|
+
typeof v.message === 'string');
|
|
21
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Provider-layer telemetry.
|
|
3
|
+
*
|
|
4
|
+
* Records every fetcher call so the Panel Markets page can show live health
|
|
5
|
+
* ("• CoinGecko OK", "• BlockRun OK"), today's call count, today's spend,
|
|
6
|
+
* and a p50 latency estimate. Entirely in-memory — dies with the process
|
|
7
|
+
* and re-hydrates from the on-disk wallet/stats files if we ever care to
|
|
8
|
+
* persist (we don't yet).
|
|
9
|
+
*
|
|
10
|
+
* Tiny by design: zero deps, no background timers, no DB. Callers push a
|
|
11
|
+
* single record per fetch; the Panel pulls a snapshot on demand.
|
|
12
|
+
*/
|
|
13
|
+
type ProviderName = 'coingecko' | 'blockrun';
|
|
14
|
+
interface FetchRecord {
|
|
15
|
+
provider: ProviderName;
|
|
16
|
+
endpoint: string;
|
|
17
|
+
ok: boolean;
|
|
18
|
+
latencyMs: number;
|
|
19
|
+
costUsd?: number;
|
|
20
|
+
ts: number;
|
|
21
|
+
}
|
|
22
|
+
interface PaidCallRow {
|
|
23
|
+
endpoint: string;
|
|
24
|
+
costUsd: number;
|
|
25
|
+
ts: number;
|
|
26
|
+
}
|
|
27
|
+
export declare function recordFetch(evt: Omit<FetchRecord, 'ts'>): void;
|
|
28
|
+
export interface ProviderSnapshot {
|
|
29
|
+
name: ProviderName;
|
|
30
|
+
calls: number;
|
|
31
|
+
ok: number;
|
|
32
|
+
failures: number;
|
|
33
|
+
p50LatencyMs: number | null;
|
|
34
|
+
lastOkAt: number | null;
|
|
35
|
+
lastErrorAt: number | null;
|
|
36
|
+
spendUsdToday: number;
|
|
37
|
+
status: 'ok' | 'degraded' | 'cold';
|
|
38
|
+
}
|
|
39
|
+
export interface TelemetrySnapshot {
|
|
40
|
+
providers: ProviderSnapshot[];
|
|
41
|
+
totals: {
|
|
42
|
+
callsToday: number;
|
|
43
|
+
spendUsdToday: number;
|
|
44
|
+
p50LatencyMs: number | null;
|
|
45
|
+
};
|
|
46
|
+
recentPaidCalls: PaidCallRow[];
|
|
47
|
+
}
|
|
48
|
+
export declare function snapshot(): TelemetrySnapshot;
|
|
49
|
+
/** Test helper: reset all counters. Do not call in production code paths. */
|
|
50
|
+
export declare function resetTelemetry(): void;
|
|
51
|
+
export {};
|