@imbingox/acex 0.1.0-beta.0 → 0.1.0-beta.1

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.
Files changed (64) hide show
  1. package/README.md +12 -54
  2. package/index.ts +1 -0
  3. package/package.json +15 -24
  4. package/src/adapters/binance/adapter.ts +53 -0
  5. package/src/adapters/binance/book-ticker.ts +123 -0
  6. package/src/adapters/binance/market-catalog.ts +251 -0
  7. package/src/adapters/types.ts +43 -0
  8. package/src/client/context.ts +60 -0
  9. package/src/client/create-client.ts +6 -0
  10. package/src/client/runtime.ts +283 -0
  11. package/src/errors.ts +20 -0
  12. package/src/index.ts +4 -0
  13. package/src/internal/async-event-bus.ts +100 -0
  14. package/src/internal/filters.ts +119 -0
  15. package/src/internal/managed-websocket.ts +258 -0
  16. package/src/managers/account-manager.ts +315 -0
  17. package/src/managers/market-manager.ts +642 -0
  18. package/src/managers/order-manager.ts +304 -0
  19. package/src/types/account.ts +160 -0
  20. package/src/types/client.ts +79 -0
  21. package/src/types/index.ts +5 -0
  22. package/src/types/market.ts +136 -0
  23. package/src/types/order.ts +142 -0
  24. package/src/types/shared.ts +78 -0
  25. package/dist/adapters/ccxt/aster-ccxt-adapter.d.ts +0 -157
  26. package/dist/adapters/ccxt/aster-ccxt-adapter.js +0 -272
  27. package/dist/adapters/ccxt/binance-usdm-ccxt-adapter.d.ts +0 -179
  28. package/dist/adapters/ccxt/binance-usdm-ccxt-adapter.js +0 -537
  29. package/dist/adapters/fake/fake-aster-adapter.d.ts +0 -130
  30. package/dist/adapters/fake/fake-aster-adapter.js +0 -283
  31. package/dist/adapters/types.d.ts +0 -210
  32. package/dist/adapters/types.js +0 -1
  33. package/dist/core/client.d.ts +0 -37
  34. package/dist/core/client.js +0 -45
  35. package/dist/core/recovery.d.ts +0 -22
  36. package/dist/core/recovery.js +0 -18
  37. package/dist/core/runtime.d.ts +0 -26
  38. package/dist/core/runtime.js +0 -150
  39. package/dist/errors/acex-error.d.ts +0 -25
  40. package/dist/errors/acex-error.js +0 -54
  41. package/dist/index.d.ts +0 -5
  42. package/dist/index.js +0 -3
  43. package/dist/managers/account-manager.d.ts +0 -41
  44. package/dist/managers/account-manager.js +0 -80
  45. package/dist/managers/market-manager.d.ts +0 -16
  46. package/dist/managers/market-manager.js +0 -28
  47. package/dist/managers/order-manager.d.ts +0 -87
  48. package/dist/managers/order-manager.js +0 -122
  49. package/dist/runtime/async-queue.d.ts +0 -8
  50. package/dist/runtime/async-queue.js +0 -88
  51. package/dist/runtime/request-id.d.ts +0 -1
  52. package/dist/runtime/request-id.js +0 -5
  53. package/dist/store/account-store.d.ts +0 -52
  54. package/dist/store/account-store.js +0 -18
  55. package/dist/store/health-store.d.ts +0 -16
  56. package/dist/store/health-store.js +0 -29
  57. package/dist/store/market-store.d.ts +0 -42
  58. package/dist/store/market-store.js +0 -51
  59. package/dist/store/order-store.d.ts +0 -38
  60. package/dist/store/order-store.js +0 -49
  61. package/dist/testing/create-fake-runtime.d.ts +0 -5
  62. package/dist/testing/create-fake-runtime.js +0 -7
  63. package/dist/types/public.d.ts +0 -11
  64. package/dist/types/public.js +0 -1
