@mmflow/sdk 0.1.0
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/index.cjs +334 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +164 -0
- package/dist/index.d.ts +164 -0
- package/dist/index.js +331 -0
- package/dist/index.js.map +1 -0
- package/package.json +30 -0
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,334 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// src/socket.ts
|
|
4
|
+
var DEFAULT_URL = "wss://mmflowai-production.up.railway.app/v1";
|
|
5
|
+
var keyOf = (channel, symbol) => `${channel}:${symbol}`;
|
|
6
|
+
var MmflowSocket = class {
|
|
7
|
+
constructor(options = {}) {
|
|
8
|
+
this.ws = null;
|
|
9
|
+
this.subs = /* @__PURE__ */ new Map();
|
|
10
|
+
this.reconnectTimer = null;
|
|
11
|
+
this.closedByUser = false;
|
|
12
|
+
this.authed = false;
|
|
13
|
+
this.nextId = 1;
|
|
14
|
+
this.url = options.url ?? DEFAULT_URL;
|
|
15
|
+
this.apiKey = options.apiKey;
|
|
16
|
+
this.autoReconnect = options.autoReconnect ?? true;
|
|
17
|
+
this.reconnectInitialMs = options.reconnectInitialMs ?? 500;
|
|
18
|
+
this.reconnectMaxMs = options.reconnectMaxMs ?? 3e4;
|
|
19
|
+
this.onError = options.onError;
|
|
20
|
+
this.backoff = this.reconnectInitialMs;
|
|
21
|
+
}
|
|
22
|
+
/** Open the socket. Idempotent; subscriptions made before connect() are
|
|
23
|
+
* sent as soon as the connection is established. */
|
|
24
|
+
connect() {
|
|
25
|
+
this.closedByUser = false;
|
|
26
|
+
this.open();
|
|
27
|
+
}
|
|
28
|
+
open() {
|
|
29
|
+
if (typeof WebSocket === "undefined") {
|
|
30
|
+
throw new Error(
|
|
31
|
+
"MmflowSocket requires a global WebSocket (browser, or Node >= 21 / the `ws` package)."
|
|
32
|
+
);
|
|
33
|
+
}
|
|
34
|
+
if (this.ws && (this.ws.readyState === WebSocket.OPEN || this.ws.readyState === WebSocket.CONNECTING)) {
|
|
35
|
+
return;
|
|
36
|
+
}
|
|
37
|
+
const ws = new WebSocket(this.url);
|
|
38
|
+
this.ws = ws;
|
|
39
|
+
this.authed = false;
|
|
40
|
+
ws.onopen = () => {
|
|
41
|
+
this.backoff = this.reconnectInitialMs;
|
|
42
|
+
if (this.apiKey) this.send({ op: "auth", apiKey: this.apiKey });
|
|
43
|
+
else this.replaySubscriptions();
|
|
44
|
+
};
|
|
45
|
+
ws.onmessage = (ev) => this.handleMessage(ev);
|
|
46
|
+
ws.onclose = () => {
|
|
47
|
+
this.ws = null;
|
|
48
|
+
this.authed = false;
|
|
49
|
+
if (!this.closedByUser && this.autoReconnect) this.scheduleReconnect();
|
|
50
|
+
};
|
|
51
|
+
ws.onerror = () => {
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
scheduleReconnect() {
|
|
55
|
+
if (this.reconnectTimer != null) return;
|
|
56
|
+
const delay = this.backoff;
|
|
57
|
+
this.reconnectTimer = setTimeout(() => {
|
|
58
|
+
this.reconnectTimer = null;
|
|
59
|
+
this.open();
|
|
60
|
+
}, delay);
|
|
61
|
+
this.backoff = Math.min(this.backoff * 2, this.reconnectMaxMs);
|
|
62
|
+
}
|
|
63
|
+
handleMessage(ev) {
|
|
64
|
+
if (typeof ev.data !== "string") return;
|
|
65
|
+
let msg;
|
|
66
|
+
try {
|
|
67
|
+
msg = JSON.parse(ev.data);
|
|
68
|
+
} catch {
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
if (msg.type === "ack" && msg.op === "auth") {
|
|
72
|
+
this.authed = true;
|
|
73
|
+
this.replaySubscriptions();
|
|
74
|
+
} else if (msg.type === "data" && msg.channel && msg.symbol) {
|
|
75
|
+
const sub = this.subs.get(keyOf(msg.channel, msg.symbol));
|
|
76
|
+
if (!sub) return;
|
|
77
|
+
const meta = {
|
|
78
|
+
channel: msg.channel,
|
|
79
|
+
symbol: msg.symbol,
|
|
80
|
+
seq: msg.seq,
|
|
81
|
+
ts: msg.ts
|
|
82
|
+
};
|
|
83
|
+
sub.handlers.forEach((handler) => {
|
|
84
|
+
try {
|
|
85
|
+
handler(msg.payload, meta);
|
|
86
|
+
} catch {
|
|
87
|
+
}
|
|
88
|
+
});
|
|
89
|
+
} else if (msg.type === "error") {
|
|
90
|
+
this.onError?.(msg.code ?? "error", msg.message ?? "");
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
send(obj) {
|
|
94
|
+
if (this.ws && this.ws.readyState === WebSocket.OPEN) {
|
|
95
|
+
this.ws.send(JSON.stringify(obj));
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
sendSubscribe(sub) {
|
|
99
|
+
if (this.apiKey && !this.authed) return;
|
|
100
|
+
this.send({
|
|
101
|
+
op: "subscribe",
|
|
102
|
+
channel: sub.channel,
|
|
103
|
+
symbol: sub.symbol,
|
|
104
|
+
id: this.nextId++
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
replaySubscriptions() {
|
|
108
|
+
this.subs.forEach((sub) => this.sendSubscribe(sub));
|
|
109
|
+
}
|
|
110
|
+
/**
|
|
111
|
+
* Subscribe to `channel` for `symbol`. Returns an unsubscribe function.
|
|
112
|
+
* Safe to call before connect() — it is (re)sent on each connection.
|
|
113
|
+
* Multiple handlers for the same (channel, symbol) share one subscription.
|
|
114
|
+
*/
|
|
115
|
+
subscribe(channel, symbol, handler) {
|
|
116
|
+
const key = keyOf(channel, symbol);
|
|
117
|
+
let sub = this.subs.get(key);
|
|
118
|
+
if (!sub) {
|
|
119
|
+
sub = { channel, symbol, handlers: /* @__PURE__ */ new Set() };
|
|
120
|
+
this.subs.set(key, sub);
|
|
121
|
+
this.sendSubscribe(sub);
|
|
122
|
+
}
|
|
123
|
+
sub.handlers.add(handler);
|
|
124
|
+
return () => {
|
|
125
|
+
const existing = this.subs.get(key);
|
|
126
|
+
if (!existing) return;
|
|
127
|
+
existing.handlers.delete(handler);
|
|
128
|
+
if (existing.handlers.size === 0) {
|
|
129
|
+
this.subs.delete(key);
|
|
130
|
+
this.send({ op: "unsubscribe", channel, symbol, id: this.nextId++ });
|
|
131
|
+
}
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
/** Close permanently (disables auto-reconnect) and drop all subscriptions. */
|
|
135
|
+
close() {
|
|
136
|
+
this.closedByUser = true;
|
|
137
|
+
if (this.reconnectTimer != null) {
|
|
138
|
+
clearTimeout(this.reconnectTimer);
|
|
139
|
+
this.reconnectTimer = null;
|
|
140
|
+
}
|
|
141
|
+
this.subs.clear();
|
|
142
|
+
this.authed = false;
|
|
143
|
+
this.ws?.close();
|
|
144
|
+
this.ws = null;
|
|
145
|
+
}
|
|
146
|
+
};
|
|
147
|
+
|
|
148
|
+
// src/feed.ts
|
|
149
|
+
var DEFAULT_BASE = "https://mmflow.ai";
|
|
150
|
+
var DEFAULT_WS = "wss://mmflowai-production.up.railway.app/v1";
|
|
151
|
+
function createMmflowFeed(opts = {}) {
|
|
152
|
+
const base = (opts.baseUrl ?? DEFAULT_BASE).replace(/\/$/, "");
|
|
153
|
+
const transport = opts.transport ?? "sse";
|
|
154
|
+
let socket = null;
|
|
155
|
+
let socketRefs = 0;
|
|
156
|
+
const ensureSocket = () => {
|
|
157
|
+
if (transport !== "ws") {
|
|
158
|
+
throw new Error('createMmflowFeed: this method requires transport="ws"');
|
|
159
|
+
}
|
|
160
|
+
if (!opts.apiKey) {
|
|
161
|
+
throw new Error('createMmflowFeed: transport="ws" requires apiKey');
|
|
162
|
+
}
|
|
163
|
+
if (!socket) {
|
|
164
|
+
socket = new MmflowSocket({
|
|
165
|
+
apiKey: opts.apiKey,
|
|
166
|
+
url: opts.wsUrl ?? DEFAULT_WS
|
|
167
|
+
});
|
|
168
|
+
socket.connect();
|
|
169
|
+
}
|
|
170
|
+
return socket;
|
|
171
|
+
};
|
|
172
|
+
const attachSocket = (channel, symbol, handler) => {
|
|
173
|
+
const sock = ensureSocket();
|
|
174
|
+
socketRefs++;
|
|
175
|
+
const unsub = sock.subscribe(channel, symbol, handler);
|
|
176
|
+
return () => {
|
|
177
|
+
unsub();
|
|
178
|
+
socketRefs = Math.max(0, socketRefs - 1);
|
|
179
|
+
if (socketRefs === 0) {
|
|
180
|
+
socket?.close();
|
|
181
|
+
socket = null;
|
|
182
|
+
}
|
|
183
|
+
};
|
|
184
|
+
};
|
|
185
|
+
const feed = {
|
|
186
|
+
async fetchCandles({ symbol, resolution, from, to }) {
|
|
187
|
+
const hours = Math.min(720, Math.max(1, Math.ceil((to - from) / 36e5)));
|
|
188
|
+
const url = new URL(`${base}/api/hl/candles`);
|
|
189
|
+
url.searchParams.set("coin", symbol || opts.symbol || "");
|
|
190
|
+
url.searchParams.set("interval", resolution);
|
|
191
|
+
url.searchParams.set("hours", String(hours));
|
|
192
|
+
const res = await fetch(url.toString());
|
|
193
|
+
if (!res.ok) {
|
|
194
|
+
throw new Error(`mmflow: candles request failed (${res.status})`);
|
|
195
|
+
}
|
|
196
|
+
const json = await res.json();
|
|
197
|
+
const rows = json.candles ?? [];
|
|
198
|
+
const out = [];
|
|
199
|
+
for (const r of rows) {
|
|
200
|
+
const time = Number(r.t);
|
|
201
|
+
if (!Number.isFinite(time)) continue;
|
|
202
|
+
out.push({
|
|
203
|
+
time,
|
|
204
|
+
open: Number(r.o),
|
|
205
|
+
high: Number(r.h),
|
|
206
|
+
low: Number(r.l),
|
|
207
|
+
close: Number(r.c),
|
|
208
|
+
volume: Number(r.v)
|
|
209
|
+
});
|
|
210
|
+
}
|
|
211
|
+
return out;
|
|
212
|
+
},
|
|
213
|
+
subscribe(symbol, _resolution, onUpdate) {
|
|
214
|
+
const feedSymbol = symbol || opts.symbol || "";
|
|
215
|
+
if (transport === "ws") {
|
|
216
|
+
return attachSocket("trades", feedSymbol, (payload) => {
|
|
217
|
+
const t = payload;
|
|
218
|
+
const price = Number(t.price);
|
|
219
|
+
const size = Number(t.size);
|
|
220
|
+
const time = Number(t.ts ?? t.time);
|
|
221
|
+
if (!Number.isFinite(price) || !Number.isFinite(size) || !Number.isFinite(time)) {
|
|
222
|
+
return;
|
|
223
|
+
}
|
|
224
|
+
onUpdate({
|
|
225
|
+
symbol: t.coin ?? t.symbol ?? feedSymbol,
|
|
226
|
+
price,
|
|
227
|
+
size,
|
|
228
|
+
time,
|
|
229
|
+
side: t.side
|
|
230
|
+
});
|
|
231
|
+
});
|
|
232
|
+
}
|
|
233
|
+
if (typeof EventSource === "undefined") {
|
|
234
|
+
throw new Error(
|
|
235
|
+
"createMmflowFeed.subscribe needs a global EventSource (browser). In Node, install an EventSource polyfill or use MmflowSocket instead."
|
|
236
|
+
);
|
|
237
|
+
}
|
|
238
|
+
const url = new URL(`${base}/api/v1/streams/trades`);
|
|
239
|
+
url.searchParams.set("coin", feedSymbol);
|
|
240
|
+
if (opts.minUsd != null) url.searchParams.set("minUsd", String(opts.minUsd));
|
|
241
|
+
if (opts.apiKey) url.searchParams.set("api_key", opts.apiKey);
|
|
242
|
+
const es = new EventSource(url.toString());
|
|
243
|
+
const onTrade = (e) => {
|
|
244
|
+
try {
|
|
245
|
+
const t = JSON.parse(e.data);
|
|
246
|
+
onUpdate({
|
|
247
|
+
symbol: t.coin ?? symbol,
|
|
248
|
+
price: t.price,
|
|
249
|
+
size: t.size,
|
|
250
|
+
time: t.ts,
|
|
251
|
+
side: t.side
|
|
252
|
+
});
|
|
253
|
+
} catch {
|
|
254
|
+
}
|
|
255
|
+
};
|
|
256
|
+
es.addEventListener("trade", onTrade);
|
|
257
|
+
return () => {
|
|
258
|
+
es.removeEventListener("trade", onTrade);
|
|
259
|
+
es.close();
|
|
260
|
+
};
|
|
261
|
+
}
|
|
262
|
+
};
|
|
263
|
+
feed.fetchFootprint = async (symbol, fpOpts) => {
|
|
264
|
+
const url = new URL(`${base}/api/v1/perps/footprint`);
|
|
265
|
+
url.searchParams.set("coin", symbol || opts.symbol || "");
|
|
266
|
+
url.searchParams.set("kind", "time");
|
|
267
|
+
url.searchParams.set("interval", String(fpOpts.intervalMs));
|
|
268
|
+
url.searchParams.set("bars", "500");
|
|
269
|
+
const res = await fetch(url.toString());
|
|
270
|
+
if (!res.ok) throw new Error(`mmflow: footprint request failed (${res.status})`);
|
|
271
|
+
const json = await res.json();
|
|
272
|
+
return (json.data ?? []).map((bar) => ({
|
|
273
|
+
t: Number(bar.t ?? bar.time),
|
|
274
|
+
open: Number(bar.open),
|
|
275
|
+
high: Number(bar.high),
|
|
276
|
+
low: Number(bar.low),
|
|
277
|
+
close: Number(bar.close),
|
|
278
|
+
levels: (bar.levels ?? []).map((level) => ({
|
|
279
|
+
price: Number(level.price),
|
|
280
|
+
buyVol: Number(level.buyVol),
|
|
281
|
+
sellVol: Number(level.sellVol)
|
|
282
|
+
}))
|
|
283
|
+
}));
|
|
284
|
+
};
|
|
285
|
+
feed.fetchVolumeProfile = async (symbol) => {
|
|
286
|
+
const url = new URL(`${base}/api/hl/marketprofile`);
|
|
287
|
+
url.searchParams.set("coin", symbol || opts.symbol || "");
|
|
288
|
+
url.searchParams.set("hours", "24");
|
|
289
|
+
url.searchParams.set("bins", "80");
|
|
290
|
+
const res = await fetch(url.toString());
|
|
291
|
+
if (!res.ok) throw new Error(`mmflow: volume profile request failed (${res.status})`);
|
|
292
|
+
const json = await res.json();
|
|
293
|
+
return {
|
|
294
|
+
bins: (json.bins ?? []).map((bin) => ({
|
|
295
|
+
px: Number(bin.px),
|
|
296
|
+
totalUsd: Number(bin.totalUsd)
|
|
297
|
+
})),
|
|
298
|
+
binWidth: Number(json.range?.binWidth ?? 0),
|
|
299
|
+
val: Number(json.val ?? 0),
|
|
300
|
+
vah: Number(json.vah ?? 0)
|
|
301
|
+
};
|
|
302
|
+
};
|
|
303
|
+
if (transport === "ws") {
|
|
304
|
+
feed.subscribeOrderBook = (symbol, onBook) => attachSocket("orderbook", symbol || opts.symbol || "", (payload) => {
|
|
305
|
+
const book = payload;
|
|
306
|
+
const map = (rows) => rows.map((level) => ({
|
|
307
|
+
px: Number(level.px ?? level.price),
|
|
308
|
+
sz: Number(level.sz ?? level.size)
|
|
309
|
+
}));
|
|
310
|
+
onBook({
|
|
311
|
+
time: Number(book.ts ?? Date.now()),
|
|
312
|
+
bids: map(book.bids ?? []),
|
|
313
|
+
asks: map(book.asks ?? [])
|
|
314
|
+
});
|
|
315
|
+
});
|
|
316
|
+
feed.subscribeMarkers = (symbol, onMarkers) => attachSocket("liquidations", symbol || opts.symbol || "", (payload) => {
|
|
317
|
+
const e = payload;
|
|
318
|
+
const marker = {
|
|
319
|
+
t: Number(e.ts ?? Date.now()),
|
|
320
|
+
price: Number(e.price),
|
|
321
|
+
sizeUsd: Number(e.notionalUsd ?? 0)
|
|
322
|
+
};
|
|
323
|
+
if (!Number.isFinite(marker.price) || marker.sizeUsd <= 0) return;
|
|
324
|
+
if (e.side === "long") onMarkers({ buys: [marker], sells: [] });
|
|
325
|
+
else onMarkers({ buys: [], sells: [marker] });
|
|
326
|
+
});
|
|
327
|
+
}
|
|
328
|
+
return feed;
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
exports.MmflowSocket = MmflowSocket;
|
|
332
|
+
exports.createMmflowFeed = createMmflowFeed;
|
|
333
|
+
//# sourceMappingURL=index.cjs.map
|
|
334
|
+
//# sourceMappingURL=index.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/socket.ts","../src/feed.ts"],"names":[],"mappings":"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","file":"index.cjs","sourcesContent":["// MmflowSocket — typed WebSocket client for the mmflow real-time data API.\n//\n// Protocol (JSON frames):\n// client → server : { op: \"auth\", apiKey }\n// { op: \"subscribe\", channel, symbol, id }\n// { op: \"unsubscribe\", channel, symbol, id }\n// server → client : { type: \"ack\", id, channel, symbol }\n// { type: \"data\", channel, symbol, seq, ts, payload }\n// { type: \"heartbeat\", ts }\n// { type: \"error\", code, message, id? }\n//\n// Resilience: exponential-backoff auto-reconnect, with re-auth + replay of all\n// active subscriptions on every (re)connection — the same discipline the app's\n// in-process HL client uses.\n\nimport type { Unsubscribe } from \"./types\";\n\nexport type MmflowChannel =\n | \"trades\"\n | \"orderbook\"\n | \"candles\"\n | \"liquidations\"\n | \"funding\"\n | \"openInterest\"\n | \"markPrice\"\n | \"footprint\";\n\nexport interface MmflowSocketOptions {\n /** Gateway URL. Default is the live Railway gateway. */\n url?: string;\n /** API key sent in the opening `auth` frame. */\n apiKey?: string;\n /** Reconnect automatically on drop. Default true. */\n autoReconnect?: boolean;\n /** Initial reconnect backoff (ms). Default 500. */\n reconnectInitialMs?: number;\n /** Max reconnect backoff (ms). Default 30000. */\n reconnectMaxMs?: number;\n /** Called on a server `error` frame. */\n onError?: (code: string, message: string) => void;\n}\n\nexport interface MmflowMessageMeta {\n channel: MmflowChannel;\n symbol: string;\n seq?: number;\n ts?: number;\n}\n\nexport type MmflowDataHandler = (\n payload: unknown,\n meta: MmflowMessageMeta,\n) => void;\n\ninterface IncomingMessage {\n type?: \"ack\" | \"data\" | \"heartbeat\" | \"pong\" | \"error\";\n op?: string;\n channel?: MmflowChannel;\n symbol?: string;\n seq?: number;\n ts?: number;\n payload?: unknown;\n code?: string;\n message?: string;\n}\n\ninterface Subscription {\n channel: MmflowChannel;\n symbol: string;\n handlers: Set<MmflowDataHandler>;\n}\n\nconst DEFAULT_URL = \"wss://mmflowai-production.up.railway.app/v1\";\nconst keyOf = (channel: string, symbol: string) => `${channel}:${symbol}`;\n\nexport class MmflowSocket {\n private readonly url: string;\n private readonly apiKey?: string;\n private readonly autoReconnect: boolean;\n private readonly reconnectInitialMs: number;\n private readonly reconnectMaxMs: number;\n private readonly onError?: (code: string, message: string) => void;\n\n private ws: WebSocket | null = null;\n private readonly subs = new Map<string, Subscription>();\n private backoff: number;\n private reconnectTimer: ReturnType<typeof setTimeout> | null = null;\n private closedByUser = false;\n private authed = false;\n private nextId = 1;\n\n constructor(options: MmflowSocketOptions = {}) {\n this.url = options.url ?? DEFAULT_URL;\n this.apiKey = options.apiKey;\n this.autoReconnect = options.autoReconnect ?? true;\n this.reconnectInitialMs = options.reconnectInitialMs ?? 500;\n this.reconnectMaxMs = options.reconnectMaxMs ?? 30_000;\n this.onError = options.onError;\n this.backoff = this.reconnectInitialMs;\n }\n\n /** Open the socket. Idempotent; subscriptions made before connect() are\n * sent as soon as the connection is established. */\n connect(): void {\n this.closedByUser = false;\n this.open();\n }\n\n private open(): void {\n if (typeof WebSocket === \"undefined\") {\n throw new Error(\n \"MmflowSocket requires a global WebSocket (browser, or Node >= 21 / the `ws` package).\",\n );\n }\n if (\n this.ws &&\n (this.ws.readyState === WebSocket.OPEN ||\n this.ws.readyState === WebSocket.CONNECTING)\n ) {\n return;\n }\n\n const ws = new WebSocket(this.url);\n this.ws = ws;\n this.authed = false;\n\n ws.onopen = () => {\n this.backoff = this.reconnectInitialMs;\n if (this.apiKey) this.send({ op: \"auth\", apiKey: this.apiKey });\n else this.replaySubscriptions();\n };\n ws.onmessage = (ev: MessageEvent) => this.handleMessage(ev);\n ws.onclose = () => {\n this.ws = null;\n this.authed = false;\n if (!this.closedByUser && this.autoReconnect) this.scheduleReconnect();\n };\n ws.onerror = () => {\n // A close event follows; reconnection is handled there.\n };\n }\n\n private scheduleReconnect(): void {\n if (this.reconnectTimer != null) return;\n const delay = this.backoff;\n this.reconnectTimer = setTimeout(() => {\n this.reconnectTimer = null;\n this.open();\n }, delay);\n this.backoff = Math.min(this.backoff * 2, this.reconnectMaxMs);\n }\n\n private handleMessage(ev: MessageEvent): void {\n if (typeof ev.data !== \"string\") return;\n let msg: IncomingMessage;\n try {\n msg = JSON.parse(ev.data) as IncomingMessage;\n } catch {\n return;\n }\n if (msg.type === \"ack\" && msg.op === \"auth\") {\n this.authed = true;\n this.replaySubscriptions();\n } else if (msg.type === \"data\" && msg.channel && msg.symbol) {\n const sub = this.subs.get(keyOf(msg.channel, msg.symbol));\n if (!sub) return;\n const meta: MmflowMessageMeta = {\n channel: msg.channel,\n symbol: msg.symbol,\n seq: msg.seq,\n ts: msg.ts,\n };\n sub.handlers.forEach((handler) => {\n try {\n handler(msg.payload, meta);\n } catch {\n // Isolate handler faults so one bad listener can't kill the socket.\n }\n });\n } else if (msg.type === \"error\") {\n this.onError?.(msg.code ?? \"error\", msg.message ?? \"\");\n }\n // \"ack\" / \"heartbeat\" / \"pong\" require no action.\n }\n\n private send(obj: unknown): void {\n if (this.ws && this.ws.readyState === WebSocket.OPEN) {\n this.ws.send(JSON.stringify(obj));\n }\n }\n\n private sendSubscribe(sub: Subscription): void {\n if (this.apiKey && !this.authed) return;\n this.send({\n op: \"subscribe\",\n channel: sub.channel,\n symbol: sub.symbol,\n id: this.nextId++,\n });\n }\n\n private replaySubscriptions(): void {\n this.subs.forEach((sub) => this.sendSubscribe(sub));\n }\n\n /**\n * Subscribe to `channel` for `symbol`. Returns an unsubscribe function.\n * Safe to call before connect() — it is (re)sent on each connection.\n * Multiple handlers for the same (channel, symbol) share one subscription.\n */\n subscribe(\n channel: MmflowChannel,\n symbol: string,\n handler: MmflowDataHandler,\n ): Unsubscribe {\n const key = keyOf(channel, symbol);\n let sub = this.subs.get(key);\n if (!sub) {\n sub = { channel, symbol, handlers: new Set() };\n this.subs.set(key, sub);\n this.sendSubscribe(sub);\n }\n sub.handlers.add(handler);\n\n return () => {\n const existing = this.subs.get(key);\n if (!existing) return;\n existing.handlers.delete(handler);\n if (existing.handlers.size === 0) {\n this.subs.delete(key);\n this.send({ op: \"unsubscribe\", channel, symbol, id: this.nextId++ });\n }\n };\n }\n\n /** Close permanently (disables auto-reconnect) and drop all subscriptions. */\n close(): void {\n this.closedByUser = true;\n if (this.reconnectTimer != null) {\n clearTimeout(this.reconnectTimer);\n this.reconnectTimer = null;\n }\n this.subs.clear();\n this.authed = false;\n this.ws?.close();\n this.ws = null;\n }\n}\n","// createMmflowFeed — a ready-made DataFeed backed by mmflow's public API.\n// History comes from the REST candles endpoint. Live trades default to SSE\n// for backwards compatibility, or can use the keyed WebSocket gateway with\n// `transport: \"ws\"`.\n//\n// import { createChart } from \"@mmflow/charts\";\n// import { createMmflowFeed } from \"@mmflow/sdk\";\n// const chart = createChart({ container });\n// chart.addCandleSeries();\n// chart.setData(createMmflowFeed()); // symbol/resolution via the binding\n\nimport { MmflowSocket } from \"./socket\";\nimport type {\n Candle,\n DataFeed,\n FeedUpdate,\n FootprintBar,\n MarkerSpec,\n OrderBookSnapshot,\n Unsubscribe,\n VolumeProfileSnapshot,\n} from \"./types\";\n\nexport interface MmflowFeedOptions {\n /** API origin. Default \"https://mmflow.ai\". */\n baseUrl?: string;\n /** Live transport. Default \"sse\" for backwards compatibility. */\n transport?: \"sse\" | \"ws\";\n /** API key. Used in the WS auth frame, or as `api_key` for keyed SSE quotas. */\n apiKey?: string;\n /** WebSocket gateway URL. Default is the live Railway gateway. */\n wsUrl?: string;\n /** Default symbol, used when the chart doesn't supply one (e.g. a vanilla\n * createChart without setData symbol opts). */\n symbol?: string;\n /** Minimum trade notional (USD) to stream. Default 0 (all prints). */\n minUsd?: number;\n}\n\nconst DEFAULT_BASE = \"https://mmflow.ai\";\nconst DEFAULT_WS = \"wss://mmflowai-production.up.railway.app/v1\";\n\n// Hyperliquid candle row as returned by /api/hl/candles (OHLCV serialized as\n// strings). `t` is the bar open time in epoch ms.\ninterface HlCandleRow {\n t: number;\n o: string;\n h: string;\n l: string;\n c: string;\n v: string;\n}\n\n// Normalized trade tick emitted by /api/v1/streams/trades (event: \"trade\").\ninterface TradeTick {\n coin: string;\n side: \"buy\" | \"sell\";\n price: number;\n size: number;\n ts: number;\n}\n\nexport function createMmflowFeed(opts: MmflowFeedOptions = {}): DataFeed {\n const base = (opts.baseUrl ?? DEFAULT_BASE).replace(/\\/$/, \"\");\n const transport = opts.transport ?? \"sse\";\n let socket: MmflowSocket | null = null;\n let socketRefs = 0;\n\n const ensureSocket = (): MmflowSocket => {\n if (transport !== \"ws\") {\n throw new Error(\"createMmflowFeed: this method requires transport=\\\"ws\\\"\");\n }\n if (!opts.apiKey) {\n throw new Error(\"createMmflowFeed: transport=\\\"ws\\\" requires apiKey\");\n }\n if (!socket) {\n socket = new MmflowSocket({\n apiKey: opts.apiKey,\n url: opts.wsUrl ?? DEFAULT_WS,\n });\n socket.connect();\n }\n return socket;\n };\n\n const attachSocket = (\n channel: Parameters<MmflowSocket[\"subscribe\"]>[0],\n symbol: string,\n handler: Parameters<MmflowSocket[\"subscribe\"]>[2],\n ): Unsubscribe => {\n const sock = ensureSocket();\n socketRefs++;\n const unsub = sock.subscribe(channel, symbol, handler);\n return () => {\n unsub();\n socketRefs = Math.max(0, socketRefs - 1);\n if (socketRefs === 0) {\n socket?.close();\n socket = null;\n }\n };\n };\n\n const feed: DataFeed = {\n async fetchCandles({ symbol, resolution, from, to }) {\n const hours = Math.min(720, Math.max(1, Math.ceil((to - from) / 3_600_000)));\n const url = new URL(`${base}/api/hl/candles`);\n url.searchParams.set(\"coin\", symbol || opts.symbol || \"\");\n url.searchParams.set(\"interval\", resolution);\n url.searchParams.set(\"hours\", String(hours));\n\n const res = await fetch(url.toString());\n if (!res.ok) {\n throw new Error(`mmflow: candles request failed (${res.status})`);\n }\n const json = (await res.json()) as { candles?: HlCandleRow[] };\n const rows = json.candles ?? [];\n\n const out: Candle[] = [];\n for (const r of rows) {\n const time = Number(r.t);\n if (!Number.isFinite(time)) continue;\n out.push({\n time,\n open: Number(r.o),\n high: Number(r.h),\n low: Number(r.l),\n close: Number(r.c),\n volume: Number(r.v),\n });\n }\n return out;\n },\n\n subscribe(symbol, _resolution, onUpdate) {\n const feedSymbol = symbol || opts.symbol || \"\";\n if (transport === \"ws\") {\n return attachSocket(\"trades\", feedSymbol, (payload) => {\n const t = payload as Partial<TradeTick> & {\n ts?: number;\n time?: number;\n symbol?: string;\n };\n const price = Number(t.price);\n const size = Number(t.size);\n const time = Number(t.ts ?? t.time);\n if (!Number.isFinite(price) || !Number.isFinite(size) || !Number.isFinite(time)) {\n return;\n }\n onUpdate({\n symbol: t.coin ?? t.symbol ?? feedSymbol,\n price,\n size,\n time,\n side: t.side,\n } satisfies FeedUpdate);\n });\n }\n if (typeof EventSource === \"undefined\") {\n throw new Error(\n \"createMmflowFeed.subscribe needs a global EventSource (browser). \" +\n \"In Node, install an EventSource polyfill or use MmflowSocket instead.\",\n );\n }\n const url = new URL(`${base}/api/v1/streams/trades`);\n url.searchParams.set(\"coin\", feedSymbol);\n if (opts.minUsd != null) url.searchParams.set(\"minUsd\", String(opts.minUsd));\n if (opts.apiKey) url.searchParams.set(\"api_key\", opts.apiKey);\n\n const es = new EventSource(url.toString());\n const onTrade = (e: MessageEvent) => {\n try {\n const t = JSON.parse(e.data as string) as TradeTick;\n onUpdate({\n symbol: t.coin ?? symbol,\n price: t.price,\n size: t.size,\n time: t.ts,\n side: t.side,\n } satisfies FeedUpdate);\n } catch {\n // Ignore malformed frames; EventSource keeps the stream alive.\n }\n };\n es.addEventListener(\"trade\", onTrade as EventListener);\n return () => {\n es.removeEventListener(\"trade\", onTrade as EventListener);\n es.close();\n };\n },\n };\n\n feed.fetchFootprint = async (symbol, fpOpts): Promise<FootprintBar[]> => {\n const url = new URL(`${base}/api/v1/perps/footprint`);\n url.searchParams.set(\"coin\", symbol || opts.symbol || \"\");\n url.searchParams.set(\"kind\", \"time\");\n url.searchParams.set(\"interval\", String(fpOpts.intervalMs));\n url.searchParams.set(\"bars\", \"500\");\n const res = await fetch(url.toString());\n if (!res.ok) throw new Error(`mmflow: footprint request failed (${res.status})`);\n const json = (await res.json()) as { data?: Array<FootprintBar & { time?: number }> };\n return (json.data ?? []).map((bar) => ({\n t: Number((bar as { t?: number }).t ?? bar.time),\n open: Number(bar.open),\n high: Number(bar.high),\n low: Number(bar.low),\n close: Number(bar.close),\n levels: (bar.levels ?? []).map((level) => ({\n price: Number(level.price),\n buyVol: Number(level.buyVol),\n sellVol: Number(level.sellVol),\n })),\n }));\n };\n\n feed.fetchVolumeProfile = async (symbol): Promise<VolumeProfileSnapshot> => {\n const url = new URL(`${base}/api/hl/marketprofile`);\n url.searchParams.set(\"coin\", symbol || opts.symbol || \"\");\n url.searchParams.set(\"hours\", \"24\");\n url.searchParams.set(\"bins\", \"80\");\n const res = await fetch(url.toString());\n if (!res.ok) throw new Error(`mmflow: volume profile request failed (${res.status})`);\n const json = (await res.json()) as {\n bins?: Array<{ px: number; totalUsd: number }>;\n range?: { binWidth?: number };\n val?: number;\n vah?: number;\n };\n return {\n bins: (json.bins ?? []).map((bin) => ({\n px: Number(bin.px),\n totalUsd: Number(bin.totalUsd),\n })),\n binWidth: Number(json.range?.binWidth ?? 0),\n val: Number(json.val ?? 0),\n vah: Number(json.vah ?? 0),\n };\n };\n\n if (transport === \"ws\") {\n feed.subscribeOrderBook = (symbol, onBook) =>\n attachSocket(\"orderbook\", symbol || opts.symbol || \"\", (payload) => {\n const book = payload as {\n ts?: number;\n bids?: Array<{ price?: number; size?: number; px?: number; sz?: number }>;\n asks?: Array<{ price?: number; size?: number; px?: number; sz?: number }>;\n };\n const map = (rows: NonNullable<typeof book.bids>) =>\n rows.map((level) => ({\n px: Number(level.px ?? level.price),\n sz: Number(level.sz ?? level.size),\n }));\n onBook({\n time: Number(book.ts ?? Date.now()),\n bids: map(book.bids ?? []),\n asks: map(book.asks ?? []),\n } satisfies OrderBookSnapshot);\n });\n\n feed.subscribeMarkers = (symbol, onMarkers) =>\n attachSocket(\"liquidations\", symbol || opts.symbol || \"\", (payload) => {\n const e = payload as {\n ts?: number;\n price?: number;\n notionalUsd?: number;\n side?: string;\n };\n const marker: MarkerSpec = {\n t: Number(e.ts ?? Date.now()),\n price: Number(e.price),\n sizeUsd: Number(e.notionalUsd ?? 0),\n };\n if (!Number.isFinite(marker.price) || marker.sizeUsd <= 0) return;\n if (e.side === \"long\") onMarkers({ buys: [marker], sells: [] });\n else onMarkers({ buys: [], sells: [marker] });\n });\n }\n\n return feed;\n}\n"]}
|
package/dist/index.d.cts
ADDED
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
type Unsubscribe = () => void;
|
|
2
|
+
interface Candle {
|
|
3
|
+
time: number;
|
|
4
|
+
open: number;
|
|
5
|
+
high: number;
|
|
6
|
+
low: number;
|
|
7
|
+
close: number;
|
|
8
|
+
volume: number;
|
|
9
|
+
}
|
|
10
|
+
interface CandleRange {
|
|
11
|
+
symbol: string;
|
|
12
|
+
resolution: string;
|
|
13
|
+
from: number;
|
|
14
|
+
to: number;
|
|
15
|
+
}
|
|
16
|
+
interface OhlcvColumns {
|
|
17
|
+
time: ArrayLike<number>;
|
|
18
|
+
open: ArrayLike<number>;
|
|
19
|
+
high: ArrayLike<number>;
|
|
20
|
+
low: ArrayLike<number>;
|
|
21
|
+
close: ArrayLike<number>;
|
|
22
|
+
volume: ArrayLike<number>;
|
|
23
|
+
length?: number;
|
|
24
|
+
}
|
|
25
|
+
interface FeedUpdate {
|
|
26
|
+
symbol: string;
|
|
27
|
+
price: number;
|
|
28
|
+
size: number;
|
|
29
|
+
time: number;
|
|
30
|
+
side?: "buy" | "sell";
|
|
31
|
+
}
|
|
32
|
+
interface OrderBookLevel {
|
|
33
|
+
px: number;
|
|
34
|
+
sz: number;
|
|
35
|
+
}
|
|
36
|
+
interface OrderBookSnapshot {
|
|
37
|
+
time: number;
|
|
38
|
+
bids: OrderBookLevel[];
|
|
39
|
+
asks: OrderBookLevel[];
|
|
40
|
+
}
|
|
41
|
+
interface FootprintLevel {
|
|
42
|
+
price: number;
|
|
43
|
+
buyVol: number;
|
|
44
|
+
sellVol: number;
|
|
45
|
+
}
|
|
46
|
+
interface FootprintBar {
|
|
47
|
+
t: number;
|
|
48
|
+
open: number;
|
|
49
|
+
high: number;
|
|
50
|
+
low: number;
|
|
51
|
+
close: number;
|
|
52
|
+
levels: FootprintLevel[];
|
|
53
|
+
}
|
|
54
|
+
interface VolumeProfileSnapshot {
|
|
55
|
+
bins: Array<{
|
|
56
|
+
px: number;
|
|
57
|
+
totalUsd: number;
|
|
58
|
+
}>;
|
|
59
|
+
binWidth: number;
|
|
60
|
+
val: number;
|
|
61
|
+
vah: number;
|
|
62
|
+
}
|
|
63
|
+
interface MarkerSpec {
|
|
64
|
+
t: number;
|
|
65
|
+
price: number;
|
|
66
|
+
sizeUsd: number;
|
|
67
|
+
}
|
|
68
|
+
interface DataFeed {
|
|
69
|
+
fetchCandles(range: CandleRange): Promise<Candle[]>;
|
|
70
|
+
fetchCandlesColumns?(range: CandleRange): Promise<OhlcvColumns>;
|
|
71
|
+
subscribe(symbol: string, resolution: string, onUpdate: (u: FeedUpdate) => void): Unsubscribe;
|
|
72
|
+
subscribeOrderBook?(symbol: string, onBook: (b: OrderBookSnapshot) => void): Unsubscribe;
|
|
73
|
+
fetchFootprint?(symbol: string, opts: {
|
|
74
|
+
resolution: string;
|
|
75
|
+
from: number;
|
|
76
|
+
to: number;
|
|
77
|
+
intervalMs: number;
|
|
78
|
+
}): Promise<FootprintBar[]>;
|
|
79
|
+
fetchVolumeProfile?(symbol: string, opts: {
|
|
80
|
+
resolution: string;
|
|
81
|
+
from: number;
|
|
82
|
+
to: number;
|
|
83
|
+
intervalMs: number;
|
|
84
|
+
}): Promise<VolumeProfileSnapshot>;
|
|
85
|
+
subscribeMarkers?(symbol: string, onMarkers: (m: {
|
|
86
|
+
buys?: MarkerSpec[];
|
|
87
|
+
sells?: MarkerSpec[];
|
|
88
|
+
}) => void): Unsubscribe;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
interface MmflowFeedOptions {
|
|
92
|
+
/** API origin. Default "https://mmflow.ai". */
|
|
93
|
+
baseUrl?: string;
|
|
94
|
+
/** Live transport. Default "sse" for backwards compatibility. */
|
|
95
|
+
transport?: "sse" | "ws";
|
|
96
|
+
/** API key. Used in the WS auth frame, or as `api_key` for keyed SSE quotas. */
|
|
97
|
+
apiKey?: string;
|
|
98
|
+
/** WebSocket gateway URL. Default is the live Railway gateway. */
|
|
99
|
+
wsUrl?: string;
|
|
100
|
+
/** Default symbol, used when the chart doesn't supply one (e.g. a vanilla
|
|
101
|
+
* createChart without setData symbol opts). */
|
|
102
|
+
symbol?: string;
|
|
103
|
+
/** Minimum trade notional (USD) to stream. Default 0 (all prints). */
|
|
104
|
+
minUsd?: number;
|
|
105
|
+
}
|
|
106
|
+
declare function createMmflowFeed(opts?: MmflowFeedOptions): DataFeed;
|
|
107
|
+
|
|
108
|
+
type MmflowChannel = "trades" | "orderbook" | "candles" | "liquidations" | "funding" | "openInterest" | "markPrice" | "footprint";
|
|
109
|
+
interface MmflowSocketOptions {
|
|
110
|
+
/** Gateway URL. Default is the live Railway gateway. */
|
|
111
|
+
url?: string;
|
|
112
|
+
/** API key sent in the opening `auth` frame. */
|
|
113
|
+
apiKey?: string;
|
|
114
|
+
/** Reconnect automatically on drop. Default true. */
|
|
115
|
+
autoReconnect?: boolean;
|
|
116
|
+
/** Initial reconnect backoff (ms). Default 500. */
|
|
117
|
+
reconnectInitialMs?: number;
|
|
118
|
+
/** Max reconnect backoff (ms). Default 30000. */
|
|
119
|
+
reconnectMaxMs?: number;
|
|
120
|
+
/** Called on a server `error` frame. */
|
|
121
|
+
onError?: (code: string, message: string) => void;
|
|
122
|
+
}
|
|
123
|
+
interface MmflowMessageMeta {
|
|
124
|
+
channel: MmflowChannel;
|
|
125
|
+
symbol: string;
|
|
126
|
+
seq?: number;
|
|
127
|
+
ts?: number;
|
|
128
|
+
}
|
|
129
|
+
type MmflowDataHandler = (payload: unknown, meta: MmflowMessageMeta) => void;
|
|
130
|
+
declare class MmflowSocket {
|
|
131
|
+
private readonly url;
|
|
132
|
+
private readonly apiKey?;
|
|
133
|
+
private readonly autoReconnect;
|
|
134
|
+
private readonly reconnectInitialMs;
|
|
135
|
+
private readonly reconnectMaxMs;
|
|
136
|
+
private readonly onError?;
|
|
137
|
+
private ws;
|
|
138
|
+
private readonly subs;
|
|
139
|
+
private backoff;
|
|
140
|
+
private reconnectTimer;
|
|
141
|
+
private closedByUser;
|
|
142
|
+
private authed;
|
|
143
|
+
private nextId;
|
|
144
|
+
constructor(options?: MmflowSocketOptions);
|
|
145
|
+
/** Open the socket. Idempotent; subscriptions made before connect() are
|
|
146
|
+
* sent as soon as the connection is established. */
|
|
147
|
+
connect(): void;
|
|
148
|
+
private open;
|
|
149
|
+
private scheduleReconnect;
|
|
150
|
+
private handleMessage;
|
|
151
|
+
private send;
|
|
152
|
+
private sendSubscribe;
|
|
153
|
+
private replaySubscriptions;
|
|
154
|
+
/**
|
|
155
|
+
* Subscribe to `channel` for `symbol`. Returns an unsubscribe function.
|
|
156
|
+
* Safe to call before connect() — it is (re)sent on each connection.
|
|
157
|
+
* Multiple handlers for the same (channel, symbol) share one subscription.
|
|
158
|
+
*/
|
|
159
|
+
subscribe(channel: MmflowChannel, symbol: string, handler: MmflowDataHandler): Unsubscribe;
|
|
160
|
+
/** Close permanently (disables auto-reconnect) and drop all subscriptions. */
|
|
161
|
+
close(): void;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
export { type Candle, type CandleRange, type DataFeed, type FeedUpdate, type FootprintBar, type FootprintLevel, type MarkerSpec, type MmflowChannel, type MmflowDataHandler, type MmflowFeedOptions, type MmflowMessageMeta, MmflowSocket, type MmflowSocketOptions, type OhlcvColumns, type OrderBookLevel, type OrderBookSnapshot, type Unsubscribe, type VolumeProfileSnapshot, createMmflowFeed };
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
type Unsubscribe = () => void;
|
|
2
|
+
interface Candle {
|
|
3
|
+
time: number;
|
|
4
|
+
open: number;
|
|
5
|
+
high: number;
|
|
6
|
+
low: number;
|
|
7
|
+
close: number;
|
|
8
|
+
volume: number;
|
|
9
|
+
}
|
|
10
|
+
interface CandleRange {
|
|
11
|
+
symbol: string;
|
|
12
|
+
resolution: string;
|
|
13
|
+
from: number;
|
|
14
|
+
to: number;
|
|
15
|
+
}
|
|
16
|
+
interface OhlcvColumns {
|
|
17
|
+
time: ArrayLike<number>;
|
|
18
|
+
open: ArrayLike<number>;
|
|
19
|
+
high: ArrayLike<number>;
|
|
20
|
+
low: ArrayLike<number>;
|
|
21
|
+
close: ArrayLike<number>;
|
|
22
|
+
volume: ArrayLike<number>;
|
|
23
|
+
length?: number;
|
|
24
|
+
}
|
|
25
|
+
interface FeedUpdate {
|
|
26
|
+
symbol: string;
|
|
27
|
+
price: number;
|
|
28
|
+
size: number;
|
|
29
|
+
time: number;
|
|
30
|
+
side?: "buy" | "sell";
|
|
31
|
+
}
|
|
32
|
+
interface OrderBookLevel {
|
|
33
|
+
px: number;
|
|
34
|
+
sz: number;
|
|
35
|
+
}
|
|
36
|
+
interface OrderBookSnapshot {
|
|
37
|
+
time: number;
|
|
38
|
+
bids: OrderBookLevel[];
|
|
39
|
+
asks: OrderBookLevel[];
|
|
40
|
+
}
|
|
41
|
+
interface FootprintLevel {
|
|
42
|
+
price: number;
|
|
43
|
+
buyVol: number;
|
|
44
|
+
sellVol: number;
|
|
45
|
+
}
|
|
46
|
+
interface FootprintBar {
|
|
47
|
+
t: number;
|
|
48
|
+
open: number;
|
|
49
|
+
high: number;
|
|
50
|
+
low: number;
|
|
51
|
+
close: number;
|
|
52
|
+
levels: FootprintLevel[];
|
|
53
|
+
}
|
|
54
|
+
interface VolumeProfileSnapshot {
|
|
55
|
+
bins: Array<{
|
|
56
|
+
px: number;
|
|
57
|
+
totalUsd: number;
|
|
58
|
+
}>;
|
|
59
|
+
binWidth: number;
|
|
60
|
+
val: number;
|
|
61
|
+
vah: number;
|
|
62
|
+
}
|
|
63
|
+
interface MarkerSpec {
|
|
64
|
+
t: number;
|
|
65
|
+
price: number;
|
|
66
|
+
sizeUsd: number;
|
|
67
|
+
}
|
|
68
|
+
interface DataFeed {
|
|
69
|
+
fetchCandles(range: CandleRange): Promise<Candle[]>;
|
|
70
|
+
fetchCandlesColumns?(range: CandleRange): Promise<OhlcvColumns>;
|
|
71
|
+
subscribe(symbol: string, resolution: string, onUpdate: (u: FeedUpdate) => void): Unsubscribe;
|
|
72
|
+
subscribeOrderBook?(symbol: string, onBook: (b: OrderBookSnapshot) => void): Unsubscribe;
|
|
73
|
+
fetchFootprint?(symbol: string, opts: {
|
|
74
|
+
resolution: string;
|
|
75
|
+
from: number;
|
|
76
|
+
to: number;
|
|
77
|
+
intervalMs: number;
|
|
78
|
+
}): Promise<FootprintBar[]>;
|
|
79
|
+
fetchVolumeProfile?(symbol: string, opts: {
|
|
80
|
+
resolution: string;
|
|
81
|
+
from: number;
|
|
82
|
+
to: number;
|
|
83
|
+
intervalMs: number;
|
|
84
|
+
}): Promise<VolumeProfileSnapshot>;
|
|
85
|
+
subscribeMarkers?(symbol: string, onMarkers: (m: {
|
|
86
|
+
buys?: MarkerSpec[];
|
|
87
|
+
sells?: MarkerSpec[];
|
|
88
|
+
}) => void): Unsubscribe;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
interface MmflowFeedOptions {
|
|
92
|
+
/** API origin. Default "https://mmflow.ai". */
|
|
93
|
+
baseUrl?: string;
|
|
94
|
+
/** Live transport. Default "sse" for backwards compatibility. */
|
|
95
|
+
transport?: "sse" | "ws";
|
|
96
|
+
/** API key. Used in the WS auth frame, or as `api_key` for keyed SSE quotas. */
|
|
97
|
+
apiKey?: string;
|
|
98
|
+
/** WebSocket gateway URL. Default is the live Railway gateway. */
|
|
99
|
+
wsUrl?: string;
|
|
100
|
+
/** Default symbol, used when the chart doesn't supply one (e.g. a vanilla
|
|
101
|
+
* createChart without setData symbol opts). */
|
|
102
|
+
symbol?: string;
|
|
103
|
+
/** Minimum trade notional (USD) to stream. Default 0 (all prints). */
|
|
104
|
+
minUsd?: number;
|
|
105
|
+
}
|
|
106
|
+
declare function createMmflowFeed(opts?: MmflowFeedOptions): DataFeed;
|
|
107
|
+
|
|
108
|
+
type MmflowChannel = "trades" | "orderbook" | "candles" | "liquidations" | "funding" | "openInterest" | "markPrice" | "footprint";
|
|
109
|
+
interface MmflowSocketOptions {
|
|
110
|
+
/** Gateway URL. Default is the live Railway gateway. */
|
|
111
|
+
url?: string;
|
|
112
|
+
/** API key sent in the opening `auth` frame. */
|
|
113
|
+
apiKey?: string;
|
|
114
|
+
/** Reconnect automatically on drop. Default true. */
|
|
115
|
+
autoReconnect?: boolean;
|
|
116
|
+
/** Initial reconnect backoff (ms). Default 500. */
|
|
117
|
+
reconnectInitialMs?: number;
|
|
118
|
+
/** Max reconnect backoff (ms). Default 30000. */
|
|
119
|
+
reconnectMaxMs?: number;
|
|
120
|
+
/** Called on a server `error` frame. */
|
|
121
|
+
onError?: (code: string, message: string) => void;
|
|
122
|
+
}
|
|
123
|
+
interface MmflowMessageMeta {
|
|
124
|
+
channel: MmflowChannel;
|
|
125
|
+
symbol: string;
|
|
126
|
+
seq?: number;
|
|
127
|
+
ts?: number;
|
|
128
|
+
}
|
|
129
|
+
type MmflowDataHandler = (payload: unknown, meta: MmflowMessageMeta) => void;
|
|
130
|
+
declare class MmflowSocket {
|
|
131
|
+
private readonly url;
|
|
132
|
+
private readonly apiKey?;
|
|
133
|
+
private readonly autoReconnect;
|
|
134
|
+
private readonly reconnectInitialMs;
|
|
135
|
+
private readonly reconnectMaxMs;
|
|
136
|
+
private readonly onError?;
|
|
137
|
+
private ws;
|
|
138
|
+
private readonly subs;
|
|
139
|
+
private backoff;
|
|
140
|
+
private reconnectTimer;
|
|
141
|
+
private closedByUser;
|
|
142
|
+
private authed;
|
|
143
|
+
private nextId;
|
|
144
|
+
constructor(options?: MmflowSocketOptions);
|
|
145
|
+
/** Open the socket. Idempotent; subscriptions made before connect() are
|
|
146
|
+
* sent as soon as the connection is established. */
|
|
147
|
+
connect(): void;
|
|
148
|
+
private open;
|
|
149
|
+
private scheduleReconnect;
|
|
150
|
+
private handleMessage;
|
|
151
|
+
private send;
|
|
152
|
+
private sendSubscribe;
|
|
153
|
+
private replaySubscriptions;
|
|
154
|
+
/**
|
|
155
|
+
* Subscribe to `channel` for `symbol`. Returns an unsubscribe function.
|
|
156
|
+
* Safe to call before connect() — it is (re)sent on each connection.
|
|
157
|
+
* Multiple handlers for the same (channel, symbol) share one subscription.
|
|
158
|
+
*/
|
|
159
|
+
subscribe(channel: MmflowChannel, symbol: string, handler: MmflowDataHandler): Unsubscribe;
|
|
160
|
+
/** Close permanently (disables auto-reconnect) and drop all subscriptions. */
|
|
161
|
+
close(): void;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
export { type Candle, type CandleRange, type DataFeed, type FeedUpdate, type FootprintBar, type FootprintLevel, type MarkerSpec, type MmflowChannel, type MmflowDataHandler, type MmflowFeedOptions, type MmflowMessageMeta, MmflowSocket, type MmflowSocketOptions, type OhlcvColumns, type OrderBookLevel, type OrderBookSnapshot, type Unsubscribe, type VolumeProfileSnapshot, createMmflowFeed };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,331 @@
|
|
|
1
|
+
// src/socket.ts
|
|
2
|
+
var DEFAULT_URL = "wss://mmflowai-production.up.railway.app/v1";
|
|
3
|
+
var keyOf = (channel, symbol) => `${channel}:${symbol}`;
|
|
4
|
+
var MmflowSocket = class {
|
|
5
|
+
constructor(options = {}) {
|
|
6
|
+
this.ws = null;
|
|
7
|
+
this.subs = /* @__PURE__ */ new Map();
|
|
8
|
+
this.reconnectTimer = null;
|
|
9
|
+
this.closedByUser = false;
|
|
10
|
+
this.authed = false;
|
|
11
|
+
this.nextId = 1;
|
|
12
|
+
this.url = options.url ?? DEFAULT_URL;
|
|
13
|
+
this.apiKey = options.apiKey;
|
|
14
|
+
this.autoReconnect = options.autoReconnect ?? true;
|
|
15
|
+
this.reconnectInitialMs = options.reconnectInitialMs ?? 500;
|
|
16
|
+
this.reconnectMaxMs = options.reconnectMaxMs ?? 3e4;
|
|
17
|
+
this.onError = options.onError;
|
|
18
|
+
this.backoff = this.reconnectInitialMs;
|
|
19
|
+
}
|
|
20
|
+
/** Open the socket. Idempotent; subscriptions made before connect() are
|
|
21
|
+
* sent as soon as the connection is established. */
|
|
22
|
+
connect() {
|
|
23
|
+
this.closedByUser = false;
|
|
24
|
+
this.open();
|
|
25
|
+
}
|
|
26
|
+
open() {
|
|
27
|
+
if (typeof WebSocket === "undefined") {
|
|
28
|
+
throw new Error(
|
|
29
|
+
"MmflowSocket requires a global WebSocket (browser, or Node >= 21 / the `ws` package)."
|
|
30
|
+
);
|
|
31
|
+
}
|
|
32
|
+
if (this.ws && (this.ws.readyState === WebSocket.OPEN || this.ws.readyState === WebSocket.CONNECTING)) {
|
|
33
|
+
return;
|
|
34
|
+
}
|
|
35
|
+
const ws = new WebSocket(this.url);
|
|
36
|
+
this.ws = ws;
|
|
37
|
+
this.authed = false;
|
|
38
|
+
ws.onopen = () => {
|
|
39
|
+
this.backoff = this.reconnectInitialMs;
|
|
40
|
+
if (this.apiKey) this.send({ op: "auth", apiKey: this.apiKey });
|
|
41
|
+
else this.replaySubscriptions();
|
|
42
|
+
};
|
|
43
|
+
ws.onmessage = (ev) => this.handleMessage(ev);
|
|
44
|
+
ws.onclose = () => {
|
|
45
|
+
this.ws = null;
|
|
46
|
+
this.authed = false;
|
|
47
|
+
if (!this.closedByUser && this.autoReconnect) this.scheduleReconnect();
|
|
48
|
+
};
|
|
49
|
+
ws.onerror = () => {
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
scheduleReconnect() {
|
|
53
|
+
if (this.reconnectTimer != null) return;
|
|
54
|
+
const delay = this.backoff;
|
|
55
|
+
this.reconnectTimer = setTimeout(() => {
|
|
56
|
+
this.reconnectTimer = null;
|
|
57
|
+
this.open();
|
|
58
|
+
}, delay);
|
|
59
|
+
this.backoff = Math.min(this.backoff * 2, this.reconnectMaxMs);
|
|
60
|
+
}
|
|
61
|
+
handleMessage(ev) {
|
|
62
|
+
if (typeof ev.data !== "string") return;
|
|
63
|
+
let msg;
|
|
64
|
+
try {
|
|
65
|
+
msg = JSON.parse(ev.data);
|
|
66
|
+
} catch {
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
69
|
+
if (msg.type === "ack" && msg.op === "auth") {
|
|
70
|
+
this.authed = true;
|
|
71
|
+
this.replaySubscriptions();
|
|
72
|
+
} else if (msg.type === "data" && msg.channel && msg.symbol) {
|
|
73
|
+
const sub = this.subs.get(keyOf(msg.channel, msg.symbol));
|
|
74
|
+
if (!sub) return;
|
|
75
|
+
const meta = {
|
|
76
|
+
channel: msg.channel,
|
|
77
|
+
symbol: msg.symbol,
|
|
78
|
+
seq: msg.seq,
|
|
79
|
+
ts: msg.ts
|
|
80
|
+
};
|
|
81
|
+
sub.handlers.forEach((handler) => {
|
|
82
|
+
try {
|
|
83
|
+
handler(msg.payload, meta);
|
|
84
|
+
} catch {
|
|
85
|
+
}
|
|
86
|
+
});
|
|
87
|
+
} else if (msg.type === "error") {
|
|
88
|
+
this.onError?.(msg.code ?? "error", msg.message ?? "");
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
send(obj) {
|
|
92
|
+
if (this.ws && this.ws.readyState === WebSocket.OPEN) {
|
|
93
|
+
this.ws.send(JSON.stringify(obj));
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
sendSubscribe(sub) {
|
|
97
|
+
if (this.apiKey && !this.authed) return;
|
|
98
|
+
this.send({
|
|
99
|
+
op: "subscribe",
|
|
100
|
+
channel: sub.channel,
|
|
101
|
+
symbol: sub.symbol,
|
|
102
|
+
id: this.nextId++
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
replaySubscriptions() {
|
|
106
|
+
this.subs.forEach((sub) => this.sendSubscribe(sub));
|
|
107
|
+
}
|
|
108
|
+
/**
|
|
109
|
+
* Subscribe to `channel` for `symbol`. Returns an unsubscribe function.
|
|
110
|
+
* Safe to call before connect() — it is (re)sent on each connection.
|
|
111
|
+
* Multiple handlers for the same (channel, symbol) share one subscription.
|
|
112
|
+
*/
|
|
113
|
+
subscribe(channel, symbol, handler) {
|
|
114
|
+
const key = keyOf(channel, symbol);
|
|
115
|
+
let sub = this.subs.get(key);
|
|
116
|
+
if (!sub) {
|
|
117
|
+
sub = { channel, symbol, handlers: /* @__PURE__ */ new Set() };
|
|
118
|
+
this.subs.set(key, sub);
|
|
119
|
+
this.sendSubscribe(sub);
|
|
120
|
+
}
|
|
121
|
+
sub.handlers.add(handler);
|
|
122
|
+
return () => {
|
|
123
|
+
const existing = this.subs.get(key);
|
|
124
|
+
if (!existing) return;
|
|
125
|
+
existing.handlers.delete(handler);
|
|
126
|
+
if (existing.handlers.size === 0) {
|
|
127
|
+
this.subs.delete(key);
|
|
128
|
+
this.send({ op: "unsubscribe", channel, symbol, id: this.nextId++ });
|
|
129
|
+
}
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
/** Close permanently (disables auto-reconnect) and drop all subscriptions. */
|
|
133
|
+
close() {
|
|
134
|
+
this.closedByUser = true;
|
|
135
|
+
if (this.reconnectTimer != null) {
|
|
136
|
+
clearTimeout(this.reconnectTimer);
|
|
137
|
+
this.reconnectTimer = null;
|
|
138
|
+
}
|
|
139
|
+
this.subs.clear();
|
|
140
|
+
this.authed = false;
|
|
141
|
+
this.ws?.close();
|
|
142
|
+
this.ws = null;
|
|
143
|
+
}
|
|
144
|
+
};
|
|
145
|
+
|
|
146
|
+
// src/feed.ts
|
|
147
|
+
var DEFAULT_BASE = "https://mmflow.ai";
|
|
148
|
+
var DEFAULT_WS = "wss://mmflowai-production.up.railway.app/v1";
|
|
149
|
+
function createMmflowFeed(opts = {}) {
|
|
150
|
+
const base = (opts.baseUrl ?? DEFAULT_BASE).replace(/\/$/, "");
|
|
151
|
+
const transport = opts.transport ?? "sse";
|
|
152
|
+
let socket = null;
|
|
153
|
+
let socketRefs = 0;
|
|
154
|
+
const ensureSocket = () => {
|
|
155
|
+
if (transport !== "ws") {
|
|
156
|
+
throw new Error('createMmflowFeed: this method requires transport="ws"');
|
|
157
|
+
}
|
|
158
|
+
if (!opts.apiKey) {
|
|
159
|
+
throw new Error('createMmflowFeed: transport="ws" requires apiKey');
|
|
160
|
+
}
|
|
161
|
+
if (!socket) {
|
|
162
|
+
socket = new MmflowSocket({
|
|
163
|
+
apiKey: opts.apiKey,
|
|
164
|
+
url: opts.wsUrl ?? DEFAULT_WS
|
|
165
|
+
});
|
|
166
|
+
socket.connect();
|
|
167
|
+
}
|
|
168
|
+
return socket;
|
|
169
|
+
};
|
|
170
|
+
const attachSocket = (channel, symbol, handler) => {
|
|
171
|
+
const sock = ensureSocket();
|
|
172
|
+
socketRefs++;
|
|
173
|
+
const unsub = sock.subscribe(channel, symbol, handler);
|
|
174
|
+
return () => {
|
|
175
|
+
unsub();
|
|
176
|
+
socketRefs = Math.max(0, socketRefs - 1);
|
|
177
|
+
if (socketRefs === 0) {
|
|
178
|
+
socket?.close();
|
|
179
|
+
socket = null;
|
|
180
|
+
}
|
|
181
|
+
};
|
|
182
|
+
};
|
|
183
|
+
const feed = {
|
|
184
|
+
async fetchCandles({ symbol, resolution, from, to }) {
|
|
185
|
+
const hours = Math.min(720, Math.max(1, Math.ceil((to - from) / 36e5)));
|
|
186
|
+
const url = new URL(`${base}/api/hl/candles`);
|
|
187
|
+
url.searchParams.set("coin", symbol || opts.symbol || "");
|
|
188
|
+
url.searchParams.set("interval", resolution);
|
|
189
|
+
url.searchParams.set("hours", String(hours));
|
|
190
|
+
const res = await fetch(url.toString());
|
|
191
|
+
if (!res.ok) {
|
|
192
|
+
throw new Error(`mmflow: candles request failed (${res.status})`);
|
|
193
|
+
}
|
|
194
|
+
const json = await res.json();
|
|
195
|
+
const rows = json.candles ?? [];
|
|
196
|
+
const out = [];
|
|
197
|
+
for (const r of rows) {
|
|
198
|
+
const time = Number(r.t);
|
|
199
|
+
if (!Number.isFinite(time)) continue;
|
|
200
|
+
out.push({
|
|
201
|
+
time,
|
|
202
|
+
open: Number(r.o),
|
|
203
|
+
high: Number(r.h),
|
|
204
|
+
low: Number(r.l),
|
|
205
|
+
close: Number(r.c),
|
|
206
|
+
volume: Number(r.v)
|
|
207
|
+
});
|
|
208
|
+
}
|
|
209
|
+
return out;
|
|
210
|
+
},
|
|
211
|
+
subscribe(symbol, _resolution, onUpdate) {
|
|
212
|
+
const feedSymbol = symbol || opts.symbol || "";
|
|
213
|
+
if (transport === "ws") {
|
|
214
|
+
return attachSocket("trades", feedSymbol, (payload) => {
|
|
215
|
+
const t = payload;
|
|
216
|
+
const price = Number(t.price);
|
|
217
|
+
const size = Number(t.size);
|
|
218
|
+
const time = Number(t.ts ?? t.time);
|
|
219
|
+
if (!Number.isFinite(price) || !Number.isFinite(size) || !Number.isFinite(time)) {
|
|
220
|
+
return;
|
|
221
|
+
}
|
|
222
|
+
onUpdate({
|
|
223
|
+
symbol: t.coin ?? t.symbol ?? feedSymbol,
|
|
224
|
+
price,
|
|
225
|
+
size,
|
|
226
|
+
time,
|
|
227
|
+
side: t.side
|
|
228
|
+
});
|
|
229
|
+
});
|
|
230
|
+
}
|
|
231
|
+
if (typeof EventSource === "undefined") {
|
|
232
|
+
throw new Error(
|
|
233
|
+
"createMmflowFeed.subscribe needs a global EventSource (browser). In Node, install an EventSource polyfill or use MmflowSocket instead."
|
|
234
|
+
);
|
|
235
|
+
}
|
|
236
|
+
const url = new URL(`${base}/api/v1/streams/trades`);
|
|
237
|
+
url.searchParams.set("coin", feedSymbol);
|
|
238
|
+
if (opts.minUsd != null) url.searchParams.set("minUsd", String(opts.minUsd));
|
|
239
|
+
if (opts.apiKey) url.searchParams.set("api_key", opts.apiKey);
|
|
240
|
+
const es = new EventSource(url.toString());
|
|
241
|
+
const onTrade = (e) => {
|
|
242
|
+
try {
|
|
243
|
+
const t = JSON.parse(e.data);
|
|
244
|
+
onUpdate({
|
|
245
|
+
symbol: t.coin ?? symbol,
|
|
246
|
+
price: t.price,
|
|
247
|
+
size: t.size,
|
|
248
|
+
time: t.ts,
|
|
249
|
+
side: t.side
|
|
250
|
+
});
|
|
251
|
+
} catch {
|
|
252
|
+
}
|
|
253
|
+
};
|
|
254
|
+
es.addEventListener("trade", onTrade);
|
|
255
|
+
return () => {
|
|
256
|
+
es.removeEventListener("trade", onTrade);
|
|
257
|
+
es.close();
|
|
258
|
+
};
|
|
259
|
+
}
|
|
260
|
+
};
|
|
261
|
+
feed.fetchFootprint = async (symbol, fpOpts) => {
|
|
262
|
+
const url = new URL(`${base}/api/v1/perps/footprint`);
|
|
263
|
+
url.searchParams.set("coin", symbol || opts.symbol || "");
|
|
264
|
+
url.searchParams.set("kind", "time");
|
|
265
|
+
url.searchParams.set("interval", String(fpOpts.intervalMs));
|
|
266
|
+
url.searchParams.set("bars", "500");
|
|
267
|
+
const res = await fetch(url.toString());
|
|
268
|
+
if (!res.ok) throw new Error(`mmflow: footprint request failed (${res.status})`);
|
|
269
|
+
const json = await res.json();
|
|
270
|
+
return (json.data ?? []).map((bar) => ({
|
|
271
|
+
t: Number(bar.t ?? bar.time),
|
|
272
|
+
open: Number(bar.open),
|
|
273
|
+
high: Number(bar.high),
|
|
274
|
+
low: Number(bar.low),
|
|
275
|
+
close: Number(bar.close),
|
|
276
|
+
levels: (bar.levels ?? []).map((level) => ({
|
|
277
|
+
price: Number(level.price),
|
|
278
|
+
buyVol: Number(level.buyVol),
|
|
279
|
+
sellVol: Number(level.sellVol)
|
|
280
|
+
}))
|
|
281
|
+
}));
|
|
282
|
+
};
|
|
283
|
+
feed.fetchVolumeProfile = async (symbol) => {
|
|
284
|
+
const url = new URL(`${base}/api/hl/marketprofile`);
|
|
285
|
+
url.searchParams.set("coin", symbol || opts.symbol || "");
|
|
286
|
+
url.searchParams.set("hours", "24");
|
|
287
|
+
url.searchParams.set("bins", "80");
|
|
288
|
+
const res = await fetch(url.toString());
|
|
289
|
+
if (!res.ok) throw new Error(`mmflow: volume profile request failed (${res.status})`);
|
|
290
|
+
const json = await res.json();
|
|
291
|
+
return {
|
|
292
|
+
bins: (json.bins ?? []).map((bin) => ({
|
|
293
|
+
px: Number(bin.px),
|
|
294
|
+
totalUsd: Number(bin.totalUsd)
|
|
295
|
+
})),
|
|
296
|
+
binWidth: Number(json.range?.binWidth ?? 0),
|
|
297
|
+
val: Number(json.val ?? 0),
|
|
298
|
+
vah: Number(json.vah ?? 0)
|
|
299
|
+
};
|
|
300
|
+
};
|
|
301
|
+
if (transport === "ws") {
|
|
302
|
+
feed.subscribeOrderBook = (symbol, onBook) => attachSocket("orderbook", symbol || opts.symbol || "", (payload) => {
|
|
303
|
+
const book = payload;
|
|
304
|
+
const map = (rows) => rows.map((level) => ({
|
|
305
|
+
px: Number(level.px ?? level.price),
|
|
306
|
+
sz: Number(level.sz ?? level.size)
|
|
307
|
+
}));
|
|
308
|
+
onBook({
|
|
309
|
+
time: Number(book.ts ?? Date.now()),
|
|
310
|
+
bids: map(book.bids ?? []),
|
|
311
|
+
asks: map(book.asks ?? [])
|
|
312
|
+
});
|
|
313
|
+
});
|
|
314
|
+
feed.subscribeMarkers = (symbol, onMarkers) => attachSocket("liquidations", symbol || opts.symbol || "", (payload) => {
|
|
315
|
+
const e = payload;
|
|
316
|
+
const marker = {
|
|
317
|
+
t: Number(e.ts ?? Date.now()),
|
|
318
|
+
price: Number(e.price),
|
|
319
|
+
sizeUsd: Number(e.notionalUsd ?? 0)
|
|
320
|
+
};
|
|
321
|
+
if (!Number.isFinite(marker.price) || marker.sizeUsd <= 0) return;
|
|
322
|
+
if (e.side === "long") onMarkers({ buys: [marker], sells: [] });
|
|
323
|
+
else onMarkers({ buys: [], sells: [marker] });
|
|
324
|
+
});
|
|
325
|
+
}
|
|
326
|
+
return feed;
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
export { MmflowSocket, createMmflowFeed };
|
|
330
|
+
//# sourceMappingURL=index.js.map
|
|
331
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/socket.ts","../src/feed.ts"],"names":[],"mappings":"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","file":"index.js","sourcesContent":["// MmflowSocket — typed WebSocket client for the mmflow real-time data API.\n//\n// Protocol (JSON frames):\n// client → server : { op: \"auth\", apiKey }\n// { op: \"subscribe\", channel, symbol, id }\n// { op: \"unsubscribe\", channel, symbol, id }\n// server → client : { type: \"ack\", id, channel, symbol }\n// { type: \"data\", channel, symbol, seq, ts, payload }\n// { type: \"heartbeat\", ts }\n// { type: \"error\", code, message, id? }\n//\n// Resilience: exponential-backoff auto-reconnect, with re-auth + replay of all\n// active subscriptions on every (re)connection — the same discipline the app's\n// in-process HL client uses.\n\nimport type { Unsubscribe } from \"./types\";\n\nexport type MmflowChannel =\n | \"trades\"\n | \"orderbook\"\n | \"candles\"\n | \"liquidations\"\n | \"funding\"\n | \"openInterest\"\n | \"markPrice\"\n | \"footprint\";\n\nexport interface MmflowSocketOptions {\n /** Gateway URL. Default is the live Railway gateway. */\n url?: string;\n /** API key sent in the opening `auth` frame. */\n apiKey?: string;\n /** Reconnect automatically on drop. Default true. */\n autoReconnect?: boolean;\n /** Initial reconnect backoff (ms). Default 500. */\n reconnectInitialMs?: number;\n /** Max reconnect backoff (ms). Default 30000. */\n reconnectMaxMs?: number;\n /** Called on a server `error` frame. */\n onError?: (code: string, message: string) => void;\n}\n\nexport interface MmflowMessageMeta {\n channel: MmflowChannel;\n symbol: string;\n seq?: number;\n ts?: number;\n}\n\nexport type MmflowDataHandler = (\n payload: unknown,\n meta: MmflowMessageMeta,\n) => void;\n\ninterface IncomingMessage {\n type?: \"ack\" | \"data\" | \"heartbeat\" | \"pong\" | \"error\";\n op?: string;\n channel?: MmflowChannel;\n symbol?: string;\n seq?: number;\n ts?: number;\n payload?: unknown;\n code?: string;\n message?: string;\n}\n\ninterface Subscription {\n channel: MmflowChannel;\n symbol: string;\n handlers: Set<MmflowDataHandler>;\n}\n\nconst DEFAULT_URL = \"wss://mmflowai-production.up.railway.app/v1\";\nconst keyOf = (channel: string, symbol: string) => `${channel}:${symbol}`;\n\nexport class MmflowSocket {\n private readonly url: string;\n private readonly apiKey?: string;\n private readonly autoReconnect: boolean;\n private readonly reconnectInitialMs: number;\n private readonly reconnectMaxMs: number;\n private readonly onError?: (code: string, message: string) => void;\n\n private ws: WebSocket | null = null;\n private readonly subs = new Map<string, Subscription>();\n private backoff: number;\n private reconnectTimer: ReturnType<typeof setTimeout> | null = null;\n private closedByUser = false;\n private authed = false;\n private nextId = 1;\n\n constructor(options: MmflowSocketOptions = {}) {\n this.url = options.url ?? DEFAULT_URL;\n this.apiKey = options.apiKey;\n this.autoReconnect = options.autoReconnect ?? true;\n this.reconnectInitialMs = options.reconnectInitialMs ?? 500;\n this.reconnectMaxMs = options.reconnectMaxMs ?? 30_000;\n this.onError = options.onError;\n this.backoff = this.reconnectInitialMs;\n }\n\n /** Open the socket. Idempotent; subscriptions made before connect() are\n * sent as soon as the connection is established. */\n connect(): void {\n this.closedByUser = false;\n this.open();\n }\n\n private open(): void {\n if (typeof WebSocket === \"undefined\") {\n throw new Error(\n \"MmflowSocket requires a global WebSocket (browser, or Node >= 21 / the `ws` package).\",\n );\n }\n if (\n this.ws &&\n (this.ws.readyState === WebSocket.OPEN ||\n this.ws.readyState === WebSocket.CONNECTING)\n ) {\n return;\n }\n\n const ws = new WebSocket(this.url);\n this.ws = ws;\n this.authed = false;\n\n ws.onopen = () => {\n this.backoff = this.reconnectInitialMs;\n if (this.apiKey) this.send({ op: \"auth\", apiKey: this.apiKey });\n else this.replaySubscriptions();\n };\n ws.onmessage = (ev: MessageEvent) => this.handleMessage(ev);\n ws.onclose = () => {\n this.ws = null;\n this.authed = false;\n if (!this.closedByUser && this.autoReconnect) this.scheduleReconnect();\n };\n ws.onerror = () => {\n // A close event follows; reconnection is handled there.\n };\n }\n\n private scheduleReconnect(): void {\n if (this.reconnectTimer != null) return;\n const delay = this.backoff;\n this.reconnectTimer = setTimeout(() => {\n this.reconnectTimer = null;\n this.open();\n }, delay);\n this.backoff = Math.min(this.backoff * 2, this.reconnectMaxMs);\n }\n\n private handleMessage(ev: MessageEvent): void {\n if (typeof ev.data !== \"string\") return;\n let msg: IncomingMessage;\n try {\n msg = JSON.parse(ev.data) as IncomingMessage;\n } catch {\n return;\n }\n if (msg.type === \"ack\" && msg.op === \"auth\") {\n this.authed = true;\n this.replaySubscriptions();\n } else if (msg.type === \"data\" && msg.channel && msg.symbol) {\n const sub = this.subs.get(keyOf(msg.channel, msg.symbol));\n if (!sub) return;\n const meta: MmflowMessageMeta = {\n channel: msg.channel,\n symbol: msg.symbol,\n seq: msg.seq,\n ts: msg.ts,\n };\n sub.handlers.forEach((handler) => {\n try {\n handler(msg.payload, meta);\n } catch {\n // Isolate handler faults so one bad listener can't kill the socket.\n }\n });\n } else if (msg.type === \"error\") {\n this.onError?.(msg.code ?? \"error\", msg.message ?? \"\");\n }\n // \"ack\" / \"heartbeat\" / \"pong\" require no action.\n }\n\n private send(obj: unknown): void {\n if (this.ws && this.ws.readyState === WebSocket.OPEN) {\n this.ws.send(JSON.stringify(obj));\n }\n }\n\n private sendSubscribe(sub: Subscription): void {\n if (this.apiKey && !this.authed) return;\n this.send({\n op: \"subscribe\",\n channel: sub.channel,\n symbol: sub.symbol,\n id: this.nextId++,\n });\n }\n\n private replaySubscriptions(): void {\n this.subs.forEach((sub) => this.sendSubscribe(sub));\n }\n\n /**\n * Subscribe to `channel` for `symbol`. Returns an unsubscribe function.\n * Safe to call before connect() — it is (re)sent on each connection.\n * Multiple handlers for the same (channel, symbol) share one subscription.\n */\n subscribe(\n channel: MmflowChannel,\n symbol: string,\n handler: MmflowDataHandler,\n ): Unsubscribe {\n const key = keyOf(channel, symbol);\n let sub = this.subs.get(key);\n if (!sub) {\n sub = { channel, symbol, handlers: new Set() };\n this.subs.set(key, sub);\n this.sendSubscribe(sub);\n }\n sub.handlers.add(handler);\n\n return () => {\n const existing = this.subs.get(key);\n if (!existing) return;\n existing.handlers.delete(handler);\n if (existing.handlers.size === 0) {\n this.subs.delete(key);\n this.send({ op: \"unsubscribe\", channel, symbol, id: this.nextId++ });\n }\n };\n }\n\n /** Close permanently (disables auto-reconnect) and drop all subscriptions. */\n close(): void {\n this.closedByUser = true;\n if (this.reconnectTimer != null) {\n clearTimeout(this.reconnectTimer);\n this.reconnectTimer = null;\n }\n this.subs.clear();\n this.authed = false;\n this.ws?.close();\n this.ws = null;\n }\n}\n","// createMmflowFeed — a ready-made DataFeed backed by mmflow's public API.\n// History comes from the REST candles endpoint. Live trades default to SSE\n// for backwards compatibility, or can use the keyed WebSocket gateway with\n// `transport: \"ws\"`.\n//\n// import { createChart } from \"@mmflow/charts\";\n// import { createMmflowFeed } from \"@mmflow/sdk\";\n// const chart = createChart({ container });\n// chart.addCandleSeries();\n// chart.setData(createMmflowFeed()); // symbol/resolution via the binding\n\nimport { MmflowSocket } from \"./socket\";\nimport type {\n Candle,\n DataFeed,\n FeedUpdate,\n FootprintBar,\n MarkerSpec,\n OrderBookSnapshot,\n Unsubscribe,\n VolumeProfileSnapshot,\n} from \"./types\";\n\nexport interface MmflowFeedOptions {\n /** API origin. Default \"https://mmflow.ai\". */\n baseUrl?: string;\n /** Live transport. Default \"sse\" for backwards compatibility. */\n transport?: \"sse\" | \"ws\";\n /** API key. Used in the WS auth frame, or as `api_key` for keyed SSE quotas. */\n apiKey?: string;\n /** WebSocket gateway URL. Default is the live Railway gateway. */\n wsUrl?: string;\n /** Default symbol, used when the chart doesn't supply one (e.g. a vanilla\n * createChart without setData symbol opts). */\n symbol?: string;\n /** Minimum trade notional (USD) to stream. Default 0 (all prints). */\n minUsd?: number;\n}\n\nconst DEFAULT_BASE = \"https://mmflow.ai\";\nconst DEFAULT_WS = \"wss://mmflowai-production.up.railway.app/v1\";\n\n// Hyperliquid candle row as returned by /api/hl/candles (OHLCV serialized as\n// strings). `t` is the bar open time in epoch ms.\ninterface HlCandleRow {\n t: number;\n o: string;\n h: string;\n l: string;\n c: string;\n v: string;\n}\n\n// Normalized trade tick emitted by /api/v1/streams/trades (event: \"trade\").\ninterface TradeTick {\n coin: string;\n side: \"buy\" | \"sell\";\n price: number;\n size: number;\n ts: number;\n}\n\nexport function createMmflowFeed(opts: MmflowFeedOptions = {}): DataFeed {\n const base = (opts.baseUrl ?? DEFAULT_BASE).replace(/\\/$/, \"\");\n const transport = opts.transport ?? \"sse\";\n let socket: MmflowSocket | null = null;\n let socketRefs = 0;\n\n const ensureSocket = (): MmflowSocket => {\n if (transport !== \"ws\") {\n throw new Error(\"createMmflowFeed: this method requires transport=\\\"ws\\\"\");\n }\n if (!opts.apiKey) {\n throw new Error(\"createMmflowFeed: transport=\\\"ws\\\" requires apiKey\");\n }\n if (!socket) {\n socket = new MmflowSocket({\n apiKey: opts.apiKey,\n url: opts.wsUrl ?? DEFAULT_WS,\n });\n socket.connect();\n }\n return socket;\n };\n\n const attachSocket = (\n channel: Parameters<MmflowSocket[\"subscribe\"]>[0],\n symbol: string,\n handler: Parameters<MmflowSocket[\"subscribe\"]>[2],\n ): Unsubscribe => {\n const sock = ensureSocket();\n socketRefs++;\n const unsub = sock.subscribe(channel, symbol, handler);\n return () => {\n unsub();\n socketRefs = Math.max(0, socketRefs - 1);\n if (socketRefs === 0) {\n socket?.close();\n socket = null;\n }\n };\n };\n\n const feed: DataFeed = {\n async fetchCandles({ symbol, resolution, from, to }) {\n const hours = Math.min(720, Math.max(1, Math.ceil((to - from) / 3_600_000)));\n const url = new URL(`${base}/api/hl/candles`);\n url.searchParams.set(\"coin\", symbol || opts.symbol || \"\");\n url.searchParams.set(\"interval\", resolution);\n url.searchParams.set(\"hours\", String(hours));\n\n const res = await fetch(url.toString());\n if (!res.ok) {\n throw new Error(`mmflow: candles request failed (${res.status})`);\n }\n const json = (await res.json()) as { candles?: HlCandleRow[] };\n const rows = json.candles ?? [];\n\n const out: Candle[] = [];\n for (const r of rows) {\n const time = Number(r.t);\n if (!Number.isFinite(time)) continue;\n out.push({\n time,\n open: Number(r.o),\n high: Number(r.h),\n low: Number(r.l),\n close: Number(r.c),\n volume: Number(r.v),\n });\n }\n return out;\n },\n\n subscribe(symbol, _resolution, onUpdate) {\n const feedSymbol = symbol || opts.symbol || \"\";\n if (transport === \"ws\") {\n return attachSocket(\"trades\", feedSymbol, (payload) => {\n const t = payload as Partial<TradeTick> & {\n ts?: number;\n time?: number;\n symbol?: string;\n };\n const price = Number(t.price);\n const size = Number(t.size);\n const time = Number(t.ts ?? t.time);\n if (!Number.isFinite(price) || !Number.isFinite(size) || !Number.isFinite(time)) {\n return;\n }\n onUpdate({\n symbol: t.coin ?? t.symbol ?? feedSymbol,\n price,\n size,\n time,\n side: t.side,\n } satisfies FeedUpdate);\n });\n }\n if (typeof EventSource === \"undefined\") {\n throw new Error(\n \"createMmflowFeed.subscribe needs a global EventSource (browser). \" +\n \"In Node, install an EventSource polyfill or use MmflowSocket instead.\",\n );\n }\n const url = new URL(`${base}/api/v1/streams/trades`);\n url.searchParams.set(\"coin\", feedSymbol);\n if (opts.minUsd != null) url.searchParams.set(\"minUsd\", String(opts.minUsd));\n if (opts.apiKey) url.searchParams.set(\"api_key\", opts.apiKey);\n\n const es = new EventSource(url.toString());\n const onTrade = (e: MessageEvent) => {\n try {\n const t = JSON.parse(e.data as string) as TradeTick;\n onUpdate({\n symbol: t.coin ?? symbol,\n price: t.price,\n size: t.size,\n time: t.ts,\n side: t.side,\n } satisfies FeedUpdate);\n } catch {\n // Ignore malformed frames; EventSource keeps the stream alive.\n }\n };\n es.addEventListener(\"trade\", onTrade as EventListener);\n return () => {\n es.removeEventListener(\"trade\", onTrade as EventListener);\n es.close();\n };\n },\n };\n\n feed.fetchFootprint = async (symbol, fpOpts): Promise<FootprintBar[]> => {\n const url = new URL(`${base}/api/v1/perps/footprint`);\n url.searchParams.set(\"coin\", symbol || opts.symbol || \"\");\n url.searchParams.set(\"kind\", \"time\");\n url.searchParams.set(\"interval\", String(fpOpts.intervalMs));\n url.searchParams.set(\"bars\", \"500\");\n const res = await fetch(url.toString());\n if (!res.ok) throw new Error(`mmflow: footprint request failed (${res.status})`);\n const json = (await res.json()) as { data?: Array<FootprintBar & { time?: number }> };\n return (json.data ?? []).map((bar) => ({\n t: Number((bar as { t?: number }).t ?? bar.time),\n open: Number(bar.open),\n high: Number(bar.high),\n low: Number(bar.low),\n close: Number(bar.close),\n levels: (bar.levels ?? []).map((level) => ({\n price: Number(level.price),\n buyVol: Number(level.buyVol),\n sellVol: Number(level.sellVol),\n })),\n }));\n };\n\n feed.fetchVolumeProfile = async (symbol): Promise<VolumeProfileSnapshot> => {\n const url = new URL(`${base}/api/hl/marketprofile`);\n url.searchParams.set(\"coin\", symbol || opts.symbol || \"\");\n url.searchParams.set(\"hours\", \"24\");\n url.searchParams.set(\"bins\", \"80\");\n const res = await fetch(url.toString());\n if (!res.ok) throw new Error(`mmflow: volume profile request failed (${res.status})`);\n const json = (await res.json()) as {\n bins?: Array<{ px: number; totalUsd: number }>;\n range?: { binWidth?: number };\n val?: number;\n vah?: number;\n };\n return {\n bins: (json.bins ?? []).map((bin) => ({\n px: Number(bin.px),\n totalUsd: Number(bin.totalUsd),\n })),\n binWidth: Number(json.range?.binWidth ?? 0),\n val: Number(json.val ?? 0),\n vah: Number(json.vah ?? 0),\n };\n };\n\n if (transport === \"ws\") {\n feed.subscribeOrderBook = (symbol, onBook) =>\n attachSocket(\"orderbook\", symbol || opts.symbol || \"\", (payload) => {\n const book = payload as {\n ts?: number;\n bids?: Array<{ price?: number; size?: number; px?: number; sz?: number }>;\n asks?: Array<{ price?: number; size?: number; px?: number; sz?: number }>;\n };\n const map = (rows: NonNullable<typeof book.bids>) =>\n rows.map((level) => ({\n px: Number(level.px ?? level.price),\n sz: Number(level.sz ?? level.size),\n }));\n onBook({\n time: Number(book.ts ?? Date.now()),\n bids: map(book.bids ?? []),\n asks: map(book.asks ?? []),\n } satisfies OrderBookSnapshot);\n });\n\n feed.subscribeMarkers = (symbol, onMarkers) =>\n attachSocket(\"liquidations\", symbol || opts.symbol || \"\", (payload) => {\n const e = payload as {\n ts?: number;\n price?: number;\n notionalUsd?: number;\n side?: string;\n };\n const marker: MarkerSpec = {\n t: Number(e.ts ?? Date.now()),\n price: Number(e.price),\n sizeUsd: Number(e.notionalUsd ?? 0),\n };\n if (!Number.isFinite(marker.price) || marker.sizeUsd <= 0) return;\n if (e.side === \"long\") onMarkers({ buys: [marker], sells: [] });\n else onMarkers({ buys: [], sells: [marker] });\n });\n }\n\n return feed;\n}\n"]}
|
package/package.json
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@mmflow/sdk",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "mmflow data SDK — a typed WebSocket client and a ready-made createMmflowFeed() that plugs live mmflow market data straight into @mmflow/charts.",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"sideEffects": false,
|
|
8
|
+
"main": "./dist/index.cjs",
|
|
9
|
+
"module": "./dist/index.js",
|
|
10
|
+
"types": "./dist/index.d.ts",
|
|
11
|
+
"exports": {
|
|
12
|
+
".": {
|
|
13
|
+
"import": {
|
|
14
|
+
"types": "./dist/index.d.ts",
|
|
15
|
+
"default": "./dist/index.js"
|
|
16
|
+
},
|
|
17
|
+
"require": {
|
|
18
|
+
"types": "./dist/index.d.cts",
|
|
19
|
+
"default": "./dist/index.cjs"
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
},
|
|
23
|
+
"files": [
|
|
24
|
+
"dist"
|
|
25
|
+
],
|
|
26
|
+
"scripts": {
|
|
27
|
+
"build": "tsup src/index.ts --format esm,cjs --dts --treeshake",
|
|
28
|
+
"typecheck": "tsc --noEmit"
|
|
29
|
+
}
|
|
30
|
+
}
|