openbroker 1.9.2 → 1.9.4
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/CHANGELOG.md +21 -0
- package/README.md +16 -0
- package/SKILL.md +69 -3
- package/dist/auto/cli.js +4 -1
- package/dist/auto/examples/dca.d.ts +2 -1
- package/dist/auto/examples/dca.d.ts.map +1 -1
- package/dist/auto/examples/dca.js +19 -1
- package/dist/auto/examples/funding-arb.d.ts +2 -1
- package/dist/auto/examples/funding-arb.d.ts.map +1 -1
- package/dist/auto/examples/funding-arb.js +19 -2
- package/dist/auto/examples/grid.d.ts +2 -1
- package/dist/auto/examples/grid.d.ts.map +1 -1
- package/dist/auto/examples/grid.js +18 -2
- package/dist/auto/examples/mm-maker.d.ts +2 -1
- package/dist/auto/examples/mm-maker.d.ts.map +1 -1
- package/dist/auto/examples/mm-maker.js +18 -2
- package/dist/auto/examples/mm-spread.d.ts +2 -1
- package/dist/auto/examples/mm-spread.d.ts.map +1 -1
- package/dist/auto/examples/mm-spread.js +18 -2
- package/dist/auto/examples/price-alert.d.ts +2 -1
- package/dist/auto/examples/price-alert.d.ts.map +1 -1
- package/dist/auto/examples/price-alert.js +2 -1
- package/dist/auto/guardrails.d.ts +19 -0
- package/dist/auto/guardrails.d.ts.map +1 -0
- package/dist/auto/guardrails.js +575 -0
- package/dist/auto/guardrails.test.d.ts +2 -0
- package/dist/auto/guardrails.test.d.ts.map +1 -0
- package/dist/auto/guardrails.test.js +173 -0
- package/dist/auto/loader.d.ts +3 -3
- package/dist/auto/loader.d.ts.map +1 -1
- package/dist/auto/loader.js +25 -3
- package/dist/auto/realtime.d.ts +45 -0
- package/dist/auto/realtime.d.ts.map +1 -0
- package/dist/auto/realtime.js +177 -0
- package/dist/auto/realtime.test.d.ts +2 -0
- package/dist/auto/realtime.test.d.ts.map +1 -0
- package/dist/auto/realtime.test.js +73 -0
- package/dist/auto/runtime.d.ts +3 -3
- package/dist/auto/runtime.d.ts.map +1 -1
- package/dist/auto/runtime.js +155 -65
- package/dist/auto/types.d.ts +45 -1
- package/dist/auto/types.d.ts.map +1 -1
- package/dist/core/client.d.ts +66 -1
- package/dist/core/client.d.ts.map +1 -1
- package/dist/core/client.js +141 -4
- package/dist/core/ws.d.ts +14 -2
- package/dist/core/ws.d.ts.map +1 -1
- package/dist/core/ws.js +53 -7
- package/dist/lib.d.ts +2 -0
- package/dist/lib.d.ts.map +1 -1
- package/dist/lib.js +1 -0
- package/package.json +5 -3
- package/scripts/auto/cli.ts +4 -1
- package/scripts/auto/examples/dca.ts +21 -2
- package/scripts/auto/examples/funding-arb.ts +21 -3
- package/scripts/auto/examples/grid.ts +20 -3
- package/scripts/auto/examples/mm-maker.ts +20 -3
- package/scripts/auto/examples/mm-spread.ts +20 -3
- package/scripts/auto/examples/price-alert.ts +4 -2
- package/scripts/auto/guardrails.test.ts +227 -0
- package/scripts/auto/guardrails.ts +700 -0
- package/scripts/auto/loader.ts +41 -4
- package/scripts/auto/realtime.test.ts +84 -0
- package/scripts/auto/realtime.ts +194 -0
- package/scripts/auto/runtime.ts +163 -69
- package/scripts/auto/types.ts +56 -1
- package/scripts/core/client.ts +175 -4
- package/scripts/core/ws.ts +57 -8
- package/scripts/lib.ts +10 -0
package/scripts/auto/loader.ts
CHANGED
|
@@ -4,7 +4,14 @@ import { existsSync, readdirSync, mkdirSync } from 'fs';
|
|
|
4
4
|
import path from 'path';
|
|
5
5
|
import os from 'os';
|
|
6
6
|
import { fileURLToPath } from 'url';
|
|
7
|
-
import
|
|
7
|
+
import { resolveAutomationGuardrails } from './guardrails.js';
|
|
8
|
+
import type {
|
|
9
|
+
AutomationFactory,
|
|
10
|
+
AutomationConfig,
|
|
11
|
+
AutomationGuardrailContext,
|
|
12
|
+
AutomationGuardrailsExport,
|
|
13
|
+
LoadedAutomation,
|
|
14
|
+
} from './types.js';
|
|
8
15
|
|
|
9
16
|
const __filename = fileURLToPath(import.meta.url);
|
|
10
17
|
const __dirname = path.dirname(__filename);
|
|
@@ -117,8 +124,27 @@ function resolveAutomationConfig(mod: Record<string, unknown>): AutomationConfig
|
|
|
117
124
|
return null;
|
|
118
125
|
}
|
|
119
126
|
|
|
120
|
-
|
|
121
|
-
|
|
127
|
+
function resolveGuardrailsExport(mod: Record<string, unknown>): AutomationGuardrailsExport | null {
|
|
128
|
+
const candidates = [
|
|
129
|
+
mod.guardrails,
|
|
130
|
+
(mod.default as Record<string, unknown> | undefined)?.guardrails,
|
|
131
|
+
(mod["module.exports"] as Record<string, unknown> | undefined)?.guardrails,
|
|
132
|
+
];
|
|
133
|
+
|
|
134
|
+
for (const candidate of candidates) {
|
|
135
|
+
if (candidate && (typeof candidate === 'object' || typeof candidate === 'function')) {
|
|
136
|
+
return candidate as AutomationGuardrailsExport;
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
return null;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/** Load an automation module and validate its factory plus required guardrails export. */
|
|
144
|
+
export async function loadAutomation(
|
|
145
|
+
scriptPath: string,
|
|
146
|
+
context: AutomationGuardrailContext = { config: {} },
|
|
147
|
+
): Promise<LoadedAutomation> {
|
|
122
148
|
const absolutePath = path.resolve(scriptPath);
|
|
123
149
|
|
|
124
150
|
// Dynamic import — tsx handles TypeScript transpilation
|
|
@@ -132,7 +158,18 @@ export async function loadAutomation(scriptPath: string): Promise<AutomationFact
|
|
|
132
158
|
);
|
|
133
159
|
}
|
|
134
160
|
|
|
135
|
-
|
|
161
|
+
const guardrailsExport = resolveGuardrailsExport(mod as Record<string, unknown>);
|
|
162
|
+
if (!guardrailsExport) {
|
|
163
|
+
throw new Error(
|
|
164
|
+
`Automation script must export "guardrails".\n` +
|
|
165
|
+
`Use { mode: "read-only" } for monitoring-only scripts or a validated trading policy.`,
|
|
166
|
+
);
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
return {
|
|
170
|
+
factory: factory as AutomationFactory,
|
|
171
|
+
guardrails: resolveAutomationGuardrails(guardrailsExport, context),
|
|
172
|
+
};
|
|
136
173
|
}
|
|
137
174
|
|
|
138
175
|
/** List available automation scripts in ~/.openbroker/automations/ */
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import assert from 'node:assert/strict';
|
|
2
|
+
import test from 'node:test';
|
|
3
|
+
import type { HyperliquidClient } from '../core/client.js';
|
|
4
|
+
import type { WebSocketManager, WsEventMap, WsEventType } from '../core/ws.js';
|
|
5
|
+
import { AutomationRealtimeData } from './realtime.js';
|
|
6
|
+
|
|
7
|
+
class FakeWebSocket {
|
|
8
|
+
connected = true;
|
|
9
|
+
private handlers = new Map<WsEventType, Set<(value: unknown) => void>>();
|
|
10
|
+
|
|
11
|
+
on(event: WsEventType, handler: (value: unknown) => void): void {
|
|
12
|
+
const set = this.handlers.get(event) ?? new Set();
|
|
13
|
+
set.add(handler);
|
|
14
|
+
this.handlers.set(event, set);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
emit<E extends WsEventType>(event: E, value: WsEventMap[E]): void {
|
|
18
|
+
for (const handler of this.handlers.get(event) ?? []) handler(value);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
async subscribeL2Book(coin: string): Promise<unknown> {
|
|
22
|
+
queueMicrotask(() => this.emit('l2Book', {
|
|
23
|
+
coin,
|
|
24
|
+
time: Date.now(),
|
|
25
|
+
levels: [
|
|
26
|
+
[{ px: '99', sz: '2', n: 1 }],
|
|
27
|
+
[{ px: '101', sz: '3', n: 1 }],
|
|
28
|
+
],
|
|
29
|
+
}));
|
|
30
|
+
return {};
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const ADDRESS = '0x0000000000000000000000000000000000000001';
|
|
35
|
+
|
|
36
|
+
test('realtime cache serves subscribed market/account data and seeds books on demand', async () => {
|
|
37
|
+
const ws = new FakeWebSocket();
|
|
38
|
+
const mergedState = {
|
|
39
|
+
assetPositions: [],
|
|
40
|
+
marginSummary: { accountValue: '100', totalNtlPos: '0', totalRawUsd: '100', totalMarginUsed: '0', withdrawable: '100' },
|
|
41
|
+
crossMarginSummary: { accountValue: '100', totalNtlPos: '0', totalRawUsd: '100', totalMarginUsed: '0', withdrawable: '100' },
|
|
42
|
+
crossMaintenanceMarginUsed: '0',
|
|
43
|
+
};
|
|
44
|
+
const client = {
|
|
45
|
+
userStateAllFromWs: () => mergedState,
|
|
46
|
+
} as unknown as HyperliquidClient;
|
|
47
|
+
const cache = new AutomationRealtimeData(ws as unknown as WebSocketManager, client, ADDRESS, true);
|
|
48
|
+
|
|
49
|
+
ws.emit('allMids', { mids: { HYPE: '100' } });
|
|
50
|
+
ws.emit('allDexsAssetCtxs', { ctxs: [['', [{ funding: '0.0001', openInterest: '1', dayNtlVlm: '2', premium: '0', oraclePx: '100', markPx: '100', prevDayPx: '99' }]]] });
|
|
51
|
+
ws.emit('spotState', { balances: [{ coin: 'USDC', token: 0, total: '100', hold: '0' }] });
|
|
52
|
+
ws.emit('openOrders', { user: ADDRESS, dex: '', orders: [] });
|
|
53
|
+
ws.emit('allDexsClearinghouseState', {
|
|
54
|
+
user: ADDRESS,
|
|
55
|
+
clearinghouseStates: [['', {
|
|
56
|
+
assetPositions: [],
|
|
57
|
+
marginSummary: { accountValue: '100', totalNtlPos: '0', totalRawUsd: '100', totalMarginUsed: '0', withdrawable: '100' },
|
|
58
|
+
crossMarginSummary: { accountValue: '100', totalNtlPos: '0', totalRawUsd: '100', totalMarginUsed: '0', withdrawable: '100' },
|
|
59
|
+
crossMaintenanceMarginUsed: '0',
|
|
60
|
+
withdrawable: '100',
|
|
61
|
+
}]],
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
assert.equal(await cache.waitUntilReady(20), true);
|
|
65
|
+
assert.equal(cache.getAllMids()?.HYPE, '100');
|
|
66
|
+
assert.equal(cache.getMainAssetCtxs()?.[0]?.funding, '0.0001');
|
|
67
|
+
assert.equal(cache.getSpotBalances(ADDRESS)?.balances[0]?.entryNtl, '0');
|
|
68
|
+
assert.deepEqual(cache.getOpenOrders(ADDRESS), []);
|
|
69
|
+
assert.equal(cache.getUserState(ADDRESS)?.marginSummary.accountValue, '100');
|
|
70
|
+
assert.equal(cache.getUserStateAll(ADDRESS)?.marginSummary.accountValue, '100');
|
|
71
|
+
|
|
72
|
+
const book = await cache.getL2Book('HYPE');
|
|
73
|
+
assert.equal(book?.levels[0][0]?.px, '99');
|
|
74
|
+
assert.equal(book?.levels[1][0]?.px, '101');
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
test('realtime cache declines reads while disconnected so the client can fall back to REST', () => {
|
|
78
|
+
const ws = new FakeWebSocket();
|
|
79
|
+
const client = { userStateAllFromWs: () => null } as unknown as HyperliquidClient;
|
|
80
|
+
const cache = new AutomationRealtimeData(ws as unknown as WebSocketManager, client, ADDRESS, false);
|
|
81
|
+
ws.emit('allMids', { mids: { HYPE: '100' } });
|
|
82
|
+
ws.connected = false;
|
|
83
|
+
assert.equal(cache.getAllMids(), null);
|
|
84
|
+
});
|
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
import type { AssetCtx, ClearinghouseState, OpenOrder } from '../core/types.js';
|
|
2
|
+
import type {
|
|
3
|
+
HyperliquidClient,
|
|
4
|
+
RealtimeBookSnapshot,
|
|
5
|
+
RealtimeDataProvider,
|
|
6
|
+
} from '../core/client.js';
|
|
7
|
+
import { WebSocketManager, type WsEventMap } from '../core/ws.js';
|
|
8
|
+
|
|
9
|
+
interface Timed<T> {
|
|
10
|
+
value: T;
|
|
11
|
+
timestamp: number;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
const MARKET_DATA_STALE_MS = 45_000;
|
|
15
|
+
const BOOK_STALE_MS = 10_000;
|
|
16
|
+
const BOOK_SEED_TIMEOUT_MS = 1_500;
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Runtime-owned WebSocket cache used transparently by HyperliquidClient read
|
|
20
|
+
* methods. While the socket is healthy, automation code calling getAllMids,
|
|
21
|
+
* getUserState(All), getSpotBalances, getMetaAndAssetCtxs, or getL2Book reads
|
|
22
|
+
* this cache. Missing/stale data returns null so the client falls back to REST.
|
|
23
|
+
*/
|
|
24
|
+
export class AutomationRealtimeData implements RealtimeDataProvider {
|
|
25
|
+
private mids: Timed<Record<string, string>> | null = null;
|
|
26
|
+
private assetCtxs: Timed<WsEventMap['allDexsAssetCtxs']['ctxs']> | null = null;
|
|
27
|
+
private clearinghouse: Timed<WsEventMap['allDexsClearinghouseState']['clearinghouseStates']> | null = null;
|
|
28
|
+
private spot: Timed<WsEventMap['spotState']> | null = null;
|
|
29
|
+
private books = new Map<string, Timed<RealtimeBookSnapshot>>();
|
|
30
|
+
private openOrders = new Map<string, Timed<OpenOrder[]>>();
|
|
31
|
+
private bookSubscriptions = new Set<string>();
|
|
32
|
+
private bookWaiters = new Map<string, Set<() => void>>();
|
|
33
|
+
|
|
34
|
+
constructor(
|
|
35
|
+
private readonly ws: WebSocketManager,
|
|
36
|
+
private readonly client: HyperliquidClient,
|
|
37
|
+
private readonly user: string,
|
|
38
|
+
private readonly unified: boolean | null,
|
|
39
|
+
private readonly expectedOrderDexes: string[] = [''],
|
|
40
|
+
) {
|
|
41
|
+
ws.on('allMids', (data) => { this.mids = { value: data.mids, timestamp: Date.now() }; });
|
|
42
|
+
ws.on('allDexsAssetCtxs', (data) => { this.assetCtxs = { value: data.ctxs, timestamp: Date.now() }; });
|
|
43
|
+
ws.on('allDexsClearinghouseState', (data) => {
|
|
44
|
+
this.clearinghouse = { value: data.clearinghouseStates, timestamp: Date.now() };
|
|
45
|
+
});
|
|
46
|
+
ws.on('spotState', (data) => { this.spot = { value: data, timestamp: Date.now() }; });
|
|
47
|
+
ws.on('openOrders', (data) => {
|
|
48
|
+
if (this.sameUser(data.user)) this.openOrders.set(data.dex || '', { value: data.orders, timestamp: Date.now() });
|
|
49
|
+
});
|
|
50
|
+
ws.on('l2Book', (data) => {
|
|
51
|
+
this.books.set(data.coin, { value: data, timestamp: Date.now() });
|
|
52
|
+
const waiters = this.bookWaiters.get(data.coin);
|
|
53
|
+
if (waiters) {
|
|
54
|
+
for (const resolve of waiters) resolve();
|
|
55
|
+
waiters.clear();
|
|
56
|
+
}
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
get connected(): boolean {
|
|
61
|
+
return this.ws.connected;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
getAllMids(): Record<string, string> | null {
|
|
65
|
+
return this.fresh(this.mids, MARKET_DATA_STALE_MS)?.value ?? null;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
getMainAssetCtxs(): AssetCtx[] | null {
|
|
69
|
+
const groups = this.fresh(this.assetCtxs, MARKET_DATA_STALE_MS)?.value;
|
|
70
|
+
if (!groups) return null;
|
|
71
|
+
const main = groups.find(([dex]) => !dex || dex === 'main') ?? groups[0];
|
|
72
|
+
return (main?.[1] as unknown as AssetCtx[] | undefined) ?? null;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
getAllDexsAssetCtxs(): WsEventMap['allDexsAssetCtxs']['ctxs'] | null {
|
|
76
|
+
return this.fresh(this.assetCtxs, MARKET_DATA_STALE_MS)?.value ?? null;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
getUserState(user: string, dex?: string): ClearinghouseState | null {
|
|
80
|
+
if (!this.sameUser(user)) return null;
|
|
81
|
+
const groups = this.clearinghouse?.value;
|
|
82
|
+
if (!groups) return null;
|
|
83
|
+
const targetDex = dex ?? '';
|
|
84
|
+
const raw = groups.find(([name]) => (name || '') === targetDex)?.[1];
|
|
85
|
+
if (!raw) return null;
|
|
86
|
+
const withdrawable = raw.withdrawable;
|
|
87
|
+
return {
|
|
88
|
+
...raw,
|
|
89
|
+
marginSummary: raw.marginSummary && withdrawable != null
|
|
90
|
+
? { ...raw.marginSummary, withdrawable }
|
|
91
|
+
: raw.marginSummary,
|
|
92
|
+
crossMarginSummary: raw.crossMarginSummary && withdrawable != null
|
|
93
|
+
? { ...raw.crossMarginSummary, withdrawable }
|
|
94
|
+
: raw.crossMarginSummary,
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
getUserStateAll(user: string): ClearinghouseState | null {
|
|
99
|
+
if (!this.sameUser(user) || !this.clearinghouse || this.unified === null) return null;
|
|
100
|
+
return this.client.userStateAllFromWs(
|
|
101
|
+
this.clearinghouse.value,
|
|
102
|
+
this.unified,
|
|
103
|
+
this.spot ? { balances: this.spot.value.balances } : undefined,
|
|
104
|
+
);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
getSpotBalances(user: string): ReturnType<RealtimeDataProvider['getSpotBalances']> {
|
|
108
|
+
if (!this.sameUser(user) || !this.spot) return null;
|
|
109
|
+
return {
|
|
110
|
+
balances: this.spot.value.balances.map((balance) => ({
|
|
111
|
+
...balance,
|
|
112
|
+
entryNtl: balance.entryNtl ?? '0',
|
|
113
|
+
})),
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
getOpenOrders(user: string): OpenOrder[] | null {
|
|
118
|
+
if (!this.sameUser(user) || !this.connected) return null;
|
|
119
|
+
const orders: OpenOrder[] = [];
|
|
120
|
+
for (const dex of new Set(['', ...this.expectedOrderDexes])) {
|
|
121
|
+
const entry = this.openOrders.get(dex);
|
|
122
|
+
if (!entry) return null;
|
|
123
|
+
orders.push(...entry.value);
|
|
124
|
+
}
|
|
125
|
+
return orders;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
async getL2Book(coin: string): Promise<RealtimeBookSnapshot | null> {
|
|
129
|
+
const cached = this.fresh(this.books.get(coin) ?? null, BOOK_STALE_MS);
|
|
130
|
+
if (cached) return cached.value;
|
|
131
|
+
if (!this.connected) return null;
|
|
132
|
+
|
|
133
|
+
if (!this.bookSubscriptions.has(coin)) {
|
|
134
|
+
this.bookSubscriptions.add(coin);
|
|
135
|
+
try {
|
|
136
|
+
await this.ws.subscribeL2Book(coin);
|
|
137
|
+
} catch {
|
|
138
|
+
this.bookSubscriptions.delete(coin);
|
|
139
|
+
return null;
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
const seeded = this.fresh(this.books.get(coin) ?? null, BOOK_STALE_MS);
|
|
144
|
+
if (seeded) return seeded.value;
|
|
145
|
+
|
|
146
|
+
await new Promise<void>((resolve) => {
|
|
147
|
+
const waiters = this.bookWaiters.get(coin) ?? new Set<() => void>();
|
|
148
|
+
this.bookWaiters.set(coin, waiters);
|
|
149
|
+
const done = () => {
|
|
150
|
+
clearTimeout(timer);
|
|
151
|
+
waiters.delete(done);
|
|
152
|
+
resolve();
|
|
153
|
+
};
|
|
154
|
+
const timer = setTimeout(done, BOOK_SEED_TIMEOUT_MS);
|
|
155
|
+
waiters.add(done);
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
return this.fresh(this.books.get(coin) ?? null, BOOK_STALE_MS)?.value ?? null;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/** Wait briefly for initial subscription snapshots before the first strategy hook runs. */
|
|
162
|
+
async waitUntilReady(timeoutMs = 10_000): Promise<boolean> {
|
|
163
|
+
const deadline = Date.now() + timeoutMs;
|
|
164
|
+
while (Date.now() < deadline) {
|
|
165
|
+
if (this.coreFeedsReady()) return true;
|
|
166
|
+
await new Promise((resolve) => setTimeout(resolve, 25));
|
|
167
|
+
}
|
|
168
|
+
return this.coreFeedsReady();
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
readinessSummary(): { expectedOrderDexes: number; seededOrderDexes: number; missingOrderDexes: string[] } {
|
|
172
|
+
const expected = [...new Set(['', ...this.expectedOrderDexes])];
|
|
173
|
+
return {
|
|
174
|
+
expectedOrderDexes: expected.length,
|
|
175
|
+
seededOrderDexes: expected.filter((dex) => this.openOrders.has(dex)).length,
|
|
176
|
+
missingOrderDexes: expected.filter((dex) => !this.openOrders.has(dex)),
|
|
177
|
+
};
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
private coreFeedsReady(): boolean {
|
|
181
|
+
const ordersReady = [...new Set(['', ...this.expectedOrderDexes])]
|
|
182
|
+
.every((dex) => this.openOrders.has(dex));
|
|
183
|
+
return Boolean(this.mids && this.assetCtxs && this.clearinghouse && this.spot && ordersReady);
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
private sameUser(user: string): boolean {
|
|
187
|
+
return user.toLowerCase() === this.user.toLowerCase();
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
private fresh<T>(entry: Timed<T> | null, maxAgeMs: number): Timed<T> | null {
|
|
191
|
+
if (!this.connected || !entry || Date.now() - entry.timestamp > maxAgeMs) return null;
|
|
192
|
+
return entry;
|
|
193
|
+
}
|
|
194
|
+
}
|