@@ -0,0 +1,283 @@
1
+ import { BinanceMarketAdapter } from "../adapters/binance/adapter.ts";
2
+ import { AcexError, type AcexErrorCode } from "../errors.ts";
3
+ import { AsyncEventBus } from "../internal/async-event-bus.ts";
4
+ import { matchesHealthFilter } from "../internal/filters.ts";
5
+ import { AccountManagerImpl } from "../managers/account-manager.ts";
6
+ import { MarketManagerImpl } from "../managers/market-manager.ts";
7
+ import { OrderManagerImpl } from "../managers/order-manager.ts";
8
+ import type {
9
+ AccountCredentials,
10
+ AccountManager,
11
+ AcexClient,
12
+ AcexInternalError,
13
+ ClientEventStreams,
14
+ ClientHealthSnapshot,
15
+ ClientStatus,
16
+ ClientStatusChangedEvent,
17
+ CreateClientOptions,
18
+ HealthEvent,
19
+ HealthEventFilter,
20
+ MarketManager,
21
+ OrderManager,
22
+ RegisterAccountInput,
23
+ RegisterAccountResult,
24
+ StopOptions,
25
+ } from "../types/index.ts";
26
+ import {
27
+ type ClientContext,
28
+ hasPrivateCredentials,
29
+ mergeCredentials,
30
+ type RegisteredAccountRecord,
31
+ } from "./context.ts";
32
+
33
+ class ClientEventStreamsImpl implements ClientEventStreams {
34
+ constructor(
35
+ private readonly healthBus: AsyncEventBus<HealthEvent>,
36
+ private readonly errorBus: AsyncEventBus<AcexInternalError>,
37
+ ) {}
38
+
39
+ errors(): AsyncIterable<AcexInternalError> {
40
+ return this.errorBus.stream();
41
+ }
42
+
43
+ health(filter?: HealthEventFilter): AsyncIterable<HealthEvent> {
44
+ return this.healthBus.stream((event) => matchesHealthFilter(event, filter));
45
+ }
46
+ }
47
+
48
+ export class AcexClientImpl implements AcexClient, ClientContext {
49
+ readonly market: MarketManager;
50
+ readonly account: AccountManager;
51
+ readonly order: OrderManager;
52
+ readonly events: ClientEventStreams;
53
+
54
+ private status: ClientStatus = "idle";
55
+ private readonly healthBus = new AsyncEventBus<HealthEvent>();
56
+ private readonly errorBus = new AsyncEventBus<AcexInternalError>();
57
+ private readonly registeredAccounts = new Map<
58
+ string,
59
+ RegisteredAccountRecord
60
+ >();
61
+ private readonly marketManager: MarketManagerImpl;
62
+ private readonly accountManager: AccountManagerImpl;
63
+ private readonly orderManager: OrderManagerImpl;
64
+
65
+ constructor(options: CreateClientOptions = {}) {
66
+ const adapter = new BinanceMarketAdapter();
67
+
68
+ this.marketManager = new MarketManagerImpl(this, adapter, {
69
+ initialL1TimeoutMs: options.market?.l1InitialMessageTimeoutMs,
70
+ l1StaleAfterMs: options.market?.l1StaleAfterMs,
71
+ l1ReconnectDelayMs: options.market?.l1ReconnectDelayMs,
72
+ l1ReconnectMaxDelayMs: options.market?.l1ReconnectMaxDelayMs,
73
+ });
74
+ this.accountManager = new AccountManagerImpl(this);
75
+ this.orderManager = new OrderManagerImpl(this);
76
+
77
+ this.market = this.marketManager;
78
+ this.account = this.accountManager;
79
+ this.order = this.orderManager;
80
+ this.events = new ClientEventStreamsImpl(this.healthBus, this.errorBus);
81
+ }
82
+
83
+ // --- AcexClient public API ---
84
+
85
+ getStatus(): ClientStatus {
86
+ return this.status;
87
+ }
88
+
89
+ getHealth(): ClientHealthSnapshot {
90
+ return {
91
+ clientStatus: this.status,
92
+ markets: this.marketManager.getStatuses(),
93
+ accounts: this.accountManager.getStatuses(),
94
+ orders: this.orderManager.getStatuses(),
95
+ updatedAt: this.now(),
96
+ };
97
+ }
98
+
99
+ async registerAccount(
100
+ input: RegisterAccountInput,
101
+ ): Promise<RegisterAccountResult> {
102
+ if (this.registeredAccounts.has(input.accountId)) {
103
+ throw this.createError(
104
+ "ACCOUNT_ALREADY_EXISTS",
105
+ `Account already exists: ${input.accountId}`,
106
+ { accountId: input.accountId, exchange: input.exchange },
107
+ );
108
+ }
109
+
110
+ this.registeredAccounts.set(input.accountId, {
111
+ accountId: input.accountId,
112
+ exchange: input.exchange,
113
+ credentials: input.credentials,
114
+ options: input.options,
115
+ });
116
+
117
+ return {
118
+ accountId: input.accountId,
119
+ exchange: input.exchange,
120
+ };
121
+ }
122
+
123
+ async updateAccountCredentials(
124
+ accountId: string,
125
+ credentials: AccountCredentials,
126
+ ): Promise<void> {
127
+ const account = this.registeredAccounts.get(accountId);
128
+ if (!account) {
129
+ throw this.createError(
130
+ "ACCOUNT_NOT_FOUND",
131
+ `Account not found: ${accountId}`,
132
+ { accountId },
133
+ );
134
+ }
135
+
136
+ account.credentials = mergeCredentials(account.credentials, credentials);
137
+
138
+ if (this.status !== "running") {
139
+ return;
140
+ }
141
+
142
+ this.accountManager.onCredentialsUpdated(accountId, account.exchange);
143
+ this.orderManager.onCredentialsUpdated(accountId, account.exchange);
144
+ }
145
+
146
+ async removeAccount(accountId: string): Promise<void> {
147
+ const account = this.registeredAccounts.get(accountId);
148
+ if (!account) {
149
+ throw this.createError(
150
+ "ACCOUNT_NOT_FOUND",
151
+ `Account not found: ${accountId}`,
152
+ { accountId },
153
+ );
154
+ }
155
+
156
+ const now = this.now();
157
+ this.accountManager.onAccountRemoved(accountId, now);
158
+ this.orderManager.onAccountRemoved(accountId, now);
159
+ this.registeredAccounts.delete(accountId);
160
+ }
161
+
162
+ async start(): Promise<void> {
163
+ if (this.status === "running") {
164
+ return;
165
+ }
166
+
167
+ this.setClientStatus("starting");
168
+ this.setClientStatus("running");
169
+
170
+ this.marketManager.onClientStarted();
171
+ this.accountManager.onClientStarted();
172
+ this.orderManager.onClientStarted();
173
+ }
174
+
175
+ async stop(_options?: StopOptions): Promise<void> {
176
+ if (this.status === "stopped" || this.status === "idle") {
177
+ if (this.status !== "stopped") {
178
+ this.setClientStatus("stopped");
179
+ }
180
+ return;
181
+ }
182
+
183
+ this.setClientStatus("stopping");
184
+
185
+ const now = this.now();
186
+ this.marketManager.onClientStopping(now);
187
+ this.accountManager.onClientStopping(now);
188
+ this.orderManager.onClientStopping(now);
189
+
190
+ this.setClientStatus("stopped");
191
+ }
192
+
193
+ // --- ClientContext ---
194
+
195
+ now(): number {
196
+ return Date.now();
197
+ }
198
+
199
+ assertStarted(): void {
200
+ if (this.status !== "running") {
201
+ throw this.createError(
202
+ "CLIENT_NOT_STARTED",
203
+ "Client must be started before subscribing to data",
204
+ );
205
+ }
206
+ }
207
+
208
+ getRegisteredAccount(accountId: string): RegisteredAccountRecord {
209
+ const account = this.registeredAccounts.get(accountId);
210
+ if (!account) {
211
+ throw this.createError(
212
+ "ACCOUNT_NOT_FOUND",
213
+ `Account not found: ${accountId}`,
214
+ { accountId },
215
+ );
216
+ }
217
+
218
+ return account;
219
+ }
220
+
221
+ ensurePrivateCredentials(accountId: string): void {
222
+ const account = this.getRegisteredAccount(accountId);
223
+ if (hasPrivateCredentials(account.credentials)) {
224
+ return;
225
+ }
226
+
227
+ throw this.createError(
228
+ "CREDENTIALS_MISSING",
229
+ `Account credentials are required for private subscriptions: ${accountId}`,
230
+ { accountId, exchange: account.exchange },
231
+ );
232
+ }
233
+
234
+ publishRuntimeError(
235
+ source: AcexInternalError["source"],
236
+ error: Error,
237
+ metadata?: Omit<AcexInternalError, "error" | "source" | "ts">,
238
+ ): void {
239
+ this.errorBus.publish({
240
+ source,
241
+ ts: this.now(),
242
+ error,
243
+ ...metadata,
244
+ });
245
+ }
246
+
247
+ publishHealthEvent(event: HealthEvent): void {
248
+ this.healthBus.publish(event);
249
+ }
250
+
251
+ // --- Private ---
252
+
253
+ private setClientStatus(status: ClientStatus): void {
254
+ if (this.status === status) {
255
+ return;
256
+ }
257
+
258
+ this.status = status;
259
+
260
+ const event: ClientStatusChangedEvent = {
261
+ type: "client.status_changed",
262
+ status,
263
+ ts: this.now(),
264
+ };
265
+
266
+ this.healthBus.publish(event);
267
+ }
268
+
269
+ private createError(
270
+ code: AcexErrorCode,
271
+ message: string,
272
+ metadata?: Omit<AcexInternalError, "error" | "source" | "ts">,
273
+ ): AcexError {
274
+ const error = new AcexError(code, message);
275
+ this.errorBus.publish({
276
+ source: "client",
277
+ ts: this.now(),
278
+ error,
279
+ ...metadata,
280
+ });
281
+ return error;
282
+ }
283
+ }
package/src/errors.ts ADDED
@@ -0,0 +1,20 @@
1
+ export type AcexErrorCode =
2
+ | "ACCOUNT_ALREADY_EXISTS"
3
+ | "ACCOUNT_NOT_FOUND"
4
+ | "CLIENT_NOT_STARTED"
5
+ | "CREDENTIALS_MISSING"
6
+ | "EXCHANGE_NOT_SUPPORTED"
7
+ | "MARKET_CATALOG_LOAD_FAILED"
8
+ | "MARKET_INACTIVE"
9
+ | "MARKET_NOT_FOUND"
10
+ | "MARKET_STREAM_TIMEOUT";
11
+
12
+ export class AcexError extends Error {
13
+ readonly code: AcexErrorCode;
14
+
15
+ constructor(code: AcexErrorCode, message: string) {
16
+ super(message);
17
+ this.name = "AcexError";
18
+ this.code = code;
19
+ }
20
+ }
package/src/index.ts ADDED
@@ -0,0 +1,4 @@
1
+ export { createClient } from "./client/create-client.ts";
2
+ export type { AcexErrorCode } from "./errors.ts";
3
+ export { AcexError } from "./errors.ts";
4
+ export * from "./types/index.ts";
@@ -0,0 +1,100 @@
1
+ type EventPredicate<T> = (event: T) => boolean;
2
+
3
+ interface BusListener<T> {
4
+ close(): void;
5
+ dispatch(event: T): void;
6
+ }
7
+
8
+ function doneResult<T>(): IteratorResult<T> {
9
+ return { done: true, value: undefined as T };
10
+ }
11
+
12
+ export class AsyncEventBus<T> {
13
+ private readonly listeners = new Set<BusListener<T>>();
14
+
15
+ publish(event: T): void {
16
+ for (const listener of this.listeners) {
17
+ listener.dispatch(event);
18
+ }
19
+ }
20
+
21
+ stream<U extends T = T>(
22
+ filter: ((event: T) => event is U) | EventPredicate<T> = () => true,
23
+ ): AsyncIterable<U> {
24
+ let closed = false;
25
+ const queue: U[] = [];
26
+ let pendingResolve: ((result: IteratorResult<U>) => void) | undefined;
27
+
28
+ const close = () => {
29
+ if (closed) {
30
+ return;
31
+ }
32
+
33
+ closed = true;
34
+ this.listeners.delete(listener);
35
+
36
+ if (pendingResolve) {
37
+ const resolve = pendingResolve;
38
+ pendingResolve = undefined;
39
+ resolve(doneResult<U>());
40
+ }
41
+ };
42
+
43
+ const listener: BusListener<T> = {
44
+ close,
45
+ dispatch: (event) => {
46
+ if (closed || !filter(event)) {
47
+ return;
48
+ }
49
+
50
+ const typedEvent = event as U;
51
+ if (pendingResolve) {
52
+ const resolve = pendingResolve;
53
+ pendingResolve = undefined;
54
+ resolve({ done: false, value: typedEvent });
55
+ return;
56
+ }
57
+
58
+ queue.push(typedEvent);
59
+ },
60
+ };
61
+
62
+ this.listeners.add(listener);
63
+
64
+ const iterator: AsyncIterableIterator<U> = {
65
+ [Symbol.asyncIterator]() {
66
+ return iterator;
67
+ },
68
+ next: async () => {
69
+ if (closed) {
70
+ return doneResult<U>();
71
+ }
72
+
73
+ const queued = queue.shift();
74
+ if (queued !== undefined) {
75
+ return { done: false, value: queued };
76
+ }
77
+
78
+ return await new Promise<IteratorResult<U>>((resolve) => {
79
+ pendingResolve = resolve;
80
+ });
81
+ },
82
+ return: async () => {
83
+ close();
84
+ return doneResult<U>();
85
+ },
86
+ throw: async (error?: unknown) => {
87
+ close();
88
+ throw error;
89
+ },
90
+ };
91
+
92
+ return iterator;
93
+ }
94
+
95
+ close(): void {
96
+ for (const listener of [...this.listeners]) {
97
+ listener.close();
98
+ }
99
+ }
100
+ }
@@ -0,0 +1,119 @@
1
+ import type {
2
+ AccountEventFilter,
3
+ Exchange,
4
+ HealthEvent,
5
+ HealthEventFilter,
6
+ MarketEventFilter,
7
+ OrderEventFilter,
8
+ } from "../types/index.ts";
9
+
10
+ export function matchesMarketFilter(
11
+ event: { exchange: Exchange; symbol: string },
12
+ filter?: MarketEventFilter,
13
+ ): boolean {
14
+ if (!filter) {
15
+ return true;
16
+ }
17
+
18
+ if (filter.exchange && event.exchange !== filter.exchange) {
19
+ return false;
20
+ }
21
+
22
+ if (filter.symbol && event.symbol !== filter.symbol) {
23
+ return false;
24
+ }
25
+
26
+ return true;
27
+ }
28
+
29
+ export function matchesAccountFilter(
30
+ event: { accountId: string; exchange: Exchange; symbol?: string },
31
+ filter?: AccountEventFilter,
32
+ ): boolean {
33
+ if (!filter) {
34
+ return true;
35
+ }
36
+
37
+ if (filter.accountId && event.accountId !== filter.accountId) {
38
+ return false;
39
+ }
40
+
41
+ if (filter.exchange && event.exchange !== filter.exchange) {
42
+ return false;
43
+ }
44
+
45
+ if (filter.symbol && event.symbol !== filter.symbol) {
46
+ return false;
47
+ }
48
+
49
+ return true;
50
+ }
51
+
52
+ export function matchesOrderFilter(
53
+ event: { accountId: string; exchange: Exchange; symbol?: string },
54
+ filter?: OrderEventFilter,
55
+ ): boolean {
56
+ if (!filter) {
57
+ return true;
58
+ }
59
+
60
+ if (filter.accountId && event.accountId !== filter.accountId) {
61
+ return false;
62
+ }
63
+
64
+ if (filter.exchange && event.exchange !== filter.exchange) {
65
+ return false;
66
+ }
67
+
68
+ if (filter.symbol && event.symbol !== filter.symbol) {
69
+ return false;
70
+ }
71
+
72
+ return true;
73
+ }
74
+
75
+ export function matchesHealthFilter(
76
+ event: HealthEvent,
77
+ filter?: HealthEventFilter,
78
+ ): boolean {
79
+ if (!filter) {
80
+ return true;
81
+ }
82
+
83
+ if (filter.scope) {
84
+ const actualScope =
85
+ event.type === "client.status_changed"
86
+ ? "client"
87
+ : event.type === "market.status_changed"
88
+ ? "market"
89
+ : event.type === "account.status_changed"
90
+ ? "account"
91
+ : "order";
92
+
93
+ if (actualScope !== filter.scope) {
94
+ return false;
95
+ }
96
+ }
97
+
98
+ if (
99
+ "exchange" in event &&
100
+ filter.exchange &&
101
+ event.exchange !== filter.exchange
102
+ ) {
103
+ return false;
104
+ }
105
+
106
+ if (
107
+ "accountId" in event &&
108
+ filter.accountId &&
109
+ event.accountId !== filter.accountId
110
+ ) {
111
+ return false;
112
+ }
113
+
114
+ if ("symbol" in event && filter.symbol && event.symbol !== filter.symbol) {
115
+ return false;
116
+ }
117
+
118
+ return true;
119
+ }