@bizjaya/finclient 0.1.1 → 0.2.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.
- package/README.md +57 -22
- package/dist/index.cjs +372 -160
- package/dist/index.d.cts +3299 -657
- package/dist/index.d.ts +3299 -657
- package/dist/index.js +371 -159
- package/dist/webhook.cjs +49 -0
- package/dist/webhook.d.cts +11 -0
- package/dist/webhook.d.ts +11 -0
- package/dist/webhook.js +24 -0
- package/package.json +19 -8
package/dist/index.js
CHANGED
|
@@ -20,9 +20,6 @@ async function* readAllChanges(readPage, cursor, signal) {
|
|
|
20
20
|
}
|
|
21
21
|
}
|
|
22
22
|
|
|
23
|
-
// src/http-transport.ts
|
|
24
|
-
import { randomUUID } from "crypto";
|
|
25
|
-
|
|
26
23
|
// src/api-error.ts
|
|
27
24
|
var FinApiError = class extends Error {
|
|
28
25
|
/** The HTTP status code returned by the API, or 0 when no response arrived (timeout or network failure). */
|
|
@@ -46,20 +43,48 @@ var FinApiError = class extends Error {
|
|
|
46
43
|
}
|
|
47
44
|
};
|
|
48
45
|
|
|
46
|
+
// src/sse.ts
|
|
47
|
+
var FinSseParser = class {
|
|
48
|
+
eventName = null;
|
|
49
|
+
data = [];
|
|
50
|
+
/** Adds one line (without its line break). Returns the event the line completed, if any. */
|
|
51
|
+
add(line) {
|
|
52
|
+
if (!line) return this.flush();
|
|
53
|
+
if (line.startsWith(":")) return null;
|
|
54
|
+
const separator = line.indexOf(":");
|
|
55
|
+
const field = separator < 0 ? line : line.slice(0, separator);
|
|
56
|
+
const value = separator < 0 ? "" : line.slice(separator + 1).replace(/^ /, "");
|
|
57
|
+
if (field === "event") this.eventName = value;
|
|
58
|
+
else if (field === "data") this.data.push(value);
|
|
59
|
+
return null;
|
|
60
|
+
}
|
|
61
|
+
/** Ends the stream: an event still being read is returned when it carries data. */
|
|
62
|
+
complete() {
|
|
63
|
+
return this.data.length ? this.flush() : null;
|
|
64
|
+
}
|
|
65
|
+
flush() {
|
|
66
|
+
const item = this.data.length ? { event: this.eventName ?? "message", data: this.data.join("\n") } : null;
|
|
67
|
+
this.eventName = null;
|
|
68
|
+
this.data = [];
|
|
69
|
+
return item;
|
|
70
|
+
}
|
|
71
|
+
};
|
|
72
|
+
|
|
49
73
|
// src/http-transport.ts
|
|
50
74
|
var retryableStatuses = /* @__PURE__ */ new Set([429, 502, 503, 504]);
|
|
51
75
|
var writeMethods = /* @__PURE__ */ new Set(["POST", "PUT", "PATCH", "DELETE"]);
|
|
52
76
|
var FinHttpTransport = class {
|
|
53
77
|
apiKey;
|
|
54
78
|
baseUrl;
|
|
79
|
+
credentials;
|
|
55
80
|
timeoutMs;
|
|
56
81
|
maxRetries;
|
|
57
82
|
fetcher;
|
|
58
83
|
delay;
|
|
59
84
|
random;
|
|
60
85
|
constructor(options) {
|
|
61
|
-
|
|
62
|
-
this.apiKey
|
|
86
|
+
this.apiKey = options.apiKey?.trim() ?? "";
|
|
87
|
+
if (!this.apiKey && !options.fetch) throw new TypeError("apiKey is required unless a custom fetch authenticates the member session.");
|
|
63
88
|
this.baseUrl = (options.baseUrl ?? "https://api.fincopy.io").replace(/\/+$/, "");
|
|
64
89
|
const base = new URL(this.baseUrl);
|
|
65
90
|
if (!/^https?:$/.test(base.protocol) || base.search || base.hash) {
|
|
@@ -70,6 +95,7 @@ var FinHttpTransport = class {
|
|
|
70
95
|
if (!Number.isFinite(this.timeoutMs) || this.timeoutMs <= 0) throw new RangeError("timeoutMs must be positive.");
|
|
71
96
|
if (!Number.isInteger(this.maxRetries) || this.maxRetries < 0) throw new RangeError("maxRetries cannot be negative.");
|
|
72
97
|
this.fetcher = options.fetch ?? globalThis.fetch.bind(globalThis);
|
|
98
|
+
this.credentials = options.credentials ?? (this.apiKey ? "same-origin" : "include");
|
|
73
99
|
this.delay = options.retryDelay ?? ((milliseconds, signal) => new Promise((resolve, reject) => {
|
|
74
100
|
if (signal?.aborted) {
|
|
75
101
|
reject(signal.reason);
|
|
@@ -112,14 +138,16 @@ var FinHttpTransport = class {
|
|
|
112
138
|
let response;
|
|
113
139
|
let raw;
|
|
114
140
|
try {
|
|
115
|
-
const headers = new Headers({
|
|
141
|
+
const headers = new Headers({ Accept: "application/json" });
|
|
142
|
+
if (this.apiKey) headers.set("X-Api-Key", this.apiKey);
|
|
116
143
|
if (bodyText !== void 0) headers.set("Content-Type", "application/json");
|
|
117
144
|
if (idempotencyKey !== void 0) headers.set("Idempotency-Key", idempotencyKey);
|
|
118
145
|
response = await this.fetcher(url, {
|
|
119
146
|
method: verb,
|
|
120
147
|
headers,
|
|
121
148
|
body: bodyText,
|
|
122
|
-
signal: controller.signal
|
|
149
|
+
signal: controller.signal,
|
|
150
|
+
credentials: this.credentials
|
|
123
151
|
});
|
|
124
152
|
raw = await response.text();
|
|
125
153
|
} catch (error) {
|
|
@@ -184,6 +212,79 @@ var FinHttpTransport = class {
|
|
|
184
212
|
chksum: envelope.chksum ?? null
|
|
185
213
|
};
|
|
186
214
|
}
|
|
215
|
+
/** Reads the event stream until the server closes it or the caller aborts. It does not reconnect. */
|
|
216
|
+
async *requestEvents(path, requestOptions = {}) {
|
|
217
|
+
const url = new URL(path.replace(/^\//, ""), `${this.baseUrl}/`);
|
|
218
|
+
const headers = new Headers({ Accept: "text/event-stream" });
|
|
219
|
+
if (this.apiKey) headers.set("X-Api-Key", this.apiKey);
|
|
220
|
+
let response;
|
|
221
|
+
try {
|
|
222
|
+
response = await this.fetcher(url, {
|
|
223
|
+
method: "GET",
|
|
224
|
+
headers,
|
|
225
|
+
signal: requestOptions.signal,
|
|
226
|
+
credentials: this.credentials
|
|
227
|
+
});
|
|
228
|
+
} catch (error) {
|
|
229
|
+
if (requestOptions.signal?.aborted) throw error;
|
|
230
|
+
throw new FinApiError(0, "The FinCopy event stream could not be opened.", null, [], { errorCode: "network", cause: error });
|
|
231
|
+
}
|
|
232
|
+
if (!response.ok) {
|
|
233
|
+
const raw = await response.text();
|
|
234
|
+
let envelope = null;
|
|
235
|
+
try {
|
|
236
|
+
envelope = raw ? JSON.parse(raw) : null;
|
|
237
|
+
} catch {
|
|
238
|
+
}
|
|
239
|
+
const fallback = `The FinCopy API request failed with HTTP ${response.status}.`;
|
|
240
|
+
throw new FinApiError(
|
|
241
|
+
response.status,
|
|
242
|
+
envelope?.message || fallback,
|
|
243
|
+
envelope?.code ?? null,
|
|
244
|
+
errorsOf(envelope?.errors),
|
|
245
|
+
{
|
|
246
|
+
errorCode: response.status === 429 ? "rate_limited" : null,
|
|
247
|
+
retryAfterMs: retryAfterMsOf(response.headers.get("Retry-After"))
|
|
248
|
+
}
|
|
249
|
+
);
|
|
250
|
+
}
|
|
251
|
+
if (!response.body) throw new FinApiError(response.status, "The FinCopy API returned an empty event stream.", null, [], { errorCode: "empty_response" });
|
|
252
|
+
const reader = response.body.getReader();
|
|
253
|
+
const decoder = new TextDecoder();
|
|
254
|
+
const parser = new FinSseParser();
|
|
255
|
+
let buffer = "";
|
|
256
|
+
try {
|
|
257
|
+
while (true) {
|
|
258
|
+
if (requestOptions.signal?.aborted) throw requestOptions.signal.reason;
|
|
259
|
+
let next;
|
|
260
|
+
try {
|
|
261
|
+
next = await reader.read();
|
|
262
|
+
} catch (error) {
|
|
263
|
+
if (requestOptions.signal?.aborted) throw error;
|
|
264
|
+
throw new FinApiError(0, "The FinCopy event stream was interrupted.", null, [], { errorCode: "network", cause: error });
|
|
265
|
+
}
|
|
266
|
+
buffer += decoder.decode(next.value, { stream: !next.done });
|
|
267
|
+
const lines = buffer.split(/\r?\n/);
|
|
268
|
+
buffer = lines.pop() ?? "";
|
|
269
|
+
for (const line of lines) {
|
|
270
|
+
const item = parser.add(line);
|
|
271
|
+
if (item) yield item;
|
|
272
|
+
}
|
|
273
|
+
if (next.done) {
|
|
274
|
+
if (buffer) {
|
|
275
|
+
const item = parser.add(buffer);
|
|
276
|
+
if (item) yield item;
|
|
277
|
+
}
|
|
278
|
+
const final = parser.complete();
|
|
279
|
+
if (final) yield final;
|
|
280
|
+
return;
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
} finally {
|
|
284
|
+
await reader.cancel().catch(() => void 0);
|
|
285
|
+
reader.releaseLock();
|
|
286
|
+
}
|
|
287
|
+
}
|
|
187
288
|
backoff(retry) {
|
|
188
289
|
return Math.min(maxBackoffMs, 250 * 2 ** retry * (0.75 + this.random() * 0.5));
|
|
189
290
|
}
|
|
@@ -211,128 +312,194 @@ function errorsOf(errors) {
|
|
|
211
312
|
}
|
|
212
313
|
function idempotencyKeyOf(supplied) {
|
|
213
314
|
const key = supplied?.trim();
|
|
214
|
-
if (!key) return
|
|
315
|
+
if (!key) return newIdempotencyKey();
|
|
215
316
|
if (key.length > 120 || /[\u0000-\u001f\u007f]/.test(key)) {
|
|
216
317
|
throw new TypeError("idempotencyKey must be 1-120 characters without control characters.");
|
|
217
318
|
}
|
|
218
319
|
return key;
|
|
219
320
|
}
|
|
321
|
+
function newIdempotencyKey() {
|
|
322
|
+
const webCrypto = globalThis.crypto;
|
|
323
|
+
if (typeof webCrypto?.randomUUID === "function") return webCrypto.randomUUID();
|
|
324
|
+
const bytes = new Uint8Array(16);
|
|
325
|
+
if (typeof webCrypto?.getRandomValues === "function") webCrypto.getRandomValues(bytes);
|
|
326
|
+
else for (let i = 0; i < bytes.length; i++) bytes[i] = Math.floor(Math.random() * 256);
|
|
327
|
+
bytes[6] = bytes[6] & 15 | 64;
|
|
328
|
+
bytes[8] = bytes[8] & 63 | 128;
|
|
329
|
+
const hex = Array.from(bytes, (b) => b.toString(16).padStart(2, "0")).join("");
|
|
330
|
+
return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20)}`;
|
|
331
|
+
}
|
|
220
332
|
|
|
221
333
|
// src/resources.ts
|
|
222
334
|
function pageQuery(query = {}) {
|
|
223
335
|
const params = new URLSearchParams();
|
|
224
|
-
const
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
if (value2 !== void 0) params.append(key, String(value2));
|
|
336
|
+
for (const key of ["PageNo", "PageSize"]) {
|
|
337
|
+
const value = query[key];
|
|
338
|
+
if (value !== void 0) params.append(key, String(value));
|
|
228
339
|
}
|
|
229
340
|
if (query.Search !== void 0) params.append("search", query.Search);
|
|
230
|
-
for (const key of ["SortBy", "SrchBy"
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
341
|
+
for (const key of ["SortBy", "SrchBy"])
|
|
342
|
+
(query[key] ?? []).forEach((clause, index) => clause.forEach((value, part) => params.append(`${key}[${index}][${part}]`, value)));
|
|
343
|
+
for (const value of query.Include ?? []) params.append("Include", value);
|
|
344
|
+
return params.toString();
|
|
345
|
+
}
|
|
346
|
+
function queryPath(path, page, filters = {}) {
|
|
347
|
+
const params = new URLSearchParams(pageQuery(page));
|
|
348
|
+
for (const [key, value] of Object.entries(filters)) if (value !== void 0 && value !== null) params.set(key, String(value));
|
|
349
|
+
const suffix = params.toString();
|
|
350
|
+
return suffix ? `${path}?${suffix}` : path;
|
|
235
351
|
}
|
|
236
|
-
function accountPath(
|
|
237
|
-
if (!
|
|
238
|
-
return `v1/accounts/${
|
|
352
|
+
function accountPath(acno) {
|
|
353
|
+
if (!Number.isSafeInteger(acno) || acno <= 0) throw new RangeError("acno must be a positive safe integer.");
|
|
354
|
+
return `v1/fxs/accounts/${acno}`;
|
|
239
355
|
}
|
|
240
|
-
function numericId(value, name) {
|
|
241
|
-
if (!Number.isSafeInteger(value) || value < 0) throw new RangeError(`${name}
|
|
356
|
+
function numericId(value, name, allowZero = true) {
|
|
357
|
+
if (!Number.isSafeInteger(value) || value < (allowZero ? 0 : 1)) throw new RangeError(`${name} is out of range.`);
|
|
242
358
|
return String(value);
|
|
243
359
|
}
|
|
244
|
-
function
|
|
360
|
+
function dateValue(value) {
|
|
245
361
|
return value instanceof Date ? value.toISOString() : value;
|
|
246
362
|
}
|
|
363
|
+
function dateFilters(query) {
|
|
364
|
+
return {
|
|
365
|
+
fromDt: query.fromDt === void 0 ? void 0 : dateValue(query.fromDt),
|
|
366
|
+
toDt: query.toDt === void 0 ? void 0 : dateValue(query.toDt)
|
|
367
|
+
};
|
|
368
|
+
}
|
|
247
369
|
var AccountsResource = class {
|
|
248
370
|
constructor(http) {
|
|
249
371
|
this.http = http;
|
|
250
372
|
}
|
|
251
373
|
http;
|
|
252
|
-
/** Lists
|
|
253
|
-
list(options = {}) {
|
|
254
|
-
return this.http.
|
|
374
|
+
/** Lists reachable accounts, optionally filtered by external reference. */
|
|
375
|
+
list(page = {}, externalRef, options = {}) {
|
|
376
|
+
return this.http.requestPage("GET", queryPath("v1/fxs/accounts", page, { externalRef }), options);
|
|
255
377
|
}
|
|
256
|
-
/** Adds a hosted account
|
|
378
|
+
/** Adds a hosted account. */
|
|
257
379
|
make(command, options = {}) {
|
|
258
|
-
return this.http.request("POST", "v1/accounts
|
|
380
|
+
return this.http.request("POST", "v1/fxs/accounts", command, options);
|
|
381
|
+
}
|
|
382
|
+
/** Creates a demo account. */
|
|
383
|
+
makeDemo(command, options = {}) {
|
|
384
|
+
return this.http.request("POST", "v1/fxs/accounts/demo", command, options);
|
|
259
385
|
}
|
|
260
|
-
/** Reads
|
|
261
|
-
|
|
262
|
-
return this.http.request("GET",
|
|
386
|
+
/** Reads demo availability for one platform. */
|
|
387
|
+
demoAvailability(platform, options = {}) {
|
|
388
|
+
return this.http.request("GET", queryPath("v1/fxs/accounts/demo-availability", void 0, { platform }), void 0, options);
|
|
389
|
+
}
|
|
390
|
+
/** Reads one account by ACNO. */
|
|
391
|
+
get(acno, options = {}) {
|
|
392
|
+
return this.http.request("GET", accountPath(acno), void 0, options);
|
|
263
393
|
}
|
|
264
394
|
/** Sets or clears the account's external reference. */
|
|
265
|
-
setExternalRef(
|
|
266
|
-
return this.http.request("PUT", `${accountPath(
|
|
395
|
+
setExternalRef(acno, command, options = {}) {
|
|
396
|
+
return this.http.request("PUT", `${accountPath(acno)}/external-ref`, command, options);
|
|
397
|
+
}
|
|
398
|
+
/** Reads account settings. */
|
|
399
|
+
getSettings(acno, options = {}) {
|
|
400
|
+
return this.http.request("GET", `${accountPath(acno)}/settings`, void 0, options);
|
|
401
|
+
}
|
|
402
|
+
/** Changes account settings. */
|
|
403
|
+
patchSettings(acno, command, options = {}) {
|
|
404
|
+
return this.http.request("PATCH", `${accountPath(acno)}/settings`, command, options);
|
|
405
|
+
}
|
|
406
|
+
/** Lists pairs for one account. */
|
|
407
|
+
pairs(acno, page = {}, search, options = {}) {
|
|
408
|
+
return this.http.requestPage("GET", queryPath(`${accountPath(acno)}/pairs`, page, { search }), options);
|
|
409
|
+
}
|
|
410
|
+
/** Reads one pair by PRNO. */
|
|
411
|
+
pair(acno, prno, options = {}) {
|
|
412
|
+
return this.http.request("GET", `${accountPath(acno)}/pairs/${numericId(prno, "prno")}`, void 0, options);
|
|
267
413
|
}
|
|
268
|
-
/**
|
|
269
|
-
|
|
270
|
-
return this.http.request("
|
|
414
|
+
/** Changes one pair's settings. */
|
|
415
|
+
patchPair(acno, prno, command, options = {}) {
|
|
416
|
+
return this.http.request("PATCH", `${accountPath(acno)}/pairs/${numericId(prno, "prno")}`, command, options);
|
|
271
417
|
}
|
|
272
|
-
/**
|
|
273
|
-
|
|
274
|
-
return this.http.request("
|
|
418
|
+
/** Reads the broker symbols and enabled list. */
|
|
419
|
+
symbols(acno, options = {}) {
|
|
420
|
+
return this.http.request("GET", `${accountPath(acno)}/symbols`, void 0, options);
|
|
275
421
|
}
|
|
276
|
-
/**
|
|
277
|
-
|
|
278
|
-
return this.http.
|
|
422
|
+
/** Adds or removes enabled symbols. */
|
|
423
|
+
editSymbols(acno, command, options = {}) {
|
|
424
|
+
return this.http.request("PUT", `${accountPath(acno)}/symbols`, command, options);
|
|
279
425
|
}
|
|
280
|
-
/** Reads
|
|
281
|
-
|
|
282
|
-
|
|
426
|
+
/** Reads a symbol's broker specification and latest quote. */
|
|
427
|
+
quote(acno, symbol, options = {}) {
|
|
428
|
+
if (!symbol.trim()) throw new TypeError("symbol is required.");
|
|
429
|
+
return this.http.request("GET", `${accountPath(acno)}/quotes/${encodeURIComponent(symbol)}`, void 0, options);
|
|
430
|
+
}
|
|
431
|
+
/** Lists live orders for one account. */
|
|
432
|
+
orders(acno, page = {}, search, options = {}) {
|
|
433
|
+
return this.http.requestPage("GET", queryPath(`${accountPath(acno)}/orders`, page, { search }), options);
|
|
434
|
+
}
|
|
435
|
+
/** Reads one live or settled order. */
|
|
436
|
+
order(acno, oid, options = {}) {
|
|
437
|
+
return this.http.request("GET", `${accountPath(acno)}/orders/${numericId(oid, "oid")}`, void 0, options);
|
|
283
438
|
}
|
|
284
|
-
/**
|
|
285
|
-
|
|
286
|
-
return this.http.
|
|
439
|
+
/** Lists settled orders. */
|
|
440
|
+
history(acno, page = {}, search, options = {}) {
|
|
441
|
+
return this.http.requestPage("GET", queryPath(`${accountPath(acno)}/history`, page, { search }), options);
|
|
442
|
+
}
|
|
443
|
+
/** Reads one settled order by OID. */
|
|
444
|
+
historyItem(acno, oid, options = {}) {
|
|
445
|
+
return this.http.request("GET", `${accountPath(acno)}/history/${numericId(oid, "oid")}`, void 0, options);
|
|
287
446
|
}
|
|
288
447
|
/** Lists HYBRID DCA proposals waiting for an answer. */
|
|
289
|
-
dcaQueue(
|
|
290
|
-
return this.http.request("GET", `${accountPath(
|
|
448
|
+
dcaQueue(acno, options = {}) {
|
|
449
|
+
return this.http.request("GET", `${accountPath(acno)}/dca`, void 0, options);
|
|
291
450
|
}
|
|
292
|
-
/**
|
|
293
|
-
|
|
294
|
-
return this.http.request("GET", `${accountPath(
|
|
451
|
+
/** Lists daily performance rows. */
|
|
452
|
+
performance(acno, query = {}) {
|
|
453
|
+
return this.http.request("GET", queryPath(`${accountPath(acno)}/performance`, void 0, dateFilters(query)), void 0, query);
|
|
295
454
|
}
|
|
296
|
-
/**
|
|
297
|
-
|
|
298
|
-
return this.http.request("
|
|
455
|
+
/** Reads the account's performance window. */
|
|
456
|
+
performanceWindow(acno, options = {}) {
|
|
457
|
+
return this.http.request("GET", `${accountPath(acno)}/performance/window`, void 0, options);
|
|
299
458
|
}
|
|
300
|
-
/** Reads
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
}
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
}
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
}
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
}
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
}
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
}
|
|
334
|
-
|
|
335
|
-
|
|
459
|
+
/** Reads performance bars for an optional UTC date range. */
|
|
460
|
+
performanceBars(acno, query = {}) {
|
|
461
|
+
return this.http.request("GET", queryPath(`${accountPath(acno)}/performance/bars`, void 0, dateFilters(query)), void 0, query);
|
|
462
|
+
}
|
|
463
|
+
/** Lists paged account activity. */
|
|
464
|
+
activity(acno, page = {}, search, options = {}) {
|
|
465
|
+
return this.http.requestPage("GET", queryPath(`${accountPath(acno)}/activity`, page, { search }), options);
|
|
466
|
+
}
|
|
467
|
+
/** Previews applying a strategy (the account owner's or a FinCopy preset): what each symbol would change. Nothing changes. */
|
|
468
|
+
previewStrategy(acno, command, options = {}) {
|
|
469
|
+
return this.http.request("POST", `${accountPath(acno)}/strategy/preview`, command, options);
|
|
470
|
+
}
|
|
471
|
+
/** Applies a strategy to the account and its symbols; a side holding orders keeps what it must keep. */
|
|
472
|
+
applyStrategy(acno, command, options = {}) {
|
|
473
|
+
return this.http.request("POST", `${accountPath(acno)}/strategy/apply`, command, options);
|
|
474
|
+
}
|
|
475
|
+
/** Lists recent terminal operations. */
|
|
476
|
+
operations(acno, page = {}, options = {}) {
|
|
477
|
+
return this.http.requestPage("GET", queryPath(`${accountPath(acno)}/operations`, page), options);
|
|
478
|
+
}
|
|
479
|
+
/** Queues a broker-password change. */
|
|
480
|
+
changePassword(acno, command, options = {}) {
|
|
481
|
+
return this.http.request("POST", `${accountPath(acno)}/password`, command, options);
|
|
482
|
+
}
|
|
483
|
+
/** Queues a terminal start. */
|
|
484
|
+
start(acno, options = {}) {
|
|
485
|
+
return this.operation(acno, "start", options);
|
|
486
|
+
}
|
|
487
|
+
/** Queues a terminal stop. */
|
|
488
|
+
stop(acno, options = {}) {
|
|
489
|
+
return this.operation(acno, "stop", options);
|
|
490
|
+
}
|
|
491
|
+
/** Queues a terminal restart. */
|
|
492
|
+
restart(acno, options = {}) {
|
|
493
|
+
return this.operation(acno, "restart", options);
|
|
494
|
+
}
|
|
495
|
+
remove(acno, commandOrOptions = {}, options = {}) {
|
|
496
|
+
const hasCommand = Object.prototype.hasOwnProperty.call(commandOrOptions, "TrPin");
|
|
497
|
+
const command = hasCommand ? commandOrOptions : { TrPin: null };
|
|
498
|
+
const requestOptions = hasCommand ? options : commandOrOptions;
|
|
499
|
+
return this.http.request("DELETE", accountPath(acno), command, requestOptions);
|
|
500
|
+
}
|
|
501
|
+
operation(acno, action, options) {
|
|
502
|
+
return this.http.request("POST", `${accountPath(acno)}/${action}`, void 0, options);
|
|
336
503
|
}
|
|
337
504
|
};
|
|
338
505
|
var OrdersResource = class {
|
|
@@ -340,45 +507,110 @@ var OrdersResource = class {
|
|
|
340
507
|
this.http = http;
|
|
341
508
|
}
|
|
342
509
|
http;
|
|
343
|
-
/**
|
|
344
|
-
|
|
345
|
-
return this.http.
|
|
510
|
+
/** Preflights a trade request. */
|
|
511
|
+
preInfo(acno, command, options = {}) {
|
|
512
|
+
return this.http.request("POST", `${accountPath(acno)}/trade/preinfo`, command, options);
|
|
346
513
|
}
|
|
347
|
-
/**
|
|
348
|
-
|
|
349
|
-
return this.http.
|
|
514
|
+
/** Sends one trade action. */
|
|
515
|
+
make(acno, command, options = {}) {
|
|
516
|
+
return this.http.request("POST", `${accountPath(acno)}/trade`, command, options);
|
|
350
517
|
}
|
|
351
|
-
/**
|
|
352
|
-
|
|
353
|
-
return this.http.request("
|
|
518
|
+
/** Sends paired BUY and SELL actions. */
|
|
519
|
+
hedge(acno, command, options = {}) {
|
|
520
|
+
return this.http.request("POST", `${accountPath(acno)}/trade/hedge`, command, options);
|
|
354
521
|
}
|
|
355
|
-
/**
|
|
356
|
-
|
|
357
|
-
return this.http.request("
|
|
522
|
+
/** Modifies a trade or pending order. */
|
|
523
|
+
modify(acno, command, options = {}) {
|
|
524
|
+
return this.http.request("POST", `${accountPath(acno)}/trade/modify`, command, options);
|
|
358
525
|
}
|
|
359
|
-
/**
|
|
360
|
-
|
|
361
|
-
return this.http.request("POST", `${accountPath(
|
|
526
|
+
/** Cancels an order that has not reached the terminal. */
|
|
527
|
+
cancelWaiting(acno, command, options = {}) {
|
|
528
|
+
return this.http.request("POST", `${accountPath(acno)}/trade/cancel`, command, options);
|
|
362
529
|
}
|
|
363
|
-
/** Accepts or rejects one
|
|
364
|
-
answerDca(
|
|
365
|
-
return this.http.request("POST", `${accountPath(
|
|
530
|
+
/** Accepts or rejects one DCA proposal. */
|
|
531
|
+
answerDca(acno, oid, command, options = {}) {
|
|
532
|
+
return this.http.request("POST", `${accountPath(acno)}/dca/${numericId(oid, "oid")}`, command, options);
|
|
366
533
|
}
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
534
|
+
};
|
|
535
|
+
var FxResource = class {
|
|
536
|
+
constructor(http) {
|
|
537
|
+
this.http = http;
|
|
370
538
|
}
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
539
|
+
http;
|
|
540
|
+
/** Lists pairs across reachable accounts. */
|
|
541
|
+
pairs(query = {}, options = {}) {
|
|
542
|
+
return this.http.requestPage("GET", listPath("v1/fxs/pairs", query), options);
|
|
543
|
+
}
|
|
544
|
+
/** Lists open orders across reachable accounts. */
|
|
545
|
+
orders(query = {}, options = {}) {
|
|
546
|
+
return this.http.requestPage("GET", listPath("v1/fxs/orders", query), options);
|
|
547
|
+
}
|
|
548
|
+
/** Lists settled orders across reachable accounts. */
|
|
549
|
+
history(query = {}, options = {}) {
|
|
550
|
+
return this.http.requestPage("GET", listPath("v1/fxs/history", query), options);
|
|
551
|
+
}
|
|
552
|
+
/** Lists activity across reachable accounts. */
|
|
553
|
+
activity(query = {}, options = {}) {
|
|
554
|
+
return this.http.requestPage("GET", listPath("v1/fxs/activity", query), options);
|
|
555
|
+
}
|
|
556
|
+
/** HYBRID DCA proposals waiting for an answer across reachable accounts, newest first. */
|
|
557
|
+
dca(acno, options = {}) {
|
|
558
|
+
return this.http.request("GET", queryPath("v1/fxs/dca", void 0, { acno }), void 0, options);
|
|
559
|
+
}
|
|
560
|
+
/** The newest terminal operations across reachable accounts. */
|
|
561
|
+
operations(page = {}, acno, options = {}) {
|
|
562
|
+
return this.http.requestPage("GET", queryPath("v1/fxs/operations", page, { acno }), options);
|
|
563
|
+
}
|
|
564
|
+
/** The last 30 days by currency book across reachable accounts. */
|
|
565
|
+
performanceWindow(query = {}) {
|
|
566
|
+
return this.http.request("GET", queryPath("v1/fxs/performance/window", void 0, { acno: query.acno, demo: query.demo }), void 0, query);
|
|
567
|
+
}
|
|
568
|
+
/** Daily performance bars across reachable accounts for an optional UTC date range. */
|
|
569
|
+
performanceBars(query = {}) {
|
|
570
|
+
return this.http.request("GET", queryPath(
|
|
571
|
+
"v1/fxs/performance/bars",
|
|
572
|
+
void 0,
|
|
573
|
+
{ acno: query.acno, demo: query.demo, ...dateFilters(query) }
|
|
574
|
+
), void 0, query);
|
|
575
|
+
}
|
|
576
|
+
/** Lists the strategies you can apply: your own and FinCopy's presets; with `acno`, that account owner's. */
|
|
577
|
+
strategies(acno, options = {}) {
|
|
578
|
+
return this.http.request("GET", queryPath("v1/fxs/strategies", void 0, { acno }), void 0, options);
|
|
579
|
+
}
|
|
580
|
+
/** Reads one strategy. */
|
|
581
|
+
strategy(isno, options = {}) {
|
|
582
|
+
return this.http.request("GET", strategyPath(isno), void 0, options);
|
|
583
|
+
}
|
|
584
|
+
/** Creates one of your own strategies. */
|
|
585
|
+
makeStrategy(command, options = {}) {
|
|
586
|
+
return this.http.request("POST", "v1/fxs/strategies", command, options);
|
|
587
|
+
}
|
|
588
|
+
/** Changes one of your own strategies; fields left unset keep their values. */
|
|
589
|
+
patchStrategy(isno, command, options = {}) {
|
|
590
|
+
return this.http.request("PATCH", strategyPath(isno), command, options);
|
|
591
|
+
}
|
|
592
|
+
/** Deletes one of your own strategies. */
|
|
593
|
+
deleteStrategy(isno, options = {}) {
|
|
594
|
+
return this.http.request("DELETE", strategyPath(isno), void 0, options);
|
|
595
|
+
}
|
|
596
|
+
/** Reads the signals shown for enabled symbols. */
|
|
597
|
+
signals(options = {}) {
|
|
598
|
+
return this.http.request("GET", "v1/fxs/signals", void 0, options);
|
|
599
|
+
}
|
|
600
|
+
/** Lists warning and error activity. */
|
|
601
|
+
alerts(page = {}, options = {}) {
|
|
602
|
+
return this.http.requestPage("GET", queryPath("v1/fxs/alerts", page), options);
|
|
374
603
|
}
|
|
375
|
-
/**
|
|
376
|
-
|
|
377
|
-
|
|
604
|
+
/**
|
|
605
|
+
* Opens the live event stream for reachable accounts. It does not reconnect: it ends when the server closes it (about
|
|
606
|
+
* every 30 minutes) or the connection breaks, so open it again in a loop, waiting a few seconds between attempts.
|
|
607
|
+
*/
|
|
608
|
+
stream(options = {}) {
|
|
609
|
+
return this.http.requestEvents("v1/fxs/stream", options);
|
|
378
610
|
}
|
|
379
|
-
/**
|
|
380
|
-
|
|
381
|
-
return this.http.request("
|
|
611
|
+
/** Searches broker servers available when adding an account. */
|
|
612
|
+
brokers(query, platform, demosOnly, options = {}) {
|
|
613
|
+
return this.http.request("GET", queryPath("v1/brokers", void 0, { Q: query, Platform: platform, DemosOnly: demosOnly }), void 0, options);
|
|
382
614
|
}
|
|
383
615
|
};
|
|
384
616
|
var ChangesResource = class {
|
|
@@ -389,18 +621,13 @@ var ChangesResource = class {
|
|
|
389
621
|
/** Reads one changes page using an optional cursor. */
|
|
390
622
|
get(cursor, take = 500, options = {}) {
|
|
391
623
|
if (!Number.isInteger(take) || take < 1 || take > 1e3) throw new RangeError("take must be between 1 and 1000.");
|
|
392
|
-
|
|
393
|
-
if (cursor) params.set("cursor", cursor);
|
|
394
|
-
return this.http.request("GET", `v1/accounts/changes?${params}`, void 0, options);
|
|
624
|
+
return this.http.request("GET", queryPath("v1/changes", void 0, { cursor, take }), void 0, options);
|
|
395
625
|
}
|
|
396
|
-
/**
|
|
397
|
-
* Streams the available changes page by page, following each returned cursor. Save each page's `Cursor` after handling
|
|
398
|
-
* its `Changes`, so a restart resumes from there.
|
|
399
|
-
*/
|
|
626
|
+
/** Reads changes page by page, following each cursor. */
|
|
400
627
|
readPages(cursor, take = 500, signal) {
|
|
401
628
|
return readChangePages((next, nextSignal) => this.page(next, take, nextSignal), cursor, signal);
|
|
402
629
|
}
|
|
403
|
-
/** Streams
|
|
630
|
+
/** Streams all available changes. Use readPages to retain a resume cursor. */
|
|
404
631
|
readAll(cursor, take = 500, signal) {
|
|
405
632
|
return readAllChanges((next, nextSignal) => this.page(next, take, nextSignal), cursor, signal);
|
|
406
633
|
}
|
|
@@ -416,21 +643,27 @@ var WebhooksResource = class {
|
|
|
416
643
|
http;
|
|
417
644
|
/** Reads this key's webhook configuration. */
|
|
418
645
|
get(options = {}) {
|
|
419
|
-
return this.http.request("GET", "v1/
|
|
646
|
+
return this.http.request("GET", "v1/webhook", void 0, options);
|
|
420
647
|
}
|
|
421
|
-
/** Sets the
|
|
648
|
+
/** Sets the webhook destination and returns its signing secret once. */
|
|
422
649
|
set(command, options = {}) {
|
|
423
|
-
return this.http.request("PUT", "v1/
|
|
650
|
+
return this.http.request("PUT", "v1/webhook", command, options);
|
|
424
651
|
}
|
|
425
652
|
/** Removes this key's webhook. */
|
|
426
653
|
delete(options = {}) {
|
|
427
|
-
return this.http.request("DELETE", "v1/
|
|
654
|
+
return this.http.request("DELETE", "v1/webhook", void 0, options);
|
|
428
655
|
}
|
|
429
|
-
/** Sends a signed test
|
|
656
|
+
/** Sends a signed test delivery. */
|
|
430
657
|
ping(options = {}) {
|
|
431
|
-
return this.http.request("POST", "v1/
|
|
658
|
+
return this.http.request("POST", "v1/webhook/ping", void 0, options);
|
|
432
659
|
}
|
|
433
660
|
};
|
|
661
|
+
function strategyPath(isno) {
|
|
662
|
+
return `v1/fxs/strategies/${numericId(isno, "isno")}`;
|
|
663
|
+
}
|
|
664
|
+
function listPath(path, query) {
|
|
665
|
+
return queryPath(path, query.page, { acno: query.acno, prno: query.prno, demo: query.demo, platform: query.platform, search: query.search });
|
|
666
|
+
}
|
|
434
667
|
|
|
435
668
|
// src/client.ts
|
|
436
669
|
var FinClient = class {
|
|
@@ -440,40 +673,19 @@ var FinClient = class {
|
|
|
440
673
|
this.copy = {
|
|
441
674
|
accounts: new AccountsResource(transport),
|
|
442
675
|
orders: new OrdersResource(transport),
|
|
676
|
+
fx: new FxResource(transport),
|
|
443
677
|
changes: new ChangesResource(transport),
|
|
444
678
|
webhooks: new WebhooksResource(transport)
|
|
445
679
|
};
|
|
446
680
|
}
|
|
447
681
|
};
|
|
448
|
-
|
|
449
|
-
// src/webhook.ts
|
|
450
|
-
import { createHmac, timingSafeEqual } from "crypto";
|
|
451
|
-
var FinWebhook = class {
|
|
452
|
-
static allowedSkewSeconds = 300;
|
|
453
|
-
/** Verify X-FinCopy-Signature over the timestamp and the exact raw request body. */
|
|
454
|
-
static verify(secret, timestamp, body, signature) {
|
|
455
|
-
if (!secret || !signature) return false;
|
|
456
|
-
const timestampText = String(timestamp);
|
|
457
|
-
if (!/^\d+$/.test(timestampText)) return false;
|
|
458
|
-
const seconds = Number(timestampText);
|
|
459
|
-
if (!Number.isSafeInteger(seconds)) return false;
|
|
460
|
-
const now = Math.floor(Date.now() / 1e3);
|
|
461
|
-
if (now - seconds > this.allowedSkewSeconds || seconds - now > this.allowedSkewSeconds) return false;
|
|
462
|
-
const suppliedHex = signature.startsWith("sha256=") ? signature.slice(7) : signature;
|
|
463
|
-
if (!/^[a-fA-F0-9]{64}$/.test(suppliedHex)) return false;
|
|
464
|
-
const supplied = Buffer.from(suppliedHex, "hex");
|
|
465
|
-
const signedBody = Buffer.isBuffer(body) ? body : Buffer.from(body);
|
|
466
|
-
const expected = createHmac("sha256", Buffer.from(secret, "utf8")).update(Buffer.from(`${timestampText}.`, "utf8")).update(signedBody).digest();
|
|
467
|
-
return supplied.length === expected.length && timingSafeEqual(supplied, expected);
|
|
468
|
-
}
|
|
469
|
-
};
|
|
470
682
|
export {
|
|
471
683
|
AccountsResource,
|
|
472
684
|
ChangesResource,
|
|
473
685
|
FinApiError,
|
|
474
686
|
FinClient,
|
|
475
687
|
FinHttpTransport,
|
|
476
|
-
|
|
688
|
+
FxResource,
|
|
477
689
|
OrdersResource,
|
|
478
690
|
WebhooksResource,
|
|
479
691
|
readAllChanges,
|