@openmarketsai/order-sheet 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/LICENSE +201 -0
- package/NOTICE +5 -0
- package/README.md +183 -0
- package/dist/auth.d.ts +24 -0
- package/dist/auth.js +36 -0
- package/dist/auth.js.map +1 -0
- package/dist/client.d.ts +158 -0
- package/dist/client.js +253 -0
- package/dist/client.js.map +1 -0
- package/dist/errors.d.ts +34 -0
- package/dist/errors.js +34 -0
- package/dist/errors.js.map +1 -0
- package/dist/generated/contract.d.ts +28 -0
- package/dist/generated/contract.js +176 -0
- package/dist/generated/contract.js.map +1 -0
- package/dist/http.d.ts +19 -0
- package/dist/http.js +58 -0
- package/dist/http.js.map +1 -0
- package/dist/index.cjs +1742 -0
- package/dist/index.d.ts +41 -0
- package/dist/index.js +31 -0
- package/dist/index.js.map +1 -0
- package/dist/math.d.ts +22 -0
- package/dist/math.js +55 -0
- package/dist/math.js.map +1 -0
- package/dist/messages.d.ts +21 -0
- package/dist/messages.js +97 -0
- package/dist/messages.js.map +1 -0
- package/dist/odds.d.ts +12 -0
- package/dist/odds.js +33 -0
- package/dist/odds.js.map +1 -0
- package/dist/quote.d.ts +20 -0
- package/dist/quote.js +117 -0
- package/dist/quote.js.map +1 -0
- package/dist/react.cjs +2137 -0
- package/dist/react.d.ts +157 -0
- package/dist/react.js +297 -0
- package/dist/react.js.map +1 -0
- package/dist/sheet.d.ts +77 -0
- package/dist/sheet.js +648 -0
- package/dist/sheet.js.map +1 -0
- package/dist/stream.d.ts +71 -0
- package/dist/stream.js +160 -0
- package/dist/stream.js.map +1 -0
- package/dist/types.d.ts +287 -0
- package/dist/types.js +2 -0
- package/dist/types.js.map +1 -0
- package/dist/ui/styles.css +284 -0
- package/dist/ui/theme.d.ts +108 -0
- package/dist/ui/theme.js +180 -0
- package/dist/ui/theme.js.map +1 -0
- package/dist/ui.cjs +2606 -0
- package/dist/ui.d.ts +124 -0
- package/dist/ui.js +154 -0
- package/dist/ui.js.map +1 -0
- package/dist/validate.d.ts +20 -0
- package/dist/validate.js +126 -0
- package/dist/validate.js.map +1 -0
- package/dist/venues.d.ts +19 -0
- package/dist/venues.js +103 -0
- package/dist/venues.js.map +1 -0
- package/dist/wire.d.ts +382 -0
- package/dist/wire.js +2 -0
- package/dist/wire.js.map +1 -0
- package/package.json +78 -0
package/dist/react.cjs
ADDED
|
@@ -0,0 +1,2137 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
|
+
var __export = (target, all) => {
|
|
7
|
+
for (var name in all)
|
|
8
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
9
|
+
};
|
|
10
|
+
var __copyProps = (to, from, except, desc) => {
|
|
11
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
12
|
+
for (let key of __getOwnPropNames(from))
|
|
13
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
14
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
15
|
+
}
|
|
16
|
+
return to;
|
|
17
|
+
};
|
|
18
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
19
|
+
|
|
20
|
+
// src/react.tsx
|
|
21
|
+
var react_exports = {};
|
|
22
|
+
__export(react_exports, {
|
|
23
|
+
CLIENT_CODE_SPECS: () => CLIENT_CODE_SPECS,
|
|
24
|
+
CONSTRAINTS_VERSION: () => CONSTRAINTS_VERSION,
|
|
25
|
+
DEFAULT_API_BASE: () => DEFAULT_API_BASE,
|
|
26
|
+
ISSUE_CODES: () => ISSUE_CODES,
|
|
27
|
+
ISSUE_DEFAULT_FIX: () => ISSUE_DEFAULT_FIX,
|
|
28
|
+
ISSUE_FIXES: () => ISSUE_FIXES,
|
|
29
|
+
ISSUE_SEVERITY: () => ISSUE_SEVERITY,
|
|
30
|
+
ODDS_FORMATS: () => ODDS_FORMATS,
|
|
31
|
+
OrderSheet: () => OrderSheet,
|
|
32
|
+
OrderSheetError: () => OrderSheetError,
|
|
33
|
+
OrderSheetProvider: () => OrderSheetProvider,
|
|
34
|
+
REQUEST_ERROR_CODES: () => REQUEST_ERROR_CODES,
|
|
35
|
+
REQUEST_ERROR_MESSAGES: () => REQUEST_ERROR_MESSAGES,
|
|
36
|
+
SERVER_CODE_MESSAGES: () => SERVER_CODE_MESSAGES,
|
|
37
|
+
buildQuote: () => buildQuote,
|
|
38
|
+
buildVenueOptions: () => buildVenueOptions,
|
|
39
|
+
contractsFromAmount: () => contractsFromAmount,
|
|
40
|
+
createInertSheet: () => createInertSheet,
|
|
41
|
+
createOrderSheetClient: () => createOrderSheetClient,
|
|
42
|
+
createOrderStream: () => createOrderStream,
|
|
43
|
+
createSheet: () => createSheet,
|
|
44
|
+
credentialSupportsStream: () => credentialSupportsStream,
|
|
45
|
+
floorToIncrement: () => floorToIncrement,
|
|
46
|
+
formatMoney: () => formatMoney,
|
|
47
|
+
formatPrice: () => formatPrice,
|
|
48
|
+
fromServerIssue: () => fromServerIssue,
|
|
49
|
+
initialSheetState: () => initialSheetState,
|
|
50
|
+
isOnGrid: () => isOnGrid,
|
|
51
|
+
isOrderSheetError: () => isOrderSheetError,
|
|
52
|
+
isSheet: () => isSheet,
|
|
53
|
+
localIssue: () => localIssue,
|
|
54
|
+
newIdempotencyKey: () => newIdempotencyKey,
|
|
55
|
+
phaseFromOrders: () => phaseFromOrders,
|
|
56
|
+
phaseFromSummary: () => phaseFromSummary,
|
|
57
|
+
resolveMaxPrice: () => resolveMaxPrice,
|
|
58
|
+
resolveOptions: () => resolveOptions,
|
|
59
|
+
round: () => round,
|
|
60
|
+
snapPrice: () => snapPrice,
|
|
61
|
+
summarize: () => summarize,
|
|
62
|
+
tabbableVenueId: () => tabbableVenueId,
|
|
63
|
+
toAmerican: () => toAmerican,
|
|
64
|
+
toDecimal: () => toDecimal,
|
|
65
|
+
toPlaceResult: () => toPlaceResult,
|
|
66
|
+
useOrderSheet: () => useOrderSheet,
|
|
67
|
+
useOrderSheetAccount: () => useOrderSheetAccount,
|
|
68
|
+
useOrderSheetClient: () => useOrderSheetClient,
|
|
69
|
+
useOrderSheetHandle: () => useOrderSheetHandle,
|
|
70
|
+
useSheetState: () => useSheetState,
|
|
71
|
+
userMessageFor: () => userMessageFor,
|
|
72
|
+
validateSheet: () => validateSheet,
|
|
73
|
+
venueKeyTarget: () => venueKeyTarget
|
|
74
|
+
});
|
|
75
|
+
module.exports = __toCommonJS(react_exports);
|
|
76
|
+
var import_react = require("react");
|
|
77
|
+
|
|
78
|
+
// src/generated/contract.ts
|
|
79
|
+
var CONSTRAINTS_VERSION = "2026-09-29.1";
|
|
80
|
+
var ISSUE_CODES = [
|
|
81
|
+
"real_money_not_permitted",
|
|
82
|
+
"practice_not_permitted",
|
|
83
|
+
"test_mode_requires_paper",
|
|
84
|
+
"venue_disabled",
|
|
85
|
+
"orders_not_supported",
|
|
86
|
+
"scope_not_granted",
|
|
87
|
+
"execution_limit_exceeded",
|
|
88
|
+
"insufficient_balance",
|
|
89
|
+
"venue_shard_underfunded",
|
|
90
|
+
"below_min_order",
|
|
91
|
+
"above_max_order",
|
|
92
|
+
"unfillable",
|
|
93
|
+
"below_min_contracts",
|
|
94
|
+
"amount_off_increment",
|
|
95
|
+
"price_out_of_band",
|
|
96
|
+
"above_max_price",
|
|
97
|
+
"price_off_tick",
|
|
98
|
+
"insufficient_size",
|
|
99
|
+
"insufficient_live_depth",
|
|
100
|
+
"no_viable_venues",
|
|
101
|
+
"no_live_price_for_paper",
|
|
102
|
+
"invalid_requested_amount",
|
|
103
|
+
"halted_upstream",
|
|
104
|
+
"unknown"
|
|
105
|
+
];
|
|
106
|
+
var ISSUE_FIXES = [
|
|
107
|
+
"raise_price",
|
|
108
|
+
"lower_price",
|
|
109
|
+
"raise_stake",
|
|
110
|
+
"lower_stake",
|
|
111
|
+
"round_stake",
|
|
112
|
+
"connect_venue",
|
|
113
|
+
"fund_venue",
|
|
114
|
+
"switch_venue",
|
|
115
|
+
"switch_to_paper",
|
|
116
|
+
"switch_to_real",
|
|
117
|
+
"use_market",
|
|
118
|
+
"use_limit",
|
|
119
|
+
"grant_scope",
|
|
120
|
+
"upgrade_plan",
|
|
121
|
+
"retry",
|
|
122
|
+
"none"
|
|
123
|
+
];
|
|
124
|
+
var REQUEST_ERROR_CODES = [
|
|
125
|
+
"missing_credentials",
|
|
126
|
+
"invalid_api_key",
|
|
127
|
+
"invalid_jwt",
|
|
128
|
+
"auth_error",
|
|
129
|
+
"not_authenticated",
|
|
130
|
+
"rate_limit_exceeded",
|
|
131
|
+
"account_forbidden",
|
|
132
|
+
"scope_not_granted",
|
|
133
|
+
"idempotency_error",
|
|
134
|
+
"account_scope_required",
|
|
135
|
+
"account_not_found",
|
|
136
|
+
"environment_unresolved",
|
|
137
|
+
"execution_key_required",
|
|
138
|
+
"entitlement_required",
|
|
139
|
+
"bad_request",
|
|
140
|
+
"unknown_parameter",
|
|
141
|
+
"conflicting_parameters",
|
|
142
|
+
"unsupported_order_type",
|
|
143
|
+
"test_mode_requires_paper",
|
|
144
|
+
"real_money_not_permitted",
|
|
145
|
+
"connect_scope_required",
|
|
146
|
+
"model_not_found",
|
|
147
|
+
"model_archived",
|
|
148
|
+
"opportunity_not_found",
|
|
149
|
+
"currency_mismatch",
|
|
150
|
+
"model_mismatch",
|
|
151
|
+
"idempotency_conflict",
|
|
152
|
+
"internal_error"
|
|
153
|
+
];
|
|
154
|
+
var REQUEST_ERROR_MESSAGES = {
|
|
155
|
+
missing_credentials: "Sign in to place orders.",
|
|
156
|
+
invalid_api_key: "This app's access key isn't valid any more.",
|
|
157
|
+
invalid_jwt: "Your session has expired \u2014 sign in again.",
|
|
158
|
+
auth_error: "We couldn't check your sign-in. Try again.",
|
|
159
|
+
not_authenticated: "Sign in to place orders.",
|
|
160
|
+
rate_limit_exceeded: "Too many requests \u2014 wait a moment and try again.",
|
|
161
|
+
account_forbidden: "This app can't act for that account.",
|
|
162
|
+
scope_not_granted: "This app hasn't been allowed to place orders.",
|
|
163
|
+
idempotency_error: "The order couldn't be submitted safely. Try again.",
|
|
164
|
+
account_scope_required: "This sign-in isn't linked to a trading account.",
|
|
165
|
+
account_not_found: "That trading account no longer exists.",
|
|
166
|
+
environment_unresolved: "We couldn't tell whether this is a test or live key.",
|
|
167
|
+
execution_key_required: "This app's key can read markets but not trade.",
|
|
168
|
+
entitlement_required: "Trading isn't included in this plan.",
|
|
169
|
+
bad_request: "Something in this order wasn't valid.",
|
|
170
|
+
unknown_parameter: "Something in this order wasn't valid.",
|
|
171
|
+
conflicting_parameters: "Something in this order wasn't valid.",
|
|
172
|
+
unsupported_order_type: "That venue doesn't take this type of order.",
|
|
173
|
+
test_mode_requires_paper: "This is a test key \u2014 it can only place practice orders.",
|
|
174
|
+
real_money_not_permitted: "Real-money trading is not included in this plan.",
|
|
175
|
+
connect_scope_required: "This account hasn't allowed this app to trade.",
|
|
176
|
+
model_not_found: "That model doesn't exist.",
|
|
177
|
+
model_archived: "That model is archived and takes no new orders.",
|
|
178
|
+
opportunity_not_found: "That order group doesn't exist.",
|
|
179
|
+
currency_mismatch: "Those orders use different currencies.",
|
|
180
|
+
model_mismatch: "That order group belongs to a different model.",
|
|
181
|
+
idempotency_conflict: "This order was already submitted with different details.",
|
|
182
|
+
internal_error: "Something went wrong on our side. Try again."
|
|
183
|
+
};
|
|
184
|
+
var ISSUE_SEVERITY = {
|
|
185
|
+
real_money_not_permitted: "block",
|
|
186
|
+
practice_not_permitted: "block",
|
|
187
|
+
test_mode_requires_paper: "block",
|
|
188
|
+
venue_disabled: "block",
|
|
189
|
+
orders_not_supported: "block",
|
|
190
|
+
scope_not_granted: "block",
|
|
191
|
+
execution_limit_exceeded: "block",
|
|
192
|
+
insufficient_balance: "block",
|
|
193
|
+
venue_shard_underfunded: "block",
|
|
194
|
+
below_min_order: "block",
|
|
195
|
+
above_max_order: "block",
|
|
196
|
+
unfillable: "block",
|
|
197
|
+
below_min_contracts: "block",
|
|
198
|
+
amount_off_increment: "warn",
|
|
199
|
+
price_out_of_band: "block",
|
|
200
|
+
above_max_price: "block",
|
|
201
|
+
price_off_tick: "warn",
|
|
202
|
+
insufficient_size: "block",
|
|
203
|
+
insufficient_live_depth: "block",
|
|
204
|
+
no_viable_venues: "block",
|
|
205
|
+
no_live_price_for_paper: "block",
|
|
206
|
+
invalid_requested_amount: "block",
|
|
207
|
+
halted_upstream: "info",
|
|
208
|
+
unknown: "block"
|
|
209
|
+
};
|
|
210
|
+
var ISSUE_DEFAULT_FIX = {
|
|
211
|
+
real_money_not_permitted: "upgrade_plan",
|
|
212
|
+
practice_not_permitted: "none",
|
|
213
|
+
test_mode_requires_paper: "switch_to_paper",
|
|
214
|
+
venue_disabled: "connect_venue",
|
|
215
|
+
orders_not_supported: "switch_venue",
|
|
216
|
+
scope_not_granted: "grant_scope",
|
|
217
|
+
execution_limit_exceeded: "none",
|
|
218
|
+
insufficient_balance: "fund_venue",
|
|
219
|
+
venue_shard_underfunded: "fund_venue",
|
|
220
|
+
below_min_order: "raise_stake",
|
|
221
|
+
above_max_order: "lower_stake",
|
|
222
|
+
unfillable: "raise_stake",
|
|
223
|
+
below_min_contracts: "raise_stake",
|
|
224
|
+
amount_off_increment: "round_stake",
|
|
225
|
+
price_out_of_band: "lower_price",
|
|
226
|
+
above_max_price: "raise_price",
|
|
227
|
+
price_off_tick: "none",
|
|
228
|
+
insufficient_size: "lower_stake",
|
|
229
|
+
insufficient_live_depth: "switch_venue",
|
|
230
|
+
no_viable_venues: "switch_venue",
|
|
231
|
+
no_live_price_for_paper: "switch_venue",
|
|
232
|
+
invalid_requested_amount: "raise_stake",
|
|
233
|
+
halted_upstream: "none",
|
|
234
|
+
unknown: "retry"
|
|
235
|
+
};
|
|
236
|
+
|
|
237
|
+
// src/errors.ts
|
|
238
|
+
var OrderSheetError = class extends Error {
|
|
239
|
+
constructor(init) {
|
|
240
|
+
super(init.message);
|
|
241
|
+
this.name = "OrderSheetError";
|
|
242
|
+
this.code = init.code;
|
|
243
|
+
this.status = init.status ?? null;
|
|
244
|
+
this.details = init.details ?? null;
|
|
245
|
+
this.retryable = init.retryable ?? false;
|
|
246
|
+
if (init.cause !== void 0) this.cause = init.cause;
|
|
247
|
+
}
|
|
248
|
+
};
|
|
249
|
+
function isOrderSheetError(e) {
|
|
250
|
+
return e instanceof OrderSheetError || typeof e === "object" && e !== null && e.name === "OrderSheetError";
|
|
251
|
+
}
|
|
252
|
+
function userMessageFor(e) {
|
|
253
|
+
const known = REQUEST_ERROR_MESSAGES[e.code];
|
|
254
|
+
if (known) return known;
|
|
255
|
+
if (e.code === "real_money_disabled") return "Real-money orders are turned off in this app.";
|
|
256
|
+
if (e.code === "network_error" || e.status === null) return "Couldn't reach OpenMarkets. Check your connection and try again.";
|
|
257
|
+
if (e.retryable) return "Something went wrong on our side. Try again.";
|
|
258
|
+
return "This order couldn't be submitted.";
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
// src/auth.ts
|
|
262
|
+
async function resolveCredential(opts) {
|
|
263
|
+
if ("apiKey" in opts && opts.apiKey) {
|
|
264
|
+
return { kind: "api_key", value: opts.apiKey, headers: { "X-API-Key": opts.apiKey } };
|
|
265
|
+
}
|
|
266
|
+
if ("auth" in opts && opts.auth) {
|
|
267
|
+
const key = await opts.auth.getApiKey();
|
|
268
|
+
if (!key) throw new OrderSheetError({ code: "no_credential", message: "Not signed in to OpenMarkets \u2014 no API key is available for this request." });
|
|
269
|
+
return { kind: "api_key", value: key, headers: { "X-API-Key": key } };
|
|
270
|
+
}
|
|
271
|
+
if ("getToken" in opts && opts.getToken) {
|
|
272
|
+
const token = await opts.getToken();
|
|
273
|
+
if (!token) throw new OrderSheetError({ code: "no_credential", message: "No session token is available for this request." });
|
|
274
|
+
return { kind: "jwt", value: token, headers: { Authorization: `Bearer ${token}` } };
|
|
275
|
+
}
|
|
276
|
+
throw new OrderSheetError({ code: "not_configured", message: "createOrderSheetClient needs one of { apiKey }, { getToken } or { auth }." });
|
|
277
|
+
}
|
|
278
|
+
function credentialSupportsStream(opts) {
|
|
279
|
+
return "apiKey" in opts && !!opts.apiKey || "auth" in opts && !!opts.auth;
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
// src/odds.ts
|
|
283
|
+
var ODDS_FORMATS = ["cents", "percent", "american", "decimal"];
|
|
284
|
+
function toAmerican(p) {
|
|
285
|
+
if (!(p > 0 && p < 1)) return "\u2014";
|
|
286
|
+
const v = p >= 0.5 ? -Math.round(100 * p / (1 - p)) : Math.round(100 * (1 - p) / p);
|
|
287
|
+
return v > 0 ? `+${v}` : `${v}`;
|
|
288
|
+
}
|
|
289
|
+
function toDecimal(p) {
|
|
290
|
+
if (!(p > 0 && p < 1)) return "\u2014";
|
|
291
|
+
return (1 / p).toFixed(2);
|
|
292
|
+
}
|
|
293
|
+
function formatPrice(p, format, american) {
|
|
294
|
+
if (p == null || !Number.isFinite(p) || p <= 0) return "\u2014";
|
|
295
|
+
switch (format) {
|
|
296
|
+
case "cents":
|
|
297
|
+
return `${Math.round(p * 100)}\xA2`;
|
|
298
|
+
case "percent":
|
|
299
|
+
return `${Math.round(p * 100)}%`;
|
|
300
|
+
case "american":
|
|
301
|
+
return american ?? toAmerican(p);
|
|
302
|
+
case "decimal":
|
|
303
|
+
return toDecimal(p);
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
function formatMoney(n, currency = "USD") {
|
|
307
|
+
if (n == null || !Number.isFinite(n)) return "\u2014";
|
|
308
|
+
const abs = Math.abs(n);
|
|
309
|
+
const body = abs.toLocaleString("en-US", { minimumFractionDigits: 2, maximumFractionDigits: 2 });
|
|
310
|
+
const sign = n < 0 ? "\u2212" : "";
|
|
311
|
+
if (currency === "USD") return `${sign}$${body}`;
|
|
312
|
+
return `${sign}${body} ${currency}`;
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
// src/messages.ts
|
|
316
|
+
var usd = (n, currency = "USD", fallback = "the stake") => typeof n === "number" && Number.isFinite(n) ? formatMoney(n, currency) : fallback;
|
|
317
|
+
var cents = (p, fallback = "this price") => typeof p === "number" && Number.isFinite(p) ? `${Math.round(p * 100)}\xA2` : fallback;
|
|
318
|
+
var venue = (ctx) => ctx.venue ?? "this venue";
|
|
319
|
+
var plural = (n, one, many) => n === 1 ? one : many;
|
|
320
|
+
var SERVER_CODE_MESSAGES = {
|
|
321
|
+
real_money_not_permitted: () => "Real-money trading is not included in this plan.",
|
|
322
|
+
practice_not_permitted: () => "Practice trading is not available with this credential.",
|
|
323
|
+
test_mode_requires_paper: () => "This is a test key \u2014 it can only place practice orders.",
|
|
324
|
+
venue_disabled: (c) => `${venue(c)} isn't connected on this account.`,
|
|
325
|
+
orders_not_supported: (c) => `${venue(c)} is quotes-only \u2014 it doesn't take orders.`,
|
|
326
|
+
scope_not_granted: (c) => `This account hasn't allowed trading on ${venue(c)}.`,
|
|
327
|
+
execution_limit_exceeded: (c) => c.detail ? `Blocked by your trading limits: ${c.detail}.` : "Blocked by your trading limits.",
|
|
328
|
+
insufficient_balance: (c) => typeof c.required === "number" && typeof c.balance === "number" ? `Needs ${usd(c.required, c.currency)} at ${venue(c)}; you have ${usd(c.balance, c.currency)}.` : `Not enough balance at ${venue(c)}.`,
|
|
329
|
+
venue_shard_underfunded: (c) => `Your ${venue(c)} balance couldn't be moved to cover this market.`,
|
|
330
|
+
below_min_order: (c) => `${venue(c)}'s minimum is ${usd(c.min, c.currency, "higher")}.`,
|
|
331
|
+
above_max_order: (c) => `${venue(c)}'s maximum is ${usd(c.max, c.currency, "lower")}.`,
|
|
332
|
+
unfillable: (c) => `${usd(c.amount, c.currency)} buys less than one contract at ${cents(c.price)} \u2014 it can't fill.`,
|
|
333
|
+
below_min_contracts: (c) => typeof c.min_contracts === "number" ? `${venue(c)} needs at least ${c.min_contracts} ${plural(c.min_contracts, "contract", "contracts")}; ${usd(c.amount, c.currency)} buys ${c.contracts ?? "fewer"}.` : `${usd(c.amount, c.currency)} buys fewer contracts than ${venue(c)} accepts.`,
|
|
334
|
+
amount_off_increment: (c) => `${venue(c)} takes stakes in steps of ${usd(c.increment, c.currency, "a cent")} \u2014 this will be rounded down.`,
|
|
335
|
+
price_out_of_band: (c) => typeof c.min === "number" && typeof c.max === "number" ? `Prices on ${venue(c)} run ${cents(c.min)}\u2013${cents(c.max)}.` : `That price is outside what ${venue(c)} accepts.`,
|
|
336
|
+
above_max_price: (c) => typeof c.price === "number" && typeof c.max_price === "number" ? `The book is at ${cents(c.price)}, above your ${cents(c.max_price)} limit.` : "The book is above your price limit.",
|
|
337
|
+
price_off_tick: (c) => `${venue(c)} prices in fixed steps \u2014 your limit was rounded to ${cents(c.price)}.`,
|
|
338
|
+
insufficient_size: (c) => `Only ${usd(c.available, c.currency, "less")} is available at ${cents(c.price)}.`,
|
|
339
|
+
insufficient_live_depth: () => "Nothing is available at this price right now.",
|
|
340
|
+
no_viable_venues: (c) => `${venue(c)} isn't quoting this position right now.`,
|
|
341
|
+
no_live_price_for_paper: () => "No live price to practise against.",
|
|
342
|
+
invalid_requested_amount: () => "Enter a stake above zero.",
|
|
343
|
+
halted_upstream: () => "Not placed \u2014 another leg failed first.",
|
|
344
|
+
unknown: (c) => c.detail ?? "The venue declined this order."
|
|
345
|
+
};
|
|
346
|
+
var CLIENT_CODE_SPECS = {
|
|
347
|
+
no_venue: { severity: "block", fix: "switch_venue", scope: "leg", message: () => "Pick a venue." },
|
|
348
|
+
no_stake: { severity: "block", fix: "raise_stake", scope: "leg", message: () => "Enter a stake." },
|
|
349
|
+
no_quote: { severity: "block", fix: "switch_venue", scope: "venue", message: (c) => `${venue(c)} has no live price for this position.` },
|
|
350
|
+
paper_limit_unsupported: { severity: "block", fix: "use_market", scope: "leg", message: () => "Practice orders fill instantly \u2014 limit orders need real money." },
|
|
351
|
+
order_type_unsupported: { severity: "block", fix: "use_market", scope: "venue", message: (c) => `${venue(c)} doesn't take ${c.detail ?? "this type of"} orders.` },
|
|
352
|
+
mode_unavailable: { severity: "block", fix: "switch_to_paper", scope: "account", message: (c) => `${c.detail === "paper" ? "Practice" : "Real-money"} trading isn't available on this account.` },
|
|
353
|
+
real_money_disabled: { severity: "block", fix: "switch_to_paper", scope: "request", message: () => "Real-money orders are turned off in this app." },
|
|
354
|
+
not_paper_bettable: { severity: "block", fix: "switch_to_real", scope: "leg", message: () => "This position can't be practised on yet." },
|
|
355
|
+
stale_quote: { severity: "warn", fix: "retry", scope: "venue", message: (c) => `${venue(c)}'s price is ${c.detail ?? "a while"} old \u2014 re-check before placing.` },
|
|
356
|
+
better_price_available: { severity: "info", fix: "switch_venue", scope: "venue", message: (c) => `${cents(c.price)} available on ${venue(c)} \u2014 switch?` },
|
|
357
|
+
constraints_stale: { severity: "info", fix: "retry", scope: "account", message: () => "Venue rules changed on the server \u2014 the preview will re-check." },
|
|
358
|
+
confirm_required: { severity: "info", fix: "none", scope: "request", message: (c) => `Confirm the real-money order for ${usd(c.amount, c.currency)} on ${venue(c)}.` },
|
|
359
|
+
confirm_expired: { severity: "block", fix: "retry", scope: "request", message: () => "The order changed since it was confirmed \u2014 review it and place it again." },
|
|
360
|
+
not_ready: { severity: "block", fix: "retry", scope: "request", message: () => "The order sheet is still loading." }
|
|
361
|
+
};
|
|
362
|
+
function isServerCode(code) {
|
|
363
|
+
return Object.prototype.hasOwnProperty.call(SERVER_CODE_MESSAGES, code);
|
|
364
|
+
}
|
|
365
|
+
function localIssue(code, ctx = {}, over = {}) {
|
|
366
|
+
if (isServerCode(code)) {
|
|
367
|
+
return {
|
|
368
|
+
code,
|
|
369
|
+
severity: over.severity ?? ISSUE_SEVERITY[code],
|
|
370
|
+
message: SERVER_CODE_MESSAGES[code](ctx),
|
|
371
|
+
detail: over.detail ?? ctx.detail ?? null,
|
|
372
|
+
fix: over.fix ?? fixFor(code, ctx),
|
|
373
|
+
scope: over.scope ?? "leg",
|
|
374
|
+
source: "local"
|
|
375
|
+
};
|
|
376
|
+
}
|
|
377
|
+
const spec = CLIENT_CODE_SPECS[code];
|
|
378
|
+
return {
|
|
379
|
+
code,
|
|
380
|
+
severity: over.severity ?? spec.severity,
|
|
381
|
+
message: spec.message(ctx),
|
|
382
|
+
detail: over.detail ?? ctx.detail ?? null,
|
|
383
|
+
fix: over.fix ?? spec.fix,
|
|
384
|
+
scope: over.scope ?? spec.scope,
|
|
385
|
+
source: "local"
|
|
386
|
+
};
|
|
387
|
+
}
|
|
388
|
+
function fixFor(code, ctx) {
|
|
389
|
+
if (code === "price_out_of_band" && typeof ctx.price === "number" && typeof ctx.min === "number") {
|
|
390
|
+
return ctx.price < ctx.min ? "raise_price" : "lower_price";
|
|
391
|
+
}
|
|
392
|
+
return ISSUE_DEFAULT_FIX[code];
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
// src/math.ts
|
|
396
|
+
var STEP_EPS = 1e-4;
|
|
397
|
+
var AMOUNT_TOL = 5e-3;
|
|
398
|
+
function contractsFromAmount(amount, pricePerContract) {
|
|
399
|
+
if (!Number.isFinite(amount) || !Number.isFinite(pricePerContract) || pricePerContract <= 0) return 0;
|
|
400
|
+
return Math.floor(amount / pricePerContract + 1e-9);
|
|
401
|
+
}
|
|
402
|
+
function isOnGrid(value, step) {
|
|
403
|
+
if (!step) return true;
|
|
404
|
+
const ratio = value / step;
|
|
405
|
+
return Math.abs(ratio - Math.round(ratio)) <= STEP_EPS;
|
|
406
|
+
}
|
|
407
|
+
function decimalsFor(step) {
|
|
408
|
+
if (!Number.isFinite(step) || step <= 0) return 2;
|
|
409
|
+
const s = step.toString();
|
|
410
|
+
const i = s.indexOf(".");
|
|
411
|
+
return i === -1 ? 0 : s.length - i - 1;
|
|
412
|
+
}
|
|
413
|
+
function floorToIncrement(amount, step) {
|
|
414
|
+
if (!step) return round(amount, 2);
|
|
415
|
+
const floored = Math.floor((amount + 1e-6) / step) * step;
|
|
416
|
+
return Number(floored.toFixed(decimalsFor(step)));
|
|
417
|
+
}
|
|
418
|
+
function snapPrice(price, tick, min, max) {
|
|
419
|
+
let p = price;
|
|
420
|
+
if (tick && tick > 0) p = Math.round(p / tick) * tick;
|
|
421
|
+
p = Math.min(max, Math.max(min, p));
|
|
422
|
+
return round(p, 4);
|
|
423
|
+
}
|
|
424
|
+
function round(n, dp) {
|
|
425
|
+
const f = Math.pow(10, dp);
|
|
426
|
+
return Math.round(n * f) / f;
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
// src/quote.ts
|
|
430
|
+
function resolveMaxPrice(venue2, orderType, userMaxPrice, slippage) {
|
|
431
|
+
const price = venue2.price;
|
|
432
|
+
const pc = venue2.constraints?.price;
|
|
433
|
+
const tick = pc ? pc.tick : 0.01;
|
|
434
|
+
const min = pc?.min ?? 0.01;
|
|
435
|
+
const max = pc?.max ?? 0.99;
|
|
436
|
+
if (userMaxPrice != null) return snapPrice(userMaxPrice, tick, min, max);
|
|
437
|
+
if (price == null) return null;
|
|
438
|
+
return snapPrice(orderType === "limit" ? price : price + slippage, tick, min, max);
|
|
439
|
+
}
|
|
440
|
+
function buildQuote(input) {
|
|
441
|
+
const { venue: venue2, stake } = input;
|
|
442
|
+
if (stake.value == null || !(stake.value > 0)) return null;
|
|
443
|
+
const size = venue2.constraints?.size;
|
|
444
|
+
const whole = size?.whole_contracts ?? false;
|
|
445
|
+
const increment = size?.increment_usd ?? null;
|
|
446
|
+
const price = input.orderType === "limit" ? input.maxPrice : venue2.price;
|
|
447
|
+
if (price == null || !(price > 0)) return null;
|
|
448
|
+
const maxPrice = input.maxPrice ?? price;
|
|
449
|
+
const pf = input.preflight?.leg;
|
|
450
|
+
const usePreflight = !!pf && pf.accepted;
|
|
451
|
+
const basis = usePreflight && input.orderType === "market" && pf.total_allocated > 0 ? pf.expected_fill_price : price;
|
|
452
|
+
const notional = stake.unit === "amount" ? stake.value : round(stake.value * basis, 4);
|
|
453
|
+
let contracts;
|
|
454
|
+
let cost;
|
|
455
|
+
if (usePreflight) {
|
|
456
|
+
contracts = pf.normalized.contracts;
|
|
457
|
+
cost = pf.normalized.amount;
|
|
458
|
+
} else if (stake.unit === "shares") {
|
|
459
|
+
contracts = whole ? Math.floor(stake.value + 1e-9) : stake.value;
|
|
460
|
+
cost = round(contracts * basis, 2);
|
|
461
|
+
} else if (whole) {
|
|
462
|
+
contracts = contractsFromAmount(notional, basis);
|
|
463
|
+
cost = round(contracts * basis, 2);
|
|
464
|
+
} else {
|
|
465
|
+
cost = floorToIncrement(notional, increment);
|
|
466
|
+
contracts = basis > 0 ? round(cost / basis, 4) : 0;
|
|
467
|
+
}
|
|
468
|
+
const payout = round(contracts, 2);
|
|
469
|
+
const leftover = round(Math.max(0, notional - cost), 2);
|
|
470
|
+
const direction = venue2.constraints?.fees.direction ?? null;
|
|
471
|
+
let fee = null;
|
|
472
|
+
if (usePreflight) {
|
|
473
|
+
fee = { taker: pf.total_taker_fee, maker: pf.total_maker_fee, direction, estimated: false };
|
|
474
|
+
} else if (venue2.feePacket && input.mode === "real") {
|
|
475
|
+
const k = cost / 100;
|
|
476
|
+
fee = { taker: round(venue2.feePacket.taker_fee * k, 2), maker: round(venue2.feePacket.maker_fee * k, 2), direction, estimated: true };
|
|
477
|
+
} else if (input.mode === "paper") {
|
|
478
|
+
fee = { taker: 0, maker: 0, direction: null, estimated: false };
|
|
479
|
+
}
|
|
480
|
+
const takerFee = fee?.taker ?? 0;
|
|
481
|
+
const effectivePrice = contracts > 0 ? round((cost + takerFee) / contracts, 4) : null;
|
|
482
|
+
const profit = round(payout - cost - takerFee, 2);
|
|
483
|
+
const expectedFill = usePreflight ? { allocated: pf.total_allocated, unfilled: pf.unfilled_amount, fillPrice: pf.expected_fill_price } : null;
|
|
484
|
+
const wouldRest = usePreflight ? pf.would_rest : input.orderType === "limit" && venue2.price != null && venue2.price > maxPrice + 1e-9;
|
|
485
|
+
const freshness = input.stale ? "stale" : usePreflight ? "preflight" : "quote";
|
|
486
|
+
const cur = input.currency;
|
|
487
|
+
return {
|
|
488
|
+
venuePartnerId: venue2.partnerId,
|
|
489
|
+
mode: input.mode,
|
|
490
|
+
orderType: input.orderType,
|
|
491
|
+
stake: { unit: stake.unit, value: stake.value },
|
|
492
|
+
price: round(basis, 4),
|
|
493
|
+
maxPrice: round(maxPrice, 4),
|
|
494
|
+
contracts,
|
|
495
|
+
cost,
|
|
496
|
+
payout,
|
|
497
|
+
leftover,
|
|
498
|
+
notional: round(notional, 2),
|
|
499
|
+
fee,
|
|
500
|
+
effectivePrice,
|
|
501
|
+
profit,
|
|
502
|
+
expectedFill,
|
|
503
|
+
wouldRest,
|
|
504
|
+
currency: cur,
|
|
505
|
+
freshness,
|
|
506
|
+
display: {
|
|
507
|
+
price: formatPrice(basis, input.oddsFormat, input.orderType === "market" ? venue2.americanOdds : null),
|
|
508
|
+
maxPrice: formatPrice(maxPrice, input.oddsFormat),
|
|
509
|
+
cost: formatMoney(cost, cur),
|
|
510
|
+
payout: formatMoney(payout, cur),
|
|
511
|
+
profit: formatMoney(profit, cur),
|
|
512
|
+
fee: fee ? `${formatMoney(takerFee, cur)}${fee.estimated ? " est." : ""}` : "\u2014",
|
|
513
|
+
effectivePrice: effectivePrice != null ? formatPrice(Math.min(0.9999, effectivePrice), input.oddsFormat) : "\u2014"
|
|
514
|
+
}
|
|
515
|
+
};
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
// src/validate.ts
|
|
519
|
+
function validateSheet(input) {
|
|
520
|
+
const issues = [];
|
|
521
|
+
const { venue: venue2, mode, orderType, stake, account } = input;
|
|
522
|
+
const cur = input.currency;
|
|
523
|
+
const push = (i) => {
|
|
524
|
+
if (!issues.some((x) => x.code === i.code && x.scope === i.scope)) issues.push(i);
|
|
525
|
+
};
|
|
526
|
+
if (account) {
|
|
527
|
+
if (!account.moneyModes.includes(mode)) {
|
|
528
|
+
const reason = account.issues.find((i) => mode === "paper" ? i.code === "practice_not_permitted" : i.code !== "practice_not_permitted");
|
|
529
|
+
push(reason ?? localIssue("mode_unavailable", { detail: mode }));
|
|
530
|
+
}
|
|
531
|
+
}
|
|
532
|
+
if (mode === "real" && !input.realMoneyEnabled) push(localIssue("real_money_disabled"));
|
|
533
|
+
if (mode === "paper" && orderType === "limit") push(localIssue("paper_limit_unsupported"));
|
|
534
|
+
if (mode === "paper" && input.paperBettable === false) push(localIssue("not_paper_bettable"));
|
|
535
|
+
if (input.constraintsStale) push(localIssue("constraints_stale"));
|
|
536
|
+
if (!venue2) {
|
|
537
|
+
push(localIssue("no_venue"));
|
|
538
|
+
return finish(issues);
|
|
539
|
+
}
|
|
540
|
+
for (const i of venue2.eligibility.issues) push(i);
|
|
541
|
+
const c = venue2.constraints;
|
|
542
|
+
const price = orderType === "limit" ? input.maxPrice : venue2.price;
|
|
543
|
+
if (price == null) {
|
|
544
|
+
if (orderType === "limit") push(localIssue("no_stake", {}, { fix: "raise_price" }));
|
|
545
|
+
}
|
|
546
|
+
if (stake.value == null || !(stake.value > 0)) {
|
|
547
|
+
push(localIssue("no_stake"));
|
|
548
|
+
return finish(issues);
|
|
549
|
+
}
|
|
550
|
+
if (price == null) return finish(issues);
|
|
551
|
+
const notional = stake.unit === "amount" ? stake.value : stake.value * price;
|
|
552
|
+
const vctx = { venue: venue2.name, currency: cur };
|
|
553
|
+
if (c && mode === "real") {
|
|
554
|
+
const size = c.size;
|
|
555
|
+
const isFullTake = venue2.available != null && Math.abs(notional - venue2.available) <= AMOUNT_TOL;
|
|
556
|
+
if (notional + AMOUNT_TOL < size.min_usd && !(size.full_fill_bypasses_min && isFullTake)) {
|
|
557
|
+
push(localIssue("below_min_order", { ...vctx, amount: notional, min: size.min_usd }));
|
|
558
|
+
}
|
|
559
|
+
if (size.max_usd != null && notional - AMOUNT_TOL > size.max_usd) {
|
|
560
|
+
push(localIssue("above_max_order", { ...vctx, amount: notional, max: size.max_usd }));
|
|
561
|
+
}
|
|
562
|
+
if (size.whole_contracts) {
|
|
563
|
+
const contracts = stake.unit === "shares" ? Math.floor(stake.value + 1e-9) : contractsFromAmount(notional, price);
|
|
564
|
+
const minC = Math.max(1, size.min_contracts);
|
|
565
|
+
if (contracts < minC) {
|
|
566
|
+
push(contracts === 0 && minC === 1 ? localIssue("unfillable", { ...vctx, amount: notional, price }) : localIssue("below_min_contracts", { ...vctx, amount: notional, price, contracts, min_contracts: minC }));
|
|
567
|
+
}
|
|
568
|
+
} else if (size.increment_usd && stake.unit === "amount" && !isOnGrid(notional, size.increment_usd)) {
|
|
569
|
+
push(localIssue("amount_off_increment", { ...vctx, amount: notional, increment: size.increment_usd }));
|
|
570
|
+
}
|
|
571
|
+
const pc = c.price;
|
|
572
|
+
if (input.maxPrice != null) {
|
|
573
|
+
if (input.maxPrice + 1e-9 < pc.min || input.maxPrice - 1e-9 > pc.max) {
|
|
574
|
+
push(localIssue("price_out_of_band", { ...vctx, price: input.maxPrice, min: pc.min, max: pc.max }));
|
|
575
|
+
} else if (pc.tick && Math.abs(snapPrice(input.maxPrice, pc.tick, pc.min, pc.max) - input.maxPrice) > 1e-6) {
|
|
576
|
+
push(localIssue("price_off_tick", { ...vctx, price: snapPrice(input.maxPrice, pc.tick, pc.min, pc.max), tick: pc.tick }));
|
|
577
|
+
}
|
|
578
|
+
}
|
|
579
|
+
if (orderType === "market" && input.maxPrice != null && venue2.price != null && venue2.price > input.maxPrice + 1e-9) {
|
|
580
|
+
push(localIssue("above_max_price", { ...vctx, price: venue2.price, max_price: input.maxPrice }));
|
|
581
|
+
}
|
|
582
|
+
if (orderType === "market" && venue2.available != null && notional - AMOUNT_TOL > venue2.available) {
|
|
583
|
+
push(localIssue("insufficient_size", { ...vctx, available: venue2.available, price: venue2.price ?? price }));
|
|
584
|
+
}
|
|
585
|
+
const bal = venue2.account.expectedBalance;
|
|
586
|
+
if (bal != null && !venue2.account.stale && notional - AMOUNT_TOL > bal) {
|
|
587
|
+
push(localIssue("insufficient_balance", { ...vctx, required: notional, balance: bal }, { severity: "warn" }));
|
|
588
|
+
}
|
|
589
|
+
}
|
|
590
|
+
if (mode === "paper" && account?.practiceWallet.balance != null && notional - AMOUNT_TOL > account.practiceWallet.balance) {
|
|
591
|
+
push(localIssue("insufficient_balance", {
|
|
592
|
+
venue: "your practice wallet",
|
|
593
|
+
currency: account.practiceWallet.currency,
|
|
594
|
+
required: notional,
|
|
595
|
+
balance: account.practiceWallet.balance
|
|
596
|
+
}, { fix: "lower_stake" }));
|
|
597
|
+
}
|
|
598
|
+
if (orderType === "market" && input.maxPrice != null && venue2.price != null && venue2.price > input.maxPrice + 1e-9 && mode === "paper") {
|
|
599
|
+
push(localIssue("above_max_price", { ...vctx, price: venue2.price, max_price: input.maxPrice }));
|
|
600
|
+
}
|
|
601
|
+
if (input.quoteAgeMs != null && input.quoteAgeMs > input.staleAfterMs) {
|
|
602
|
+
push(localIssue("stale_quote", { ...vctx, detail: `${Math.round(input.quoteAgeMs / 1e3)}s` }));
|
|
603
|
+
}
|
|
604
|
+
const best = input.venues.find((v) => v.best);
|
|
605
|
+
if (best && best.partnerId !== venue2.partnerId && best.price != null && venue2.price != null && best.price < venue2.price - 1e-9 && orderType === "market") {
|
|
606
|
+
push(localIssue("better_price_available", { venue: best.name, price: best.price }));
|
|
607
|
+
}
|
|
608
|
+
return finish(issues);
|
|
609
|
+
}
|
|
610
|
+
function finish(issues) {
|
|
611
|
+
const blocking = issues.filter((i) => i.severity === "block");
|
|
612
|
+
const warnings = issues.filter((i) => i.severity === "warn");
|
|
613
|
+
const info = issues.filter((i) => i.severity === "info");
|
|
614
|
+
return { ok: blocking.length === 0, blocking, warnings, info, all: issues };
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
// src/venues.ts
|
|
618
|
+
function fromServerIssue(i) {
|
|
619
|
+
return { ...i, source: "server" };
|
|
620
|
+
}
|
|
621
|
+
function buildVenueOptions(input) {
|
|
622
|
+
const byId = /* @__PURE__ */ new Map();
|
|
623
|
+
for (const v of input.constraints?.venues ?? []) byId.set(v.partner_id, v);
|
|
624
|
+
const rows = input.position.partner_liquidities;
|
|
625
|
+
const practiceBook = input.account?.practiceWallet.partnerId ?? input.constraints?.account.practice_wallet.partner_id ?? null;
|
|
626
|
+
const options = [];
|
|
627
|
+
for (const row of rows) {
|
|
628
|
+
if (practiceBook && row.partner_id === practiceBook) continue;
|
|
629
|
+
const c = byId.get(row.partner_id) ?? null;
|
|
630
|
+
options.push(toOption(row, c, input));
|
|
631
|
+
}
|
|
632
|
+
let best = null;
|
|
633
|
+
for (const o of options) {
|
|
634
|
+
if (!o.actionable || o.price == null) continue;
|
|
635
|
+
if (!best || o.price < (best.price ?? Infinity)) best = o;
|
|
636
|
+
}
|
|
637
|
+
if (best) best.best = true;
|
|
638
|
+
return options.sort((a, b) => {
|
|
639
|
+
const pa = a.actionable && a.price != null ? a.price : Infinity;
|
|
640
|
+
const pb = b.actionable && b.price != null ? b.price : Infinity;
|
|
641
|
+
return pa - pb;
|
|
642
|
+
});
|
|
643
|
+
}
|
|
644
|
+
function toOption(row, c, input) {
|
|
645
|
+
const actionable = row.actionable !== false && row.price > 0;
|
|
646
|
+
const name = c?.name ?? row.partner_name ?? row.partner_id;
|
|
647
|
+
const issues = [];
|
|
648
|
+
if (c) {
|
|
649
|
+
const elig = input.mode === "real" ? c.eligibility.real : c.eligibility.paper;
|
|
650
|
+
for (const i of elig.issues) issues.push(fromServerIssue(i));
|
|
651
|
+
if (input.mode === "real" && c.orders_allowed && !c.order_types.includes(input.orderType)) {
|
|
652
|
+
issues.push(localIssue("order_type_unsupported", { venue: name, detail: input.orderType }));
|
|
653
|
+
}
|
|
654
|
+
} else if (input.constraints) {
|
|
655
|
+
if (input.mode === "real") issues.push(localIssue("orders_not_supported", { venue: name }, { scope: "venue" }));
|
|
656
|
+
}
|
|
657
|
+
if (input.mode === "paper" && input.orderType === "limit") {
|
|
658
|
+
issues.push(localIssue("paper_limit_unsupported", { venue: name }));
|
|
659
|
+
}
|
|
660
|
+
if (!actionable) issues.push(localIssue("no_quote", { venue: name }));
|
|
661
|
+
const status = issues.some((i) => i.severity === "block") ? "blocked" : issues.some((i) => i.severity === "warn") ? "warn" : "ok";
|
|
662
|
+
const modes = [];
|
|
663
|
+
if (c ? c.eligibility.paper.ok : true) modes.push("paper");
|
|
664
|
+
if (c ? c.eligibility.real.ok : false) modes.push("real");
|
|
665
|
+
return {
|
|
666
|
+
partnerId: row.partner_id,
|
|
667
|
+
name,
|
|
668
|
+
logoUrl: c?.logo_url ?? null,
|
|
669
|
+
liquidityHash: row.liquidity_hash ?? null,
|
|
670
|
+
buyId: row.buy_id ?? null,
|
|
671
|
+
price: actionable ? row.price : null,
|
|
672
|
+
americanOdds: row.american_odds ?? null,
|
|
673
|
+
available: actionable ? row.available : null,
|
|
674
|
+
actionable,
|
|
675
|
+
feePacket: row.fee_packet ?? null,
|
|
676
|
+
account: {
|
|
677
|
+
connected: c?.account.connected ?? false,
|
|
678
|
+
status: c?.account.status ?? null,
|
|
679
|
+
credentialStatus: c?.account.credential_status ?? null,
|
|
680
|
+
currency: c?.account.currency ?? null,
|
|
681
|
+
balance: c?.account.balance ?? null,
|
|
682
|
+
expectedBalance: c?.account.expected_balance ?? null,
|
|
683
|
+
stale: c?.account.is_stale ?? true,
|
|
684
|
+
scopeTradeExecute: c?.account.scope_trade_execute ?? true
|
|
685
|
+
},
|
|
686
|
+
supports: {
|
|
687
|
+
orderTypes: c ? [...c.order_types] : ["market"],
|
|
688
|
+
cancel: c?.cancel_supported ?? false,
|
|
689
|
+
modes
|
|
690
|
+
},
|
|
691
|
+
eligibility: { status, issues },
|
|
692
|
+
constraints: c,
|
|
693
|
+
best: false,
|
|
694
|
+
selected: input.selectedPartnerId === row.partner_id,
|
|
695
|
+
display: { price: formatPrice(actionable ? row.price : null, input.oddsFormat, row.american_odds) }
|
|
696
|
+
};
|
|
697
|
+
}
|
|
698
|
+
|
|
699
|
+
// src/sheet.ts
|
|
700
|
+
var TERMINAL_PHASES = /* @__PURE__ */ new Set(["filled", "failed", "cancelled", "settled"]);
|
|
701
|
+
function initialSheetState(o, ref, init = {}) {
|
|
702
|
+
const userMaxPrice = init.maxPrice ?? null;
|
|
703
|
+
return {
|
|
704
|
+
phase: "loading",
|
|
705
|
+
position: "position" in ref ? ref.position : null,
|
|
706
|
+
venues: [],
|
|
707
|
+
venue: null,
|
|
708
|
+
mode: init.mode ?? o.defaults.mode,
|
|
709
|
+
orderType: init.orderType ?? o.defaults.orderType,
|
|
710
|
+
stake: init.stake ? "amount" in init.stake ? { unit: "amount", value: init.stake.amount } : { unit: "shares", value: init.stake.shares } : { unit: o.defaults.stakeUnit, value: null },
|
|
711
|
+
maxPrice: userMaxPrice,
|
|
712
|
+
maxPriceIsAuto: userMaxPrice == null,
|
|
713
|
+
slippage: init.slippage ?? o.defaults.slippage,
|
|
714
|
+
oddsFormat: init.oddsFormat ?? o.defaults.oddsFormat,
|
|
715
|
+
quote: null,
|
|
716
|
+
validation: { ok: false, blocking: [], warnings: [], info: [], all: [] },
|
|
717
|
+
preflight: null,
|
|
718
|
+
preflightStatus: "idle",
|
|
719
|
+
result: null,
|
|
720
|
+
orders: [],
|
|
721
|
+
error: null,
|
|
722
|
+
account: null,
|
|
723
|
+
canPlace: false,
|
|
724
|
+
confirmLabel: "",
|
|
725
|
+
hint: null,
|
|
726
|
+
loading: true,
|
|
727
|
+
updatedAt: o.now()
|
|
728
|
+
};
|
|
729
|
+
}
|
|
730
|
+
function createInertSheet(o, ref, init = {}) {
|
|
731
|
+
const state = initialSheetState(o, ref, init);
|
|
732
|
+
const notReady = async () => ({ status: "blocked", issues: [localIssue("not_ready")] });
|
|
733
|
+
const noop = () => {
|
|
734
|
+
};
|
|
735
|
+
return {
|
|
736
|
+
getState: () => state,
|
|
737
|
+
subscribe: () => noop,
|
|
738
|
+
select: noop,
|
|
739
|
+
set: noop,
|
|
740
|
+
refresh: async () => {
|
|
741
|
+
},
|
|
742
|
+
preflight: async () => null,
|
|
743
|
+
place: notReady,
|
|
744
|
+
confirm: notReady,
|
|
745
|
+
cancelConfirm: noop,
|
|
746
|
+
cancel: async () => ({ cancelled: [], skipped: [] }),
|
|
747
|
+
reset: noop,
|
|
748
|
+
destroy: noop,
|
|
749
|
+
ingestOrderUpdate: noop,
|
|
750
|
+
ingestOrderSettled: noop
|
|
751
|
+
};
|
|
752
|
+
}
|
|
753
|
+
function createSheet(client, ref, init = {}) {
|
|
754
|
+
const o = client.options;
|
|
755
|
+
const listeners = /* @__PURE__ */ new Set();
|
|
756
|
+
let destroyed = false;
|
|
757
|
+
let userMaxPrice = init.maxPrice ?? null;
|
|
758
|
+
let selectedPartnerId = init.partnerId ?? refPartner(ref);
|
|
759
|
+
let preflightTimer = null;
|
|
760
|
+
let preflightSeq = 0;
|
|
761
|
+
let pendingIdempotencyKey = null;
|
|
762
|
+
let confirmation = null;
|
|
763
|
+
let inFlight = null;
|
|
764
|
+
let constraintsStale = false;
|
|
765
|
+
let quoteAt = null;
|
|
766
|
+
let pollTimer = null;
|
|
767
|
+
const trackedIds = /* @__PURE__ */ new Set();
|
|
768
|
+
const offs = [];
|
|
769
|
+
let state = initialSheetState(o, ref, init);
|
|
770
|
+
const emit = () => {
|
|
771
|
+
for (const l of listeners) {
|
|
772
|
+
try {
|
|
773
|
+
l(state);
|
|
774
|
+
} catch (e) {
|
|
775
|
+
console.error("[order-sheet] listener threw", e);
|
|
776
|
+
}
|
|
777
|
+
}
|
|
778
|
+
};
|
|
779
|
+
const patchState = (p) => {
|
|
780
|
+
state = { ...state, ...p, updatedAt: o.now() };
|
|
781
|
+
emit();
|
|
782
|
+
};
|
|
783
|
+
const inputKey = () => JSON.stringify([
|
|
784
|
+
selectedPartnerId,
|
|
785
|
+
state.mode,
|
|
786
|
+
state.orderType,
|
|
787
|
+
state.stake.unit,
|
|
788
|
+
state.stake.value,
|
|
789
|
+
effectiveMaxPrice()
|
|
790
|
+
]);
|
|
791
|
+
function effectiveMaxPrice() {
|
|
792
|
+
const venue2 = currentVenue();
|
|
793
|
+
if (!venue2) return userMaxPrice;
|
|
794
|
+
return resolveMaxPrice(venue2, state.orderType, userMaxPrice, state.slippage);
|
|
795
|
+
}
|
|
796
|
+
function currentVenue() {
|
|
797
|
+
if (!state.position || !selectedPartnerId) return null;
|
|
798
|
+
const venues = buildVenueOptions({
|
|
799
|
+
position: state.position,
|
|
800
|
+
constraints: client.getCachedConstraints(),
|
|
801
|
+
account: state.account,
|
|
802
|
+
mode: state.mode,
|
|
803
|
+
orderType: state.orderType,
|
|
804
|
+
selectedPartnerId,
|
|
805
|
+
oddsFormat: state.oddsFormat
|
|
806
|
+
});
|
|
807
|
+
return venues.find((v) => v.partnerId === selectedPartnerId) ?? null;
|
|
808
|
+
}
|
|
809
|
+
function recompute(extra = {}) {
|
|
810
|
+
if (destroyed) return;
|
|
811
|
+
const next = { ...extra };
|
|
812
|
+
const position = extra.position ?? state.position;
|
|
813
|
+
const account = extra.account ?? state.account;
|
|
814
|
+
const constraints = client.getCachedConstraints();
|
|
815
|
+
const mode = extra.mode ?? state.mode;
|
|
816
|
+
const orderType = extra.orderType ?? state.orderType;
|
|
817
|
+
const stake = extra.stake ?? state.stake;
|
|
818
|
+
const oddsFormat = extra.oddsFormat ?? state.oddsFormat;
|
|
819
|
+
const slippage = extra.slippage ?? state.slippage;
|
|
820
|
+
let venues = [];
|
|
821
|
+
let venue2 = null;
|
|
822
|
+
if (position) {
|
|
823
|
+
venues = buildVenueOptions({ position, constraints, account, mode, orderType, selectedPartnerId, oddsFormat });
|
|
824
|
+
venue2 = venues.find((v) => v.partnerId === selectedPartnerId) ?? null;
|
|
825
|
+
}
|
|
826
|
+
const maxPrice = venue2 ? resolveMaxPrice(venue2, orderType, userMaxPrice, slippage) : userMaxPrice;
|
|
827
|
+
const currency = mode === "paper" ? account?.practiceWallet.currency ?? "ATLAS" : venue2?.account.currency ?? "USD";
|
|
828
|
+
const key = JSON.stringify([selectedPartnerId, mode, orderType, stake.unit, stake.value, maxPrice]);
|
|
829
|
+
const preflight = state.preflight && state.preflight.inputKey === key ? state.preflight : null;
|
|
830
|
+
const quoteAgeMs = quoteAt != null ? o.now() - quoteAt : null;
|
|
831
|
+
let validation = validateSheet({
|
|
832
|
+
account,
|
|
833
|
+
venue: venue2,
|
|
834
|
+
venues,
|
|
835
|
+
mode,
|
|
836
|
+
orderType,
|
|
837
|
+
stake,
|
|
838
|
+
maxPrice,
|
|
839
|
+
realMoneyEnabled: o.realMoney.enabled,
|
|
840
|
+
paperBettable: position ? position.paper_bettable : null,
|
|
841
|
+
quoteAgeMs,
|
|
842
|
+
staleAfterMs: o.defaults.staleAfterMs,
|
|
843
|
+
constraintsStale,
|
|
844
|
+
currency
|
|
845
|
+
});
|
|
846
|
+
if (preflight) validation = mergeServerVerdict(validation, preflight);
|
|
847
|
+
const quote = venue2 ? buildQuote({
|
|
848
|
+
venue: venue2,
|
|
849
|
+
mode,
|
|
850
|
+
orderType,
|
|
851
|
+
stake,
|
|
852
|
+
maxPrice,
|
|
853
|
+
oddsFormat,
|
|
854
|
+
preflight,
|
|
855
|
+
currency,
|
|
856
|
+
stale: quoteAgeMs != null && quoteAgeMs > o.defaults.staleAfterMs
|
|
857
|
+
}) : null;
|
|
858
|
+
if (confirmation && confirmation.key !== key) clearConfirmation();
|
|
859
|
+
const loading = extra.loading ?? state.loading;
|
|
860
|
+
let phase = state.phase;
|
|
861
|
+
if (loading) phase = "loading";
|
|
862
|
+
else if (state.phase === "placing" && !extra.phase) phase = state.phase;
|
|
863
|
+
else if (state.phase === "confirming" && !extra.phase && confirmation) phase = state.phase;
|
|
864
|
+
else if (state.result && TERMINAL_PHASES.has(state.phase) && !extra.phase) phase = state.phase;
|
|
865
|
+
else if (state.result && ["resting", "pending", "partial"].includes(state.phase) && !extra.phase) phase = state.phase;
|
|
866
|
+
else if (extra.phase) phase = extra.phase;
|
|
867
|
+
else if (!venue2 || stake.value == null) phase = "idle";
|
|
868
|
+
else phase = validation.ok && quote ? "ready" : "blocked";
|
|
869
|
+
const hintIssue = validation.info.find((i) => i.code === "better_price_available");
|
|
870
|
+
const best = hintIssue ? venues.find((v) => v.best) ?? null : null;
|
|
871
|
+
next.position = position;
|
|
872
|
+
next.account = account;
|
|
873
|
+
next.venues = venues;
|
|
874
|
+
next.venue = venue2;
|
|
875
|
+
next.mode = mode;
|
|
876
|
+
next.orderType = orderType;
|
|
877
|
+
next.stake = stake;
|
|
878
|
+
next.oddsFormat = oddsFormat;
|
|
879
|
+
next.slippage = slippage;
|
|
880
|
+
next.maxPrice = maxPrice;
|
|
881
|
+
next.maxPriceIsAuto = userMaxPrice == null;
|
|
882
|
+
next.quote = quote;
|
|
883
|
+
next.validation = validation;
|
|
884
|
+
next.preflight = preflight;
|
|
885
|
+
next.preflightStatus = preflight ? state.preflightStatus : state.preflightStatus === "pending" ? "pending" : "idle";
|
|
886
|
+
next.phase = phase;
|
|
887
|
+
next.canPlace = (phase === "ready" || phase === "confirming") && validation.ok && !!quote;
|
|
888
|
+
next.confirmLabel = confirmLabel(phase, mode, venue2, quote?.display.cost ?? null, o);
|
|
889
|
+
next.hint = best && hintIssue ? { code: "better_price_available", venue: best, message: hintIssue.message } : null;
|
|
890
|
+
next.loading = loading;
|
|
891
|
+
patchState(next);
|
|
892
|
+
}
|
|
893
|
+
function mergeServerVerdict(v, pf) {
|
|
894
|
+
const leg = pf.leg;
|
|
895
|
+
const all = [];
|
|
896
|
+
if (!leg.accepted && leg.rejected_code) {
|
|
897
|
+
all.push({
|
|
898
|
+
code: leg.rejected_code,
|
|
899
|
+
severity: "block",
|
|
900
|
+
message: leg.message_user ?? localIssue(leg.rejected_code, { detail: leg.rejected_reason ?? void 0 }).message,
|
|
901
|
+
detail: leg.rejected_reason,
|
|
902
|
+
fix: leg.fix ?? localIssue(leg.rejected_code).fix,
|
|
903
|
+
scope: "leg",
|
|
904
|
+
source: "server"
|
|
905
|
+
});
|
|
906
|
+
}
|
|
907
|
+
for (const w of leg.warnings ?? []) all.push(fromServerIssue(w));
|
|
908
|
+
const serverCodes = new Set(all.map((i) => i.code));
|
|
909
|
+
for (const i of v.all) if (!serverCodes.has(i.code) || i.source === "server") all.push(i);
|
|
910
|
+
const blocking = all.filter((i) => i.severity === "block");
|
|
911
|
+
return { ok: blocking.length === 0, blocking, warnings: all.filter((i) => i.severity === "warn"), info: all.filter((i) => i.severity === "info"), all };
|
|
912
|
+
}
|
|
913
|
+
async function load() {
|
|
914
|
+
patchState({ loading: true, error: null });
|
|
915
|
+
try {
|
|
916
|
+
const [loaded, constraints] = await Promise.all([client.loadPosition(ref), client.constraints().catch(() => null)]);
|
|
917
|
+
constraintsStale = client.constraintsStale();
|
|
918
|
+
if (!selectedPartnerId && loaded.partnerId) selectedPartnerId = loaded.partnerId;
|
|
919
|
+
quoteAt = o.now();
|
|
920
|
+
const account = constraints ? client.accountFromConstraints(constraints) : await client.account().catch(() => null);
|
|
921
|
+
recompute({ position: loaded.position, account, loading: false });
|
|
922
|
+
scheduleAutoPreflight();
|
|
923
|
+
} catch (e) {
|
|
924
|
+
const err = isOrderSheetError(e) ? e : new OrderSheetError({ code: "network_error", message: e?.message ?? String(e), cause: e });
|
|
925
|
+
patchState({ loading: false, phase: "blocked", error: err });
|
|
926
|
+
}
|
|
927
|
+
}
|
|
928
|
+
function scheduleAutoPreflight() {
|
|
929
|
+
if (!o.preflight.auto) return;
|
|
930
|
+
if (preflightTimer) {
|
|
931
|
+
o.timers.clear(preflightTimer);
|
|
932
|
+
preflightTimer = null;
|
|
933
|
+
}
|
|
934
|
+
if (!state.validation.ok || !state.quote) return;
|
|
935
|
+
if (state.preflight && state.preflight.inputKey === inputKey()) return;
|
|
936
|
+
preflightTimer = o.timers.set(() => {
|
|
937
|
+
preflightTimer = null;
|
|
938
|
+
void runPreflight({});
|
|
939
|
+
}, o.preflight.debounceMs);
|
|
940
|
+
}
|
|
941
|
+
async function runPreflight(opts) {
|
|
942
|
+
const venue2 = state.venue;
|
|
943
|
+
const leg = buildLeg();
|
|
944
|
+
if (!venue2 || !leg) return null;
|
|
945
|
+
const key = inputKey();
|
|
946
|
+
const seq = ++preflightSeq;
|
|
947
|
+
patchState({ preflightStatus: "pending" });
|
|
948
|
+
try {
|
|
949
|
+
const res = await client.preflight([leg], { verifyLiquidity: opts.verifyLiquidity });
|
|
950
|
+
if (seq !== preflightSeq || destroyed) return null;
|
|
951
|
+
const snapshot = { inputKey: key, at: o.now(), leg: res.results[0], haltedAtIndex: res.halted_at_index ?? null };
|
|
952
|
+
state = { ...state, preflight: snapshot, preflightStatus: "done", error: null };
|
|
953
|
+
recompute();
|
|
954
|
+
return snapshot;
|
|
955
|
+
} catch (e) {
|
|
956
|
+
if (seq !== preflightSeq || destroyed) return null;
|
|
957
|
+
const err = isOrderSheetError(e) ? e : new OrderSheetError({ code: "network_error", message: String(e?.message ?? e), cause: e });
|
|
958
|
+
patchState({ preflightStatus: "error", error: err });
|
|
959
|
+
recompute();
|
|
960
|
+
return null;
|
|
961
|
+
}
|
|
962
|
+
}
|
|
963
|
+
function buildLeg() {
|
|
964
|
+
const venue2 = state.venue;
|
|
965
|
+
const maxPrice = state.maxPrice;
|
|
966
|
+
if (!venue2 || state.stake.value == null || maxPrice == null) return null;
|
|
967
|
+
const leg = {
|
|
968
|
+
max_price: maxPrice,
|
|
969
|
+
// ALWAYS explicit. The API defaults an omitted mode to REAL.
|
|
970
|
+
mode: state.mode,
|
|
971
|
+
order_type: state.orderType,
|
|
972
|
+
...state.stake.unit === "amount" ? { amount: state.stake.value } : { shares: state.stake.value }
|
|
973
|
+
};
|
|
974
|
+
if (venue2.liquidityHash) leg.liquidity_hash = venue2.liquidityHash;
|
|
975
|
+
else if (state.position) {
|
|
976
|
+
leg.position_hash = state.position.position_hash;
|
|
977
|
+
leg.partner_id = venue2.partnerId;
|
|
978
|
+
} else return null;
|
|
979
|
+
return leg;
|
|
980
|
+
}
|
|
981
|
+
function clearConfirmation() {
|
|
982
|
+
confirmation = null;
|
|
983
|
+
pendingIdempotencyKey = null;
|
|
984
|
+
}
|
|
985
|
+
function place(opts = {}) {
|
|
986
|
+
if (inFlight) return inFlight;
|
|
987
|
+
return track(placeOrConfirm(opts, false));
|
|
988
|
+
}
|
|
989
|
+
function confirm() {
|
|
990
|
+
if (inFlight) return inFlight;
|
|
991
|
+
return track(placeOrConfirm({}, true));
|
|
992
|
+
}
|
|
993
|
+
function track(p) {
|
|
994
|
+
inFlight = p;
|
|
995
|
+
const done = () => {
|
|
996
|
+
if (inFlight === p) inFlight = null;
|
|
997
|
+
};
|
|
998
|
+
p.then(done, done);
|
|
999
|
+
return p;
|
|
1000
|
+
}
|
|
1001
|
+
async function placeOrConfirm(opts, viaConfirm) {
|
|
1002
|
+
if (preflightTimer) {
|
|
1003
|
+
o.timers.clear(preflightTimer);
|
|
1004
|
+
preflightTimer = null;
|
|
1005
|
+
}
|
|
1006
|
+
recompute();
|
|
1007
|
+
const venue2 = state.venue;
|
|
1008
|
+
const leg = buildLeg();
|
|
1009
|
+
const key = inputKey();
|
|
1010
|
+
const confirmedNow = state.phase === "confirming" && confirmation !== null && confirmation.key === key;
|
|
1011
|
+
if (viaConfirm && !confirmedNow) {
|
|
1012
|
+
if (state.phase === "confirming") {
|
|
1013
|
+
clearConfirmation();
|
|
1014
|
+
recompute();
|
|
1015
|
+
}
|
|
1016
|
+
return { status: "blocked", issues: [localIssue("confirm_expired")] };
|
|
1017
|
+
}
|
|
1018
|
+
if (!state.validation.ok || !state.quote || !venue2 || !leg) {
|
|
1019
|
+
return { status: "blocked", issues: state.validation.blocking.length ? state.validation.blocking : [localIssue("no_stake")] };
|
|
1020
|
+
}
|
|
1021
|
+
if (state.mode === "real" && o.realMoney.confirm && !confirmedNow) {
|
|
1022
|
+
confirmation = { key };
|
|
1023
|
+
pendingIdempotencyKey = opts.idempotencyKey ?? newIdempotencyKey();
|
|
1024
|
+
const issue = localIssue("confirm_required", { venue: venue2.name, amount: state.quote.cost, currency: state.quote.currency });
|
|
1025
|
+
recompute({ phase: "confirming" });
|
|
1026
|
+
return { status: "confirm_required", issues: [issue] };
|
|
1027
|
+
}
|
|
1028
|
+
const idempotencyKey = opts.idempotencyKey ?? pendingIdempotencyKey ?? newIdempotencyKey();
|
|
1029
|
+
confirmation = null;
|
|
1030
|
+
pendingIdempotencyKey = idempotencyKey;
|
|
1031
|
+
recompute({ phase: "placing" });
|
|
1032
|
+
let raw;
|
|
1033
|
+
try {
|
|
1034
|
+
raw = await client.buy([leg], { idempotencyKey, verifyLiquidity: opts.verifyLiquidity });
|
|
1035
|
+
} catch (e) {
|
|
1036
|
+
const err = isOrderSheetError(e) ? e : new OrderSheetError({ code: "network_error", message: String(e?.message ?? e), cause: e });
|
|
1037
|
+
if (err.retryable) patchState({ error: err, phase: "failed" });
|
|
1038
|
+
else {
|
|
1039
|
+
pendingIdempotencyKey = null;
|
|
1040
|
+
state = { ...state, error: err };
|
|
1041
|
+
recompute({ phase: "blocked" });
|
|
1042
|
+
}
|
|
1043
|
+
throw err;
|
|
1044
|
+
}
|
|
1045
|
+
pendingIdempotencyKey = null;
|
|
1046
|
+
const result = toPlaceResult(raw, venue2, state.account, idempotencyKey, o.now());
|
|
1047
|
+
for (const l of result.legs) for (const id of l.routerOrderIds) trackedIds.add(id);
|
|
1048
|
+
const orders = result.legs.flatMap((l) => l.routerOrderIds.map((id) => ({
|
|
1049
|
+
routerOrderId: id,
|
|
1050
|
+
partnerId: l.routedPartnerIds[0] ?? l.partnerId ?? "",
|
|
1051
|
+
status: l.status === "completed" ? "closed" : "open",
|
|
1052
|
+
fillStatus: l.status === "completed" ? l.fill.unfilled > 5e-3 ? "partially_filled" : "filled" : "unfilled",
|
|
1053
|
+
filledAmt: l.fill.notional,
|
|
1054
|
+
unfilledAmt: l.fill.unfilled,
|
|
1055
|
+
avgPrice: l.fill.avgPrice,
|
|
1056
|
+
settled: null,
|
|
1057
|
+
updatedAt: new Date(o.now()).toISOString()
|
|
1058
|
+
})));
|
|
1059
|
+
state = { ...state, result, orders, error: null };
|
|
1060
|
+
recompute({ phase: phaseFromSummary(result.summary) });
|
|
1061
|
+
if (result.summary === "resting" || result.summary === "pending") followOrders().catch(() => {
|
|
1062
|
+
});
|
|
1063
|
+
return { status: "placed", result };
|
|
1064
|
+
}
|
|
1065
|
+
async function followOrders() {
|
|
1066
|
+
if (credentialSupportsStream(o.auth) && o.stream.enabled) {
|
|
1067
|
+
await client.ensureStream();
|
|
1068
|
+
return;
|
|
1069
|
+
}
|
|
1070
|
+
if (pollTimer || !o.stream.pollIntervalMs) return;
|
|
1071
|
+
const tick = async () => {
|
|
1072
|
+
pollTimer = null;
|
|
1073
|
+
if (destroyed || !state.position || trackedIds.size === 0 || TERMINAL_PHASES.has(state.phase)) return;
|
|
1074
|
+
try {
|
|
1075
|
+
const rows = await client.listOrders({ contestId: state.position.contest_id });
|
|
1076
|
+
for (const r of rows) if (trackedIds.has(r.router_order_id)) applyOrderRow(r);
|
|
1077
|
+
} catch {
|
|
1078
|
+
}
|
|
1079
|
+
if (!destroyed && !TERMINAL_PHASES.has(state.phase)) pollTimer = o.timers.set(tick, o.stream.pollIntervalMs);
|
|
1080
|
+
};
|
|
1081
|
+
pollTimer = o.timers.set(tick, o.stream.pollIntervalMs);
|
|
1082
|
+
}
|
|
1083
|
+
function applyOrderRow(r) {
|
|
1084
|
+
ingestOrderUpdate({
|
|
1085
|
+
type: "order.update",
|
|
1086
|
+
router_order_id: r.router_order_id,
|
|
1087
|
+
partner_id: r.partner_id,
|
|
1088
|
+
router_contest_id: r.contest_id,
|
|
1089
|
+
status: r.status,
|
|
1090
|
+
fill_status: r.fill_status ?? "unfilled",
|
|
1091
|
+
filled_amt: r.filled_amt ?? 0,
|
|
1092
|
+
unfilled_amt: r.unfilled_amt ?? 0,
|
|
1093
|
+
avg_price: null,
|
|
1094
|
+
timestamp: new Date(o.now()).toISOString()
|
|
1095
|
+
});
|
|
1096
|
+
if (r.result) ingestOrderSettled({
|
|
1097
|
+
type: "order.settled",
|
|
1098
|
+
router_order_id: r.router_order_id,
|
|
1099
|
+
partner_id: r.partner_id,
|
|
1100
|
+
router_contest_id: r.contest_id,
|
|
1101
|
+
result: r.result,
|
|
1102
|
+
winnings: r.winnings ?? 0,
|
|
1103
|
+
net_winnings: r.net_winnings ?? 0,
|
|
1104
|
+
timestamp: new Date(o.now()).toISOString()
|
|
1105
|
+
});
|
|
1106
|
+
}
|
|
1107
|
+
function ingestOrderUpdate(frame) {
|
|
1108
|
+
if (!trackedIds.has(frame.router_order_id)) return;
|
|
1109
|
+
const orders = state.orders.map((t) => t.routerOrderId === frame.router_order_id ? { ...t, status: frame.status, fillStatus: frame.fill_status, filledAmt: frame.filled_amt, unfilledAmt: frame.unfilled_amt, avgPrice: frame.avg_price ?? t.avgPrice, updatedAt: frame.timestamp } : t);
|
|
1110
|
+
state = { ...state, orders };
|
|
1111
|
+
recompute({ phase: phaseFromOrders(orders, state.phase) });
|
|
1112
|
+
}
|
|
1113
|
+
function ingestOrderSettled(frame) {
|
|
1114
|
+
if (!trackedIds.has(frame.router_order_id)) return;
|
|
1115
|
+
const orders = state.orders.map((t) => t.routerOrderId === frame.router_order_id ? { ...t, settled: { result: frame.result, winnings: frame.winnings, netWinnings: frame.net_winnings }, updatedAt: frame.timestamp } : t);
|
|
1116
|
+
state = { ...state, orders };
|
|
1117
|
+
recompute({ phase: orders.every((t) => t.settled) ? "settled" : state.phase });
|
|
1118
|
+
}
|
|
1119
|
+
offs.push(client.events.on("order.update", ingestOrderUpdate));
|
|
1120
|
+
offs.push(client.events.on("order.settled", ingestOrderSettled));
|
|
1121
|
+
offs.push(client.events.on("constraints", () => {
|
|
1122
|
+
constraintsStale = client.constraintsStale();
|
|
1123
|
+
recompute();
|
|
1124
|
+
scheduleAutoPreflight();
|
|
1125
|
+
}));
|
|
1126
|
+
offs.push(client.events.on("stream.connected", () => {
|
|
1127
|
+
void refetchTracked();
|
|
1128
|
+
}));
|
|
1129
|
+
async function refetchTracked() {
|
|
1130
|
+
if (destroyed || !state.position || trackedIds.size === 0 || TERMINAL_PHASES.has(state.phase)) return;
|
|
1131
|
+
try {
|
|
1132
|
+
const rows = await client.listOrders({ contestId: state.position.contest_id });
|
|
1133
|
+
if (destroyed) return;
|
|
1134
|
+
for (const r of rows) if (trackedIds.has(r.router_order_id)) applyOrderRow(r);
|
|
1135
|
+
} catch {
|
|
1136
|
+
}
|
|
1137
|
+
}
|
|
1138
|
+
void load();
|
|
1139
|
+
return {
|
|
1140
|
+
getState: () => state,
|
|
1141
|
+
subscribe(l) {
|
|
1142
|
+
listeners.add(l);
|
|
1143
|
+
return () => {
|
|
1144
|
+
listeners.delete(l);
|
|
1145
|
+
};
|
|
1146
|
+
},
|
|
1147
|
+
select(partnerId) {
|
|
1148
|
+
if (state.phase === "placing") return;
|
|
1149
|
+
clearConfirmation();
|
|
1150
|
+
selectedPartnerId = partnerId;
|
|
1151
|
+
if (state.maxPriceIsAuto) userMaxPrice = null;
|
|
1152
|
+
state = { ...state, preflight: null, preflightStatus: "idle" };
|
|
1153
|
+
recompute();
|
|
1154
|
+
scheduleAutoPreflight();
|
|
1155
|
+
},
|
|
1156
|
+
set(patch) {
|
|
1157
|
+
const next = {};
|
|
1158
|
+
if (patch.stake !== void 0) {
|
|
1159
|
+
next.stake = patch.stake === null ? { unit: state.stake.unit, value: null } : "amount" in patch.stake ? { unit: "amount", value: patch.stake.amount } : { unit: "shares", value: patch.stake.shares };
|
|
1160
|
+
}
|
|
1161
|
+
if (patch.maxPrice !== void 0) userMaxPrice = patch.maxPrice;
|
|
1162
|
+
if (patch.orderType !== void 0) {
|
|
1163
|
+
next.orderType = patch.orderType;
|
|
1164
|
+
if (state.maxPriceIsAuto) userMaxPrice = null;
|
|
1165
|
+
}
|
|
1166
|
+
if (patch.mode !== void 0) next.mode = patch.mode;
|
|
1167
|
+
if (patch.oddsFormat !== void 0) next.oddsFormat = patch.oddsFormat;
|
|
1168
|
+
if (patch.slippage !== void 0) next.slippage = patch.slippage;
|
|
1169
|
+
clearConfirmation();
|
|
1170
|
+
state = { ...state, error: null };
|
|
1171
|
+
recompute(next);
|
|
1172
|
+
scheduleAutoPreflight();
|
|
1173
|
+
},
|
|
1174
|
+
refresh: load,
|
|
1175
|
+
preflight: (opts = {}) => runPreflight(opts),
|
|
1176
|
+
place,
|
|
1177
|
+
confirm,
|
|
1178
|
+
cancelConfirm() {
|
|
1179
|
+
if (state.phase === "placing") return;
|
|
1180
|
+
clearConfirmation();
|
|
1181
|
+
recompute();
|
|
1182
|
+
},
|
|
1183
|
+
async cancel() {
|
|
1184
|
+
const ids = state.orders.filter((t) => t.fillStatus !== "filled" && !t.settled).map((t) => t.routerOrderId);
|
|
1185
|
+
if (ids.length === 0) return { cancelled: [], skipped: [] };
|
|
1186
|
+
const res = await client.cancelOrders(ids);
|
|
1187
|
+
const cancelled = res.cancelled.map((c) => c.router_order_id);
|
|
1188
|
+
const orders = state.orders.map((t) => cancelled.includes(t.routerOrderId) ? { ...t, status: "cancelled", updatedAt: new Date(o.now()).toISOString() } : t);
|
|
1189
|
+
state = { ...state, orders };
|
|
1190
|
+
recompute({ phase: cancelled.length > 0 && orders.every((t) => t.status === "cancelled" || t.fillStatus === "filled") ? "cancelled" : state.phase });
|
|
1191
|
+
return { cancelled, skipped: [...res.skipped.map((s) => s.router_order_id), ...res.errors.map((e) => e.router_order_id)] };
|
|
1192
|
+
},
|
|
1193
|
+
reset() {
|
|
1194
|
+
if (state.phase === "placing") return;
|
|
1195
|
+
clearConfirmation();
|
|
1196
|
+
userMaxPrice = init.maxPrice ?? null;
|
|
1197
|
+
trackedIds.clear();
|
|
1198
|
+
state = { ...state, stake: { unit: state.stake.unit, value: null }, result: null, orders: [], preflight: null, preflightStatus: "idle", error: null };
|
|
1199
|
+
recompute({ phase: "idle" });
|
|
1200
|
+
},
|
|
1201
|
+
destroy() {
|
|
1202
|
+
if (destroyed) return;
|
|
1203
|
+
destroyed = true;
|
|
1204
|
+
clearConfirmation();
|
|
1205
|
+
listeners.clear();
|
|
1206
|
+
for (const off of offs) off();
|
|
1207
|
+
if (preflightTimer) o.timers.clear(preflightTimer);
|
|
1208
|
+
if (pollTimer) o.timers.clear(pollTimer);
|
|
1209
|
+
},
|
|
1210
|
+
ingestOrderUpdate,
|
|
1211
|
+
ingestOrderSettled
|
|
1212
|
+
};
|
|
1213
|
+
}
|
|
1214
|
+
function refPartner(ref) {
|
|
1215
|
+
if ("liquidityHash" in ref) return ref.liquidityHash.split(":")[0] || null;
|
|
1216
|
+
if ("partnerId" in ref && ref.partnerId) return ref.partnerId;
|
|
1217
|
+
return null;
|
|
1218
|
+
}
|
|
1219
|
+
function newIdempotencyKey() {
|
|
1220
|
+
const c = globalThis.crypto;
|
|
1221
|
+
if (c?.randomUUID) return `oms_${c.randomUUID()}`;
|
|
1222
|
+
return `oms_${Date.now().toString(36)}_${Math.random().toString(36).slice(2, 12)}`;
|
|
1223
|
+
}
|
|
1224
|
+
function confirmLabel(phase, mode, venue2, cost, o) {
|
|
1225
|
+
const L = o.labels;
|
|
1226
|
+
if (phase === "placing") return L.placing;
|
|
1227
|
+
if (phase === "confirming") return `${L.confirmReal}${cost ? ` \xB7 ${cost}` : ""}${venue2 ? ` on ${venue2.name}` : ""}`;
|
|
1228
|
+
const verb = mode === "real" ? L.placeReal : L.placePractice;
|
|
1229
|
+
return cost && phase !== "idle" ? `${verb} \xB7 ${cost}` : verb;
|
|
1230
|
+
}
|
|
1231
|
+
function phaseFromSummary(summary) {
|
|
1232
|
+
switch (summary) {
|
|
1233
|
+
case "filled":
|
|
1234
|
+
return "filled";
|
|
1235
|
+
case "partial":
|
|
1236
|
+
return "partial";
|
|
1237
|
+
case "resting":
|
|
1238
|
+
return "resting";
|
|
1239
|
+
case "pending":
|
|
1240
|
+
return "pending";
|
|
1241
|
+
case "failed":
|
|
1242
|
+
return "failed";
|
|
1243
|
+
}
|
|
1244
|
+
}
|
|
1245
|
+
function phaseFromOrders(orders, current) {
|
|
1246
|
+
if (orders.length === 0) return current;
|
|
1247
|
+
if (orders.every((t) => t.settled)) return "settled";
|
|
1248
|
+
if (orders.every((t) => t.fillStatus === "filled")) return "filled";
|
|
1249
|
+
if (orders.every((t) => t.status === "cancelled")) return orders.some((t) => t.filledAmt > 0) ? "partial" : "cancelled";
|
|
1250
|
+
if (orders.some((t) => t.status === "cancelled" || t.status === "closed")) {
|
|
1251
|
+
return orders.some((t) => t.filledAmt > 0) ? "partial" : "failed";
|
|
1252
|
+
}
|
|
1253
|
+
if (orders.some((t) => t.fillStatus === "partially_filled")) return "partial";
|
|
1254
|
+
return current === "pending" ? "pending" : "resting";
|
|
1255
|
+
}
|
|
1256
|
+
function toPlaceResult(raw, venue2, account, idempotencyKey, now) {
|
|
1257
|
+
const practiceBook = account?.practiceWallet.partnerId ?? null;
|
|
1258
|
+
const legs = raw.results.map((r) => {
|
|
1259
|
+
const routed = r.venues.map((v) => v.partner_id);
|
|
1260
|
+
const isPaperBook = routed.length > 0 && practiceBook != null && routed.every((p) => p === practiceBook);
|
|
1261
|
+
const substituted = !!r.requested_partner_id && routed.length > 0 && routed.some((p) => p !== r.requested_partner_id) && !isPaperBook;
|
|
1262
|
+
const issue = r.rejected_code ? {
|
|
1263
|
+
code: r.rejected_code,
|
|
1264
|
+
severity: "block",
|
|
1265
|
+
scope: "leg",
|
|
1266
|
+
source: "server",
|
|
1267
|
+
message: r.message_user ?? localIssue(r.rejected_code, { venue: venue2.name, detail: r.rejected_reason ?? void 0 }).message,
|
|
1268
|
+
detail: r.rejected_reason,
|
|
1269
|
+
fix: r.fix ?? localIssue(r.rejected_code).fix
|
|
1270
|
+
} : r.status === "failed" || r.status === "rejected" ? { code: "unknown", severity: "block", scope: "leg", source: "server", message: r.rejected_reason ?? "The venue declined this order.", detail: r.rejected_reason, fix: "retry" } : null;
|
|
1271
|
+
return {
|
|
1272
|
+
index: r.index,
|
|
1273
|
+
status: r.status,
|
|
1274
|
+
partnerId: r.requested_partner_id,
|
|
1275
|
+
routedPartnerIds: routed,
|
|
1276
|
+
substituted,
|
|
1277
|
+
routerOrderIds: r.venues.map((v) => v.router_order_id).filter((id) => !!id),
|
|
1278
|
+
fill: { notional: r.filled_notional, shares: r.filled_shares, avgPrice: r.avg_price, unfilled: r.unfilled_notional },
|
|
1279
|
+
issue,
|
|
1280
|
+
sorDecisionId: r.sor_decision_id
|
|
1281
|
+
};
|
|
1282
|
+
});
|
|
1283
|
+
return { summary: summarize(legs), legs, opportunityId: raw.opportunity_id, idempotencyKey, placedAt: now, raw };
|
|
1284
|
+
}
|
|
1285
|
+
function summarize(legs) {
|
|
1286
|
+
if (legs.length === 0) return "failed";
|
|
1287
|
+
if (legs.some((l) => l.status === "failed" || l.status === "rejected")) {
|
|
1288
|
+
return legs.some((l) => l.fill.notional > 0) ? "partial" : "failed";
|
|
1289
|
+
}
|
|
1290
|
+
if (legs.some((l) => l.status === "resting")) return "resting";
|
|
1291
|
+
if (legs.some((l) => l.status === "pending")) return "pending";
|
|
1292
|
+
return legs.some((l) => l.fill.unfilled > 5e-3) ? "partial" : "filled";
|
|
1293
|
+
}
|
|
1294
|
+
|
|
1295
|
+
// src/http.ts
|
|
1296
|
+
var RETRYABLE_STATUS = /* @__PURE__ */ new Set([408, 425, 429, 500, 502, 503, 504]);
|
|
1297
|
+
function createHttp(options) {
|
|
1298
|
+
const baseUrl = options.baseUrl.replace(/\/+$/, "");
|
|
1299
|
+
const doFetch = options.fetch ?? ((input, init) => globalThis.fetch(input, init));
|
|
1300
|
+
async function request(method, path, opts = {}) {
|
|
1301
|
+
const cred = await resolveCredential(options.auth);
|
|
1302
|
+
const headers = {
|
|
1303
|
+
Accept: "application/json",
|
|
1304
|
+
...cred.headers,
|
|
1305
|
+
...options.actAs ? { "X-OpenMarkets-Account": options.actAs } : {},
|
|
1306
|
+
...opts.body !== void 0 ? { "Content-Type": "application/json" } : {},
|
|
1307
|
+
...opts.headers ?? {}
|
|
1308
|
+
};
|
|
1309
|
+
let res;
|
|
1310
|
+
try {
|
|
1311
|
+
res = await doFetch(`${baseUrl}${path}`, {
|
|
1312
|
+
method,
|
|
1313
|
+
headers,
|
|
1314
|
+
body: opts.body !== void 0 ? JSON.stringify(opts.body) : void 0,
|
|
1315
|
+
signal: opts.signal
|
|
1316
|
+
});
|
|
1317
|
+
} catch (cause) {
|
|
1318
|
+
if (cause?.name === "AbortError") throw cause;
|
|
1319
|
+
throw new OrderSheetError({ code: "network_error", message: `Could not reach ${baseUrl}: ${cause?.message ?? String(cause)}`, retryable: true, cause });
|
|
1320
|
+
}
|
|
1321
|
+
let parsed = null;
|
|
1322
|
+
const text = await res.text();
|
|
1323
|
+
if (text) {
|
|
1324
|
+
try {
|
|
1325
|
+
parsed = JSON.parse(text);
|
|
1326
|
+
} catch {
|
|
1327
|
+
throw new OrderSheetError({ code: "invalid_response", message: `Non-JSON response (${res.status}) from ${method} ${path}`, status: res.status, retryable: res.status >= 500 });
|
|
1328
|
+
}
|
|
1329
|
+
}
|
|
1330
|
+
if (!res.ok) {
|
|
1331
|
+
const err = parsed?.error;
|
|
1332
|
+
throw new OrderSheetError({
|
|
1333
|
+
code: err?.code ?? `http_${res.status}`,
|
|
1334
|
+
message: err?.message ?? `${method} ${path} failed with ${res.status}`,
|
|
1335
|
+
status: res.status,
|
|
1336
|
+
details: err?.details ?? null,
|
|
1337
|
+
retryable: RETRYABLE_STATUS.has(res.status)
|
|
1338
|
+
});
|
|
1339
|
+
}
|
|
1340
|
+
const env = parsed;
|
|
1341
|
+
if (!env || typeof env !== "object" || !("data" in env)) {
|
|
1342
|
+
throw new OrderSheetError({ code: "invalid_response", message: `Unexpected response shape from ${method} ${path}`, status: res.status });
|
|
1343
|
+
}
|
|
1344
|
+
return env.data;
|
|
1345
|
+
}
|
|
1346
|
+
return { request, baseUrl };
|
|
1347
|
+
}
|
|
1348
|
+
|
|
1349
|
+
// src/stream.ts
|
|
1350
|
+
function createOrderStream(options) {
|
|
1351
|
+
const channels = options.channels ?? ["orders", "balance"];
|
|
1352
|
+
const timers = options.timers ?? { set: setTimeout, clear: clearTimeout };
|
|
1353
|
+
const backoff = { initialMs: 1e3, maxMs: 3e4, maxAttempts: Infinity, ...options.backoff ?? {} };
|
|
1354
|
+
const handlers = /* @__PURE__ */ new Map();
|
|
1355
|
+
const balanceVersions = /* @__PURE__ */ new Map();
|
|
1356
|
+
let ws = null;
|
|
1357
|
+
let status = "idle";
|
|
1358
|
+
let attempts = 0;
|
|
1359
|
+
let wantOpen = false;
|
|
1360
|
+
let reconnectTimer = null;
|
|
1361
|
+
let pingTimer = null;
|
|
1362
|
+
const emit = (event, payload) => {
|
|
1363
|
+
for (const h of handlers.get(event) ?? []) {
|
|
1364
|
+
try {
|
|
1365
|
+
h(payload);
|
|
1366
|
+
} catch (e) {
|
|
1367
|
+
console.error("[order-sheet] stream handler threw", e);
|
|
1368
|
+
}
|
|
1369
|
+
}
|
|
1370
|
+
};
|
|
1371
|
+
const setStatus = (s) => {
|
|
1372
|
+
if (s !== status) {
|
|
1373
|
+
status = s;
|
|
1374
|
+
emit("status", s);
|
|
1375
|
+
}
|
|
1376
|
+
};
|
|
1377
|
+
const stopPing = () => {
|
|
1378
|
+
if (pingTimer) {
|
|
1379
|
+
timers.clear(pingTimer);
|
|
1380
|
+
pingTimer = null;
|
|
1381
|
+
}
|
|
1382
|
+
};
|
|
1383
|
+
const schedulePing = () => {
|
|
1384
|
+
stopPing();
|
|
1385
|
+
const every = options.pingIntervalMs ?? 25e3;
|
|
1386
|
+
if (!(every > 0)) return;
|
|
1387
|
+
pingTimer = timers.set(() => {
|
|
1388
|
+
if (ws && ws.readyState === 1) {
|
|
1389
|
+
try {
|
|
1390
|
+
ws.send(JSON.stringify({ action: "ping" }));
|
|
1391
|
+
} catch {
|
|
1392
|
+
}
|
|
1393
|
+
}
|
|
1394
|
+
schedulePing();
|
|
1395
|
+
}, every);
|
|
1396
|
+
};
|
|
1397
|
+
function ingest(frame) {
|
|
1398
|
+
switch (frame.type) {
|
|
1399
|
+
case "connected": {
|
|
1400
|
+
attempts = 0;
|
|
1401
|
+
for (const channel of channels) ws?.send(JSON.stringify({ action: "subscribe", channel }));
|
|
1402
|
+
emit("connected", { server_time: frame.server_time ?? null });
|
|
1403
|
+
return;
|
|
1404
|
+
}
|
|
1405
|
+
case "order.update":
|
|
1406
|
+
emit("order.update", frame);
|
|
1407
|
+
return;
|
|
1408
|
+
case "order.settled":
|
|
1409
|
+
emit("order.settled", frame);
|
|
1410
|
+
return;
|
|
1411
|
+
case "balance.update": {
|
|
1412
|
+
const f = frame;
|
|
1413
|
+
const last = balanceVersions.get(f.account_partner_id);
|
|
1414
|
+
if (last != null && typeof f.balance_version === "number" && f.balance_version <= last) return;
|
|
1415
|
+
if (typeof f.balance_version === "number") balanceVersions.set(f.account_partner_id, f.balance_version);
|
|
1416
|
+
emit("balance.update", f);
|
|
1417
|
+
return;
|
|
1418
|
+
}
|
|
1419
|
+
case "error": {
|
|
1420
|
+
const f = frame;
|
|
1421
|
+
emit("error", { code: f.code ?? "stream_error", message: f.message ?? "stream error" });
|
|
1422
|
+
return;
|
|
1423
|
+
}
|
|
1424
|
+
default:
|
|
1425
|
+
return;
|
|
1426
|
+
}
|
|
1427
|
+
}
|
|
1428
|
+
async function open() {
|
|
1429
|
+
const Ctor = options.WebSocket ?? globalThis.WebSocket;
|
|
1430
|
+
if (!Ctor) {
|
|
1431
|
+
emit("error", { code: "stream_unavailable", message: "No WebSocket implementation available." });
|
|
1432
|
+
setStatus("closed");
|
|
1433
|
+
return;
|
|
1434
|
+
}
|
|
1435
|
+
const key = await options.getApiKey();
|
|
1436
|
+
if (!key) {
|
|
1437
|
+
emit("error", { code: "no_credential", message: "The stream needs an API key." });
|
|
1438
|
+
setStatus("closed");
|
|
1439
|
+
return;
|
|
1440
|
+
}
|
|
1441
|
+
const sep = options.url.includes("?") ? "&" : "?";
|
|
1442
|
+
setStatus("connecting");
|
|
1443
|
+
const socket = new Ctor(`${options.url}${sep}api_key=${encodeURIComponent(key)}`);
|
|
1444
|
+
ws = socket;
|
|
1445
|
+
socket.onopen = () => {
|
|
1446
|
+
setStatus("open");
|
|
1447
|
+
schedulePing();
|
|
1448
|
+
};
|
|
1449
|
+
socket.onmessage = (ev) => {
|
|
1450
|
+
let frame = null;
|
|
1451
|
+
try {
|
|
1452
|
+
frame = JSON.parse(String(ev.data));
|
|
1453
|
+
} catch {
|
|
1454
|
+
return;
|
|
1455
|
+
}
|
|
1456
|
+
if (frame && typeof frame === "object" && typeof frame.type === "string") ingest(frame);
|
|
1457
|
+
};
|
|
1458
|
+
socket.onerror = () => {
|
|
1459
|
+
};
|
|
1460
|
+
socket.onclose = () => {
|
|
1461
|
+
if (ws !== socket) return;
|
|
1462
|
+
ws = null;
|
|
1463
|
+
stopPing();
|
|
1464
|
+
const willRetry = wantOpen && attempts < backoff.maxAttempts;
|
|
1465
|
+
setStatus("closed");
|
|
1466
|
+
emit("disconnected", { willRetry });
|
|
1467
|
+
if (willRetry) {
|
|
1468
|
+
const delay = Math.min(backoff.maxMs, backoff.initialMs * Math.pow(2, attempts));
|
|
1469
|
+
attempts += 1;
|
|
1470
|
+
reconnectTimer = timers.set(() => {
|
|
1471
|
+
reconnectTimer = null;
|
|
1472
|
+
if (wantOpen) void open();
|
|
1473
|
+
}, delay);
|
|
1474
|
+
}
|
|
1475
|
+
};
|
|
1476
|
+
}
|
|
1477
|
+
return {
|
|
1478
|
+
get status() {
|
|
1479
|
+
return status;
|
|
1480
|
+
},
|
|
1481
|
+
async connect() {
|
|
1482
|
+
wantOpen = true;
|
|
1483
|
+
if (ws) return;
|
|
1484
|
+
await open();
|
|
1485
|
+
},
|
|
1486
|
+
disconnect() {
|
|
1487
|
+
wantOpen = false;
|
|
1488
|
+
if (reconnectTimer) {
|
|
1489
|
+
timers.clear(reconnectTimer);
|
|
1490
|
+
reconnectTimer = null;
|
|
1491
|
+
}
|
|
1492
|
+
stopPing();
|
|
1493
|
+
const s = ws;
|
|
1494
|
+
ws = null;
|
|
1495
|
+
try {
|
|
1496
|
+
s?.close(1e3, "client disconnect");
|
|
1497
|
+
} catch {
|
|
1498
|
+
}
|
|
1499
|
+
setStatus("closed");
|
|
1500
|
+
if (s) emit("disconnected", { willRetry: false });
|
|
1501
|
+
},
|
|
1502
|
+
on(event, handler) {
|
|
1503
|
+
if (!handlers.has(event)) handlers.set(event, /* @__PURE__ */ new Set());
|
|
1504
|
+
const set = handlers.get(event);
|
|
1505
|
+
set.add(handler);
|
|
1506
|
+
return () => {
|
|
1507
|
+
set.delete(handler);
|
|
1508
|
+
};
|
|
1509
|
+
},
|
|
1510
|
+
ingest
|
|
1511
|
+
};
|
|
1512
|
+
}
|
|
1513
|
+
|
|
1514
|
+
// src/client.ts
|
|
1515
|
+
var DEFAULT_API_BASE = "https://api.openmarkets.ai";
|
|
1516
|
+
var DEFAULT_LABELS = {
|
|
1517
|
+
placePractice: "Place practice order",
|
|
1518
|
+
placeReal: "Place real order",
|
|
1519
|
+
confirmReal: "Confirm real order",
|
|
1520
|
+
placing: "Placing\u2026",
|
|
1521
|
+
practice: "Practice",
|
|
1522
|
+
real: "Real money"
|
|
1523
|
+
};
|
|
1524
|
+
function assertLegModes(legs) {
|
|
1525
|
+
if (!Array.isArray(legs) || legs.length === 0) throw new OrderSheetError({ code: "bad_request", message: "At least one leg is required." });
|
|
1526
|
+
legs.forEach((l, i) => {
|
|
1527
|
+
const mode = l?.mode;
|
|
1528
|
+
if (mode !== "paper" && mode !== "real") {
|
|
1529
|
+
throw new OrderSheetError({ code: "bad_request", message: `Leg ${i}: mode must be "paper" or "real" (got ${JSON.stringify(mode)}). The API would treat a missing mode as real money.`, details: { index: i, field: "mode" } });
|
|
1530
|
+
}
|
|
1531
|
+
});
|
|
1532
|
+
}
|
|
1533
|
+
function resolveOptions(input) {
|
|
1534
|
+
const auth = input.apiKey ? { apiKey: input.apiKey } : input.auth ? { auth: input.auth } : input.getToken ? { getToken: input.getToken } : {};
|
|
1535
|
+
const baseUrl = (input.baseUrl ?? DEFAULT_API_BASE).replace(/\/+$/, "");
|
|
1536
|
+
return {
|
|
1537
|
+
auth,
|
|
1538
|
+
baseUrl,
|
|
1539
|
+
streamUrl: input.streamUrl ?? `${baseUrl.replace(/^http/, "ws")}/flow/v1/stream`,
|
|
1540
|
+
actAs: input.actAs ?? null,
|
|
1541
|
+
defaults: { mode: "paper", orderType: "market", stakeUnit: "amount", slippage: 0.02, oddsFormat: "cents", staleAfterMs: 6e4, ...input.defaults ?? {} },
|
|
1542
|
+
realMoney: { enabled: false, confirm: true, ...input.realMoney ?? {} },
|
|
1543
|
+
preflight: { auto: true, debounceMs: 300, ...input.preflight ?? {} },
|
|
1544
|
+
constraints: { ttlMs: 6e4, ...input.constraints ?? {} },
|
|
1545
|
+
stream: { enabled: true, pollIntervalMs: 5e3, ...input.stream ?? {} },
|
|
1546
|
+
labels: { ...DEFAULT_LABELS, ...input.labels ?? {} },
|
|
1547
|
+
now: input.now ?? (() => Date.now()),
|
|
1548
|
+
timers: input.timers ?? { set: setTimeout, clear: clearTimeout }
|
|
1549
|
+
};
|
|
1550
|
+
}
|
|
1551
|
+
function createOrderSheetClient(input) {
|
|
1552
|
+
const options = resolveOptions(input);
|
|
1553
|
+
const http = createHttp({ baseUrl: options.baseUrl, auth: options.auth, fetch: input.fetch, actAs: options.actAs });
|
|
1554
|
+
const handlers = /* @__PURE__ */ new Map();
|
|
1555
|
+
const events = {
|
|
1556
|
+
on(event, handler) {
|
|
1557
|
+
if (!handlers.has(event)) handlers.set(event, /* @__PURE__ */ new Set());
|
|
1558
|
+
handlers.get(event).add(handler);
|
|
1559
|
+
return () => {
|
|
1560
|
+
handlers.get(event)?.delete(handler);
|
|
1561
|
+
};
|
|
1562
|
+
},
|
|
1563
|
+
emit(event, payload) {
|
|
1564
|
+
for (const h of handlers.get(event) ?? []) {
|
|
1565
|
+
try {
|
|
1566
|
+
h(payload);
|
|
1567
|
+
} catch (e) {
|
|
1568
|
+
console.error("[order-sheet] handler threw", e);
|
|
1569
|
+
}
|
|
1570
|
+
}
|
|
1571
|
+
}
|
|
1572
|
+
};
|
|
1573
|
+
let cached = null;
|
|
1574
|
+
let inflight = null;
|
|
1575
|
+
let stale = false;
|
|
1576
|
+
let stream = null;
|
|
1577
|
+
const sheets = /* @__PURE__ */ new Set();
|
|
1578
|
+
async function constraints(opts = {}) {
|
|
1579
|
+
const fresh = cached && !stale && options.now() - cached.at < options.constraints.ttlMs;
|
|
1580
|
+
if (fresh && !opts.force) return cached.value;
|
|
1581
|
+
if (inflight) return inflight;
|
|
1582
|
+
inflight = http.request("GET", "/flow/v1/auth/orders/constraints").then((value) => {
|
|
1583
|
+
cached = { value, at: options.now() };
|
|
1584
|
+
stale = false;
|
|
1585
|
+
events.emit("constraints", value);
|
|
1586
|
+
return value;
|
|
1587
|
+
}).finally(() => {
|
|
1588
|
+
inflight = null;
|
|
1589
|
+
});
|
|
1590
|
+
return inflight;
|
|
1591
|
+
}
|
|
1592
|
+
function accountFromConstraints(c) {
|
|
1593
|
+
return {
|
|
1594
|
+
routerAccountId: c.account.router_account_id,
|
|
1595
|
+
environment: c.account.environment,
|
|
1596
|
+
canExecute: c.account.can_execute,
|
|
1597
|
+
canPractice: c.account.can_practice,
|
|
1598
|
+
moneyModes: [...c.account.money_modes],
|
|
1599
|
+
practiceOnly: c.account.policy.practice_only,
|
|
1600
|
+
isFiduciary: c.account.is_fiduciary,
|
|
1601
|
+
issues: c.account.issues.map(fromServerIssue),
|
|
1602
|
+
practiceWallet: { partnerId: c.account.practice_wallet.partner_id, currency: c.account.practice_wallet.currency, balance: c.account.practice_wallet.balance },
|
|
1603
|
+
policy: { ...c.account.policy },
|
|
1604
|
+
constraintsVersion: c.constraints_version
|
|
1605
|
+
};
|
|
1606
|
+
}
|
|
1607
|
+
async function account(opts = {}) {
|
|
1608
|
+
try {
|
|
1609
|
+
return accountFromConstraints(await constraints(opts));
|
|
1610
|
+
} catch (e) {
|
|
1611
|
+
if (e instanceof OrderSheetError && (e.code === "account_scope_required" || e.code === "account_not_found")) {
|
|
1612
|
+
const me = await http.request("GET", "/flow/v1/auth/me");
|
|
1613
|
+
return {
|
|
1614
|
+
routerAccountId: me.router_account_id,
|
|
1615
|
+
environment: me.environment,
|
|
1616
|
+
canExecute: me.can_execute,
|
|
1617
|
+
canPractice: me.can_practice,
|
|
1618
|
+
moneyModes: [...me.money_modes],
|
|
1619
|
+
practiceOnly: false,
|
|
1620
|
+
isFiduciary: false,
|
|
1621
|
+
issues: [],
|
|
1622
|
+
practiceWallet: { partnerId: null, currency: "ATLAS", balance: null },
|
|
1623
|
+
policy: null,
|
|
1624
|
+
constraintsVersion: null
|
|
1625
|
+
};
|
|
1626
|
+
}
|
|
1627
|
+
throw e;
|
|
1628
|
+
}
|
|
1629
|
+
}
|
|
1630
|
+
async function loadPosition(ref) {
|
|
1631
|
+
if ("position" in ref) return { position: ref.position, partnerId: ref.partnerId ?? null };
|
|
1632
|
+
let contestId;
|
|
1633
|
+
let positionHash;
|
|
1634
|
+
let partnerId;
|
|
1635
|
+
if ("liquidityHash" in ref) {
|
|
1636
|
+
const parts = ref.liquidityHash.split(":");
|
|
1637
|
+
if (parts.length < 7) throw new OrderSheetError({ code: "bad_request", message: `Not a liquidity hash: ${ref.liquidityHash}` });
|
|
1638
|
+
partnerId = parts[0];
|
|
1639
|
+
positionHash = parts.slice(1).join(":");
|
|
1640
|
+
contestId = parts[1];
|
|
1641
|
+
} else {
|
|
1642
|
+
const parts = ref.positionHash.split(":");
|
|
1643
|
+
if (parts.length < 6) throw new OrderSheetError({ code: "bad_request", message: `Not a position hash: ${ref.positionHash}` });
|
|
1644
|
+
positionHash = ref.positionHash;
|
|
1645
|
+
contestId = parts[0];
|
|
1646
|
+
partnerId = ref.partnerId ?? null;
|
|
1647
|
+
}
|
|
1648
|
+
const positions = await http.request("GET", `/flow/v1/contests/${encodeURIComponent(contestId)}/liquidity`);
|
|
1649
|
+
const position = positions.find((p) => p.position_hash === positionHash);
|
|
1650
|
+
if (!position) throw new OrderSheetError({ code: "position_not_found", message: `Position ${positionHash} is not quoted on contest ${contestId}.`, status: 404 });
|
|
1651
|
+
return { position, partnerId };
|
|
1652
|
+
}
|
|
1653
|
+
function withAtomic(body, atomic, verifyLiquidity) {
|
|
1654
|
+
if (atomic) return { ...body, check_all_first: true, on_leg_failure: "halt", placement: "parallel", verify_liquidity: true };
|
|
1655
|
+
return verifyLiquidity ? { ...body, verify_liquidity: true } : body;
|
|
1656
|
+
}
|
|
1657
|
+
async function ensureStream() {
|
|
1658
|
+
if (!options.stream.enabled || !credentialSupportsStream(options.auth)) return null;
|
|
1659
|
+
if (!stream) {
|
|
1660
|
+
stream = createOrderStream({
|
|
1661
|
+
url: options.streamUrl,
|
|
1662
|
+
getApiKey: async () => (await resolveCredential(options.auth)).value,
|
|
1663
|
+
WebSocket: input.WebSocket,
|
|
1664
|
+
timers: options.timers
|
|
1665
|
+
});
|
|
1666
|
+
stream.on("order.update", (f) => events.emit("order.update", f));
|
|
1667
|
+
stream.on("order.settled", (f) => events.emit("order.settled", f));
|
|
1668
|
+
stream.on("balance.update", (f) => {
|
|
1669
|
+
stale = true;
|
|
1670
|
+
events.emit("balance.update", f);
|
|
1671
|
+
events.emit("constraints", cached?.value ?? null);
|
|
1672
|
+
if (sheets.size > 0) constraints().catch(() => {
|
|
1673
|
+
});
|
|
1674
|
+
});
|
|
1675
|
+
stream.on("connected", (f) => events.emit("stream.connected", f));
|
|
1676
|
+
}
|
|
1677
|
+
await stream.connect();
|
|
1678
|
+
return stream;
|
|
1679
|
+
}
|
|
1680
|
+
const client = {
|
|
1681
|
+
options,
|
|
1682
|
+
http,
|
|
1683
|
+
events,
|
|
1684
|
+
account,
|
|
1685
|
+
constraints,
|
|
1686
|
+
getCachedConstraints: () => cached?.value ?? null,
|
|
1687
|
+
constraintsStale: () => !!cached && cached.value.constraints_version !== CONSTRAINTS_VERSION,
|
|
1688
|
+
accountFromConstraints,
|
|
1689
|
+
invalidateConstraints() {
|
|
1690
|
+
stale = true;
|
|
1691
|
+
events.emit("constraints", cached?.value ?? null);
|
|
1692
|
+
},
|
|
1693
|
+
loadPosition,
|
|
1694
|
+
openSheet(ref, init) {
|
|
1695
|
+
const created = createSheet(client, ref, init);
|
|
1696
|
+
const sheet = { ...created, destroy() {
|
|
1697
|
+
sheets.delete(sheet);
|
|
1698
|
+
created.destroy();
|
|
1699
|
+
} };
|
|
1700
|
+
sheets.add(sheet);
|
|
1701
|
+
return sheet;
|
|
1702
|
+
},
|
|
1703
|
+
async preflight(legs, opts = {}) {
|
|
1704
|
+
assertLegModes(legs);
|
|
1705
|
+
return http.request("POST", "/flow/v1/auth/orders/preflight", { body: withAtomic({ orders: legs }, opts.atomic, opts.verifyLiquidity) });
|
|
1706
|
+
},
|
|
1707
|
+
async buy(legs, opts = {}) {
|
|
1708
|
+
assertLegModes(legs);
|
|
1709
|
+
if (!options.realMoney.enabled && legs.some((l) => l.mode === "real")) {
|
|
1710
|
+
throw new OrderSheetError({ code: "real_money_disabled", message: 'A leg has mode "real" but real-money orders are turned off in this app (realMoney.enabled is false).' });
|
|
1711
|
+
}
|
|
1712
|
+
const body = withAtomic({
|
|
1713
|
+
orders: legs,
|
|
1714
|
+
...opts.opportunityId ? { opportunity_id: opts.opportunityId } : {},
|
|
1715
|
+
...opts.modelId ? { model_id: opts.modelId } : {}
|
|
1716
|
+
}, opts.atomic, opts.verifyLiquidity);
|
|
1717
|
+
const key = opts.idempotencyKey ?? newIdempotencyKey();
|
|
1718
|
+
return http.request("POST", "/flow/v1/auth/orders/buy", { body, headers: { "Idempotency-Key": key } });
|
|
1719
|
+
},
|
|
1720
|
+
async restingOrders() {
|
|
1721
|
+
return (await http.request("GET", "/flow/v1/auth/orders/resting/me")).orders;
|
|
1722
|
+
},
|
|
1723
|
+
listOrders(opts = {}) {
|
|
1724
|
+
const q = new URLSearchParams();
|
|
1725
|
+
if (opts.contestId) q.set("contest_id", opts.contestId);
|
|
1726
|
+
if (opts.status) q.set("status", opts.status);
|
|
1727
|
+
q.set("limit", String(opts.limit ?? 50));
|
|
1728
|
+
return http.request("GET", `/flow/v1/auth/orders?${q.toString()}`);
|
|
1729
|
+
},
|
|
1730
|
+
cancelOrders(routerOrderIds) {
|
|
1731
|
+
return http.request("POST", "/flow/v1/auth/orders/cancel", { body: { router_order_ids: routerOrderIds } });
|
|
1732
|
+
},
|
|
1733
|
+
me: () => http.request("GET", "/flow/v1/auth/me"),
|
|
1734
|
+
get stream() {
|
|
1735
|
+
return stream;
|
|
1736
|
+
},
|
|
1737
|
+
ensureStream,
|
|
1738
|
+
reset() {
|
|
1739
|
+
cached = null;
|
|
1740
|
+
stale = false;
|
|
1741
|
+
stream?.disconnect();
|
|
1742
|
+
stream = null;
|
|
1743
|
+
for (const s of sheets) s.destroy();
|
|
1744
|
+
sheets.clear();
|
|
1745
|
+
events.emit("constraints", null);
|
|
1746
|
+
},
|
|
1747
|
+
destroy() {
|
|
1748
|
+
client.reset();
|
|
1749
|
+
handlers.clear();
|
|
1750
|
+
}
|
|
1751
|
+
};
|
|
1752
|
+
return client;
|
|
1753
|
+
}
|
|
1754
|
+
|
|
1755
|
+
// src/react.tsx
|
|
1756
|
+
var import_jsx_runtime = require("react/jsx-runtime");
|
|
1757
|
+
var ClientContext = (0, import_react.createContext)(null);
|
|
1758
|
+
function OrderSheetProvider({ client, children }) {
|
|
1759
|
+
return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(ClientContext.Provider, { value: client, children });
|
|
1760
|
+
}
|
|
1761
|
+
function useOrderSheetClient() {
|
|
1762
|
+
const c = (0, import_react.useContext)(ClientContext);
|
|
1763
|
+
if (!c) throw new Error("useOrderSheetClient: wrap the tree in <OrderSheetProvider client={\u2026}> or pass a client explicitly.");
|
|
1764
|
+
return c;
|
|
1765
|
+
}
|
|
1766
|
+
function refKey(ref) {
|
|
1767
|
+
if ("position" in ref) return `pos:${ref.position.position_hash}:${ref.partnerId ?? ""}`;
|
|
1768
|
+
if ("liquidityHash" in ref) return `lh:${ref.liquidityHash}`;
|
|
1769
|
+
return `ph:${ref.positionHash}:${ref.partnerId ?? ""}`;
|
|
1770
|
+
}
|
|
1771
|
+
function useSheetState(sheet) {
|
|
1772
|
+
return (0, import_react.useSyncExternalStore)(sheet.subscribe, sheet.getState, sheet.getState);
|
|
1773
|
+
}
|
|
1774
|
+
function isSheet(x) {
|
|
1775
|
+
return typeof x === "object" && x !== null && typeof x.getState === "function" && typeof x.subscribe === "function";
|
|
1776
|
+
}
|
|
1777
|
+
function useOrderSheet(client, ref, init) {
|
|
1778
|
+
const ctx = (0, import_react.useContext)(ClientContext);
|
|
1779
|
+
const om = client ?? ctx;
|
|
1780
|
+
const given = isSheet(ref) ? ref : null;
|
|
1781
|
+
if (!om && !given) throw new Error("useOrderSheet: pass a client or wrap the tree in <OrderSheetProvider>.");
|
|
1782
|
+
const key = given ? "given" : refKey(ref);
|
|
1783
|
+
const latest = (0, import_react.useRef)({ ref, init });
|
|
1784
|
+
latest.current = { ref, init };
|
|
1785
|
+
const [opened, setOpened] = (0, import_react.useState)(null);
|
|
1786
|
+
(0, import_react.useEffect)(() => {
|
|
1787
|
+
if (given || !om) return;
|
|
1788
|
+
const sheet2 = om.openSheet(latest.current.ref, latest.current.init);
|
|
1789
|
+
setOpened({ key, om, sheet: sheet2 });
|
|
1790
|
+
return () => {
|
|
1791
|
+
sheet2.destroy();
|
|
1792
|
+
setOpened((cur) => cur?.sheet === sheet2 ? null : cur);
|
|
1793
|
+
};
|
|
1794
|
+
}, [key, om, given]);
|
|
1795
|
+
const inert = (0, import_react.useMemo)(
|
|
1796
|
+
() => given || !om ? null : createInertSheet(om.options, latest.current.ref, latest.current.init),
|
|
1797
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
1798
|
+
[key, om, given]
|
|
1799
|
+
);
|
|
1800
|
+
const live = opened && opened.key === key && opened.om === om ? opened.sheet : null;
|
|
1801
|
+
const sheet = given ?? live ?? inert;
|
|
1802
|
+
const state = (0, import_react.useSyncExternalStore)(sheet.subscribe, sheet.getState, sheet.getState);
|
|
1803
|
+
return (0, import_react.useMemo)(() => ({
|
|
1804
|
+
...state,
|
|
1805
|
+
sheet,
|
|
1806
|
+
select: sheet.select,
|
|
1807
|
+
set: sheet.set,
|
|
1808
|
+
setStake: (value, unit) => sheet.set({ stake: value == null ? null : (unit ?? state.stake.unit) === "shares" ? { shares: value } : { amount: value } }),
|
|
1809
|
+
setMode: (mode) => sheet.set({ mode }),
|
|
1810
|
+
setOrderType: (orderType) => sheet.set({ orderType }),
|
|
1811
|
+
setMaxPrice: (maxPrice) => sheet.set({ maxPrice }),
|
|
1812
|
+
place: sheet.place,
|
|
1813
|
+
confirm: sheet.confirm,
|
|
1814
|
+
cancelConfirm: sheet.cancelConfirm,
|
|
1815
|
+
cancel: sheet.cancel,
|
|
1816
|
+
runPreflight: sheet.preflight,
|
|
1817
|
+
reset: sheet.reset,
|
|
1818
|
+
refresh: sheet.refresh
|
|
1819
|
+
}), [state, sheet]);
|
|
1820
|
+
}
|
|
1821
|
+
function useOrderSheetAccount(client) {
|
|
1822
|
+
const ctx = (0, import_react.useContext)(ClientContext);
|
|
1823
|
+
const om = client ?? ctx;
|
|
1824
|
+
const [account, setAccount] = (0, import_react.useState)(null);
|
|
1825
|
+
const [loading, setLoading] = (0, import_react.useState)(true);
|
|
1826
|
+
const [error, setError] = (0, import_react.useState)(null);
|
|
1827
|
+
const refresh = (0, import_react.useCallback)(async () => {
|
|
1828
|
+
if (!om) return;
|
|
1829
|
+
setLoading(true);
|
|
1830
|
+
try {
|
|
1831
|
+
setAccount(await om.account({ force: true }));
|
|
1832
|
+
setError(null);
|
|
1833
|
+
} catch (e) {
|
|
1834
|
+
setError(e);
|
|
1835
|
+
} finally {
|
|
1836
|
+
setLoading(false);
|
|
1837
|
+
}
|
|
1838
|
+
}, [om]);
|
|
1839
|
+
(0, import_react.useEffect)(() => {
|
|
1840
|
+
let alive = true;
|
|
1841
|
+
if (!om) return;
|
|
1842
|
+
om.account().then((a) => {
|
|
1843
|
+
if (alive) {
|
|
1844
|
+
setAccount(a);
|
|
1845
|
+
setError(null);
|
|
1846
|
+
}
|
|
1847
|
+
}).catch((e) => {
|
|
1848
|
+
if (alive) setError(e);
|
|
1849
|
+
}).finally(() => {
|
|
1850
|
+
if (alive) setLoading(false);
|
|
1851
|
+
});
|
|
1852
|
+
const off = om.events.on("constraints", (c) => {
|
|
1853
|
+
if (alive && c) setAccount(om.accountFromConstraints(c));
|
|
1854
|
+
});
|
|
1855
|
+
return () => {
|
|
1856
|
+
alive = false;
|
|
1857
|
+
off();
|
|
1858
|
+
};
|
|
1859
|
+
}, [om]);
|
|
1860
|
+
return { account, loading, error, refresh };
|
|
1861
|
+
}
|
|
1862
|
+
var HandleContext = (0, import_react.createContext)(null);
|
|
1863
|
+
function useOrderSheetHandle() {
|
|
1864
|
+
const h = (0, import_react.useContext)(HandleContext);
|
|
1865
|
+
if (!h) throw new Error("OrderSheet.* must be rendered inside <OrderSheet.Root handle={\u2026}>.");
|
|
1866
|
+
return h;
|
|
1867
|
+
}
|
|
1868
|
+
var Root = (0, import_react.forwardRef)(function Root2({ handle, onOutcome, onPlaceError, children, ...rest }, ref) {
|
|
1869
|
+
return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(HandleContext.Provider, { value: handle, children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
|
|
1870
|
+
"form",
|
|
1871
|
+
{
|
|
1872
|
+
ref,
|
|
1873
|
+
"data-oms-part": "root",
|
|
1874
|
+
"data-phase": handle.phase,
|
|
1875
|
+
"data-mode": handle.mode,
|
|
1876
|
+
"data-order-type": handle.orderType,
|
|
1877
|
+
"data-loading": handle.loading || void 0,
|
|
1878
|
+
noValidate: true,
|
|
1879
|
+
onSubmit: (e) => {
|
|
1880
|
+
e.preventDefault();
|
|
1881
|
+
handle.place().then((o) => onOutcome?.(o)).catch((err) => onPlaceError?.(err));
|
|
1882
|
+
},
|
|
1883
|
+
...rest,
|
|
1884
|
+
children
|
|
1885
|
+
}
|
|
1886
|
+
) });
|
|
1887
|
+
});
|
|
1888
|
+
var VENUE_KEYS = /* @__PURE__ */ new Set(["ArrowRight", "ArrowDown", "ArrowLeft", "ArrowUp", "Home", "End"]);
|
|
1889
|
+
var isEnabledVenue = (v) => v.eligibility.status !== "blocked";
|
|
1890
|
+
function venueKeyTarget(key, venues, fromPartnerId) {
|
|
1891
|
+
const enabled = venues.filter(isEnabledVenue);
|
|
1892
|
+
if (enabled.length === 0 || !VENUE_KEYS.has(key)) return null;
|
|
1893
|
+
if (key === "Home") return enabled[0];
|
|
1894
|
+
if (key === "End") return enabled[enabled.length - 1];
|
|
1895
|
+
const forward = key === "ArrowRight" || key === "ArrowDown";
|
|
1896
|
+
const at = fromPartnerId == null ? -1 : venues.findIndex((v) => v.partnerId === fromPartnerId);
|
|
1897
|
+
if (at < 0) return forward ? enabled[0] : enabled[enabled.length - 1];
|
|
1898
|
+
const n = venues.length;
|
|
1899
|
+
for (let step = 1; step <= n; step++) {
|
|
1900
|
+
const v = venues[(at + (forward ? step : -step) + n * step) % n];
|
|
1901
|
+
if (isEnabledVenue(v)) return v;
|
|
1902
|
+
}
|
|
1903
|
+
return null;
|
|
1904
|
+
}
|
|
1905
|
+
function tabbableVenueId(venues) {
|
|
1906
|
+
return (venues.find((v) => v.selected) ?? venues.find(isEnabledVenue) ?? venues[0])?.partnerId ?? null;
|
|
1907
|
+
}
|
|
1908
|
+
function Venues({ render, hideBlocked, ...rest }) {
|
|
1909
|
+
const h = useOrderSheetHandle();
|
|
1910
|
+
const venues = hideBlocked ? h.venues.filter(isEnabledVenue) : h.venues;
|
|
1911
|
+
const nodes = (0, import_react.useRef)(/* @__PURE__ */ new Map());
|
|
1912
|
+
const tabbable = tabbableVenueId(venues);
|
|
1913
|
+
const move = (key, from) => {
|
|
1914
|
+
const target = venueKeyTarget(key, venues, from);
|
|
1915
|
+
if (!target) return false;
|
|
1916
|
+
h.select(target.partnerId);
|
|
1917
|
+
nodes.current.get(target.partnerId)?.focus();
|
|
1918
|
+
return true;
|
|
1919
|
+
};
|
|
1920
|
+
return /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { "data-oms-part": "venues", role: "radiogroup", "aria-label": "Venue", ...rest, children: venues.map((v) => /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
|
|
1921
|
+
"div",
|
|
1922
|
+
{
|
|
1923
|
+
ref: (el) => {
|
|
1924
|
+
if (el) nodes.current.set(v.partnerId, el);
|
|
1925
|
+
else nodes.current.delete(v.partnerId);
|
|
1926
|
+
},
|
|
1927
|
+
"data-oms-part": "venue",
|
|
1928
|
+
"data-state": v.eligibility.status,
|
|
1929
|
+
"data-selected": v.selected || void 0,
|
|
1930
|
+
"data-best": v.best || void 0,
|
|
1931
|
+
"data-connected": v.account.connected || void 0,
|
|
1932
|
+
role: "radio",
|
|
1933
|
+
"aria-checked": v.selected,
|
|
1934
|
+
"aria-disabled": v.eligibility.status === "blocked" || void 0,
|
|
1935
|
+
tabIndex: v.partnerId === tabbable ? 0 : -1,
|
|
1936
|
+
onClick: () => h.select(v.partnerId),
|
|
1937
|
+
onKeyDown: (e) => {
|
|
1938
|
+
if (e.key === "Enter" || e.key === " ") {
|
|
1939
|
+
e.preventDefault();
|
|
1940
|
+
h.select(v.partnerId);
|
|
1941
|
+
return;
|
|
1942
|
+
}
|
|
1943
|
+
if (move(e.key, v.partnerId)) e.preventDefault();
|
|
1944
|
+
},
|
|
1945
|
+
children: render ? render(v, h) : /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_jsx_runtime.Fragment, { children: [
|
|
1946
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { "data-oms-part": "venue-name", children: v.name }),
|
|
1947
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { "data-oms-part": "venue-price", children: v.display.price }),
|
|
1948
|
+
v.best && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { "data-oms-part": "venue-best", children: "best" }),
|
|
1949
|
+
v.eligibility.issues[0] && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { "data-oms-part": "venue-reason", children: v.eligibility.issues[0].message })
|
|
1950
|
+
] })
|
|
1951
|
+
},
|
|
1952
|
+
v.partnerId
|
|
1953
|
+
)) });
|
|
1954
|
+
}
|
|
1955
|
+
function Mode({ labels, ...rest }) {
|
|
1956
|
+
const h = useOrderSheetHandle();
|
|
1957
|
+
const modes = ["paper", "real"];
|
|
1958
|
+
const available = h.account?.moneyModes ?? ["paper"];
|
|
1959
|
+
return /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { "data-oms-part": "mode", role: "group", "aria-label": "Money mode", ...rest, children: modes.map((m) => /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
|
|
1960
|
+
"button",
|
|
1961
|
+
{
|
|
1962
|
+
type: "button",
|
|
1963
|
+
"data-oms-part": "mode-option",
|
|
1964
|
+
"data-mode": m,
|
|
1965
|
+
"data-available": available.includes(m) || void 0,
|
|
1966
|
+
"aria-pressed": h.mode === m,
|
|
1967
|
+
disabled: h.phase === "placing",
|
|
1968
|
+
onClick: () => h.setMode(m),
|
|
1969
|
+
children: m === "paper" ? labels?.paper ?? "Practice" : labels?.real ?? "Real money"
|
|
1970
|
+
},
|
|
1971
|
+
m
|
|
1972
|
+
)) });
|
|
1973
|
+
}
|
|
1974
|
+
function OrderTypeToggle(props) {
|
|
1975
|
+
const h = useOrderSheetHandle();
|
|
1976
|
+
const types = ["market", "limit"];
|
|
1977
|
+
const supported = h.venue?.supports.orderTypes ?? ["market"];
|
|
1978
|
+
return /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { "data-oms-part": "order-type", role: "group", "aria-label": "Order type", ...props, children: types.map((t) => /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
|
|
1979
|
+
"button",
|
|
1980
|
+
{
|
|
1981
|
+
type: "button",
|
|
1982
|
+
"data-oms-part": "order-type-option",
|
|
1983
|
+
"data-order-type": t,
|
|
1984
|
+
"data-available": supported.includes(t) && !(h.mode === "paper" && t === "limit") || void 0,
|
|
1985
|
+
"aria-pressed": h.orderType === t,
|
|
1986
|
+
disabled: h.phase === "placing",
|
|
1987
|
+
onClick: () => h.setOrderType(t),
|
|
1988
|
+
children: t === "market" ? "Market" : "Limit"
|
|
1989
|
+
},
|
|
1990
|
+
t
|
|
1991
|
+
)) });
|
|
1992
|
+
}
|
|
1993
|
+
function Stake({ label, unitToggle = true, bare, id, ...rest }) {
|
|
1994
|
+
const h = useOrderSheetHandle();
|
|
1995
|
+
const auto = (0, import_react.useId)();
|
|
1996
|
+
const inputId = id ?? auto;
|
|
1997
|
+
const unit = h.stake.unit;
|
|
1998
|
+
const input = /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
|
|
1999
|
+
"input",
|
|
2000
|
+
{
|
|
2001
|
+
id: inputId,
|
|
2002
|
+
"data-oms-part": "stake-input",
|
|
2003
|
+
"data-unit": unit,
|
|
2004
|
+
type: "number",
|
|
2005
|
+
inputMode: "decimal",
|
|
2006
|
+
min: 0,
|
|
2007
|
+
step: unit === "amount" ? 0.01 : h.venue?.constraints?.size.whole_contracts ? 1 : 0.01,
|
|
2008
|
+
value: h.stake.value ?? "",
|
|
2009
|
+
disabled: h.phase === "placing",
|
|
2010
|
+
onChange: (e) => {
|
|
2011
|
+
const n = e.target.valueAsNumber;
|
|
2012
|
+
h.setStake(Number.isFinite(n) ? n : null);
|
|
2013
|
+
},
|
|
2014
|
+
"aria-invalid": h.validation.blocking.some((i) => i.fix === "raise_stake" || i.fix === "lower_stake" || i.fix === "round_stake") || void 0,
|
|
2015
|
+
...rest
|
|
2016
|
+
}
|
|
2017
|
+
);
|
|
2018
|
+
if (bare) return input;
|
|
2019
|
+
return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { "data-oms-part": "stake", "data-unit": unit, children: [
|
|
2020
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("label", { "data-oms-part": "stake-label", htmlFor: inputId, children: label ?? (unit === "amount" ? "Stake" : "Contracts") }),
|
|
2021
|
+
input,
|
|
2022
|
+
unitToggle && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { "data-oms-part": "stake-unit", role: "group", "aria-label": "Stake unit", children: ["amount", "shares"].map((u) => /* @__PURE__ */ (0, import_jsx_runtime.jsx)("button", { type: "button", "data-oms-part": "stake-unit-option", "data-unit": u, "aria-pressed": unit === u, onClick: () => h.setStake(h.stake.value, u), children: u === "amount" ? h.quote?.currency === "USD" || !h.quote ? "$" : h.quote.currency : "contracts" }, u)) })
|
|
2023
|
+
] });
|
|
2024
|
+
}
|
|
2025
|
+
function Price({ label, bare, id, ...rest }) {
|
|
2026
|
+
const h = useOrderSheetHandle();
|
|
2027
|
+
const auto = (0, import_react.useId)();
|
|
2028
|
+
const inputId = id ?? auto;
|
|
2029
|
+
const tick = h.venue?.constraints?.price.tick ?? 0.01;
|
|
2030
|
+
const input = /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
|
|
2031
|
+
"input",
|
|
2032
|
+
{
|
|
2033
|
+
id: inputId,
|
|
2034
|
+
"data-oms-part": "price-input",
|
|
2035
|
+
"data-auto": h.maxPriceIsAuto || void 0,
|
|
2036
|
+
type: "number",
|
|
2037
|
+
inputMode: "decimal",
|
|
2038
|
+
min: h.venue?.constraints?.price.min ?? 0.01,
|
|
2039
|
+
max: h.venue?.constraints?.price.max ?? 0.99,
|
|
2040
|
+
step: tick,
|
|
2041
|
+
value: h.maxPrice ?? "",
|
|
2042
|
+
disabled: h.phase === "placing",
|
|
2043
|
+
onChange: (e) => {
|
|
2044
|
+
const n = e.target.valueAsNumber;
|
|
2045
|
+
h.setMaxPrice(Number.isFinite(n) ? n : null);
|
|
2046
|
+
},
|
|
2047
|
+
...rest
|
|
2048
|
+
}
|
|
2049
|
+
);
|
|
2050
|
+
if (bare) return input;
|
|
2051
|
+
return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { "data-oms-part": "price", "data-auto": h.maxPriceIsAuto || void 0, "data-order-type": h.orderType, children: [
|
|
2052
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("label", { "data-oms-part": "price-label", htmlFor: inputId, children: label ?? (h.orderType === "limit" ? "Limit price" : "Max price") }),
|
|
2053
|
+
input,
|
|
2054
|
+
!h.maxPriceIsAuto && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("button", { type: "button", "data-oms-part": "price-reset", onClick: () => h.setMaxPrice(null), children: "auto" })
|
|
2055
|
+
] });
|
|
2056
|
+
}
|
|
2057
|
+
function Quote({ render, ...rest }) {
|
|
2058
|
+
const h = useOrderSheetHandle();
|
|
2059
|
+
const q = h.quote;
|
|
2060
|
+
return /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { "data-oms-part": "quote", "data-freshness": q?.freshness, "aria-live": "polite", ...rest, children: render ? render(h) : q ? /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("dl", { children: [
|
|
2061
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("dt", { children: "Price" }),
|
|
2062
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("dd", { "data-oms-part": "quote-price", children: q.display.price }),
|
|
2063
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("dt", { children: "Cost" }),
|
|
2064
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("dd", { "data-oms-part": "quote-cost", children: q.display.cost }),
|
|
2065
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("dt", { children: "Contracts" }),
|
|
2066
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("dd", { "data-oms-part": "quote-contracts", children: q.contracts }),
|
|
2067
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("dt", { children: "To win" }),
|
|
2068
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("dd", { "data-oms-part": "quote-payout", children: q.display.payout }),
|
|
2069
|
+
q.fee && /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_jsx_runtime.Fragment, { children: [
|
|
2070
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("dt", { children: "Fee" }),
|
|
2071
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("dd", { "data-oms-part": "quote-fee", children: q.display.fee })
|
|
2072
|
+
] }),
|
|
2073
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("dt", { children: "Profit" }),
|
|
2074
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("dd", { "data-oms-part": "quote-profit", children: q.display.profit })
|
|
2075
|
+
] }) : null });
|
|
2076
|
+
}
|
|
2077
|
+
function Issues({ severities, render, ...rest }) {
|
|
2078
|
+
const h = useOrderSheetHandle();
|
|
2079
|
+
const list = h.validation.all.filter((i) => !severities || severities.includes(i.severity));
|
|
2080
|
+
const hasBlock = list.some((i) => i.severity === "block");
|
|
2081
|
+
return (
|
|
2082
|
+
// polite, not assertive: a blocking issue can change on every keystroke of the
|
|
2083
|
+
// stake, and an assertive region would interrupt the user each time.
|
|
2084
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { "data-oms-part": "issues", role: "status", "aria-live": "polite", "data-blocking": hasBlock || void 0, ...rest, children: list.map((i) => /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { "data-oms-part": "issue", "data-severity": i.severity, "data-code": i.code, "data-fix": i.fix, "data-source": i.source, children: render ? render(i, h) : i.message }, `${i.scope}:${i.code}`)) })
|
|
2085
|
+
);
|
|
2086
|
+
}
|
|
2087
|
+
function Submit({ children, ...rest }) {
|
|
2088
|
+
const h = useOrderSheetHandle();
|
|
2089
|
+
return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
|
|
2090
|
+
"button",
|
|
2091
|
+
{
|
|
2092
|
+
type: "submit",
|
|
2093
|
+
"data-oms-part": "submit",
|
|
2094
|
+
"data-mode": h.mode,
|
|
2095
|
+
"data-phase": h.phase,
|
|
2096
|
+
disabled: !h.canPlace,
|
|
2097
|
+
"aria-busy": h.phase === "placing" || void 0,
|
|
2098
|
+
...rest,
|
|
2099
|
+
children: typeof children === "function" ? children(h) : children ?? h.confirmLabel
|
|
2100
|
+
}
|
|
2101
|
+
);
|
|
2102
|
+
}
|
|
2103
|
+
function Hint({ render, ...rest }) {
|
|
2104
|
+
const h = useOrderSheetHandle();
|
|
2105
|
+
if (!h.hint) return null;
|
|
2106
|
+
const v = h.hint.venue;
|
|
2107
|
+
return /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { "data-oms-part": "hint", ...rest, children: render ? render(v, h) : /* @__PURE__ */ (0, import_jsx_runtime.jsx)("button", { type: "button", "data-oms-part": "hint-switch", onClick: () => h.select(v.partnerId), children: h.hint.message }) });
|
|
2108
|
+
}
|
|
2109
|
+
function Outcome({ render, ...rest }) {
|
|
2110
|
+
const h = useOrderSheetHandle();
|
|
2111
|
+
const r = h.result;
|
|
2112
|
+
if (!r) return null;
|
|
2113
|
+
const leg = r.legs[0];
|
|
2114
|
+
return /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { "data-oms-part": "outcome", "data-summary": r.summary, "data-phase": h.phase, "aria-live": "polite", ...rest, children: render ? render(h) : /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_jsx_runtime.Fragment, { children: [
|
|
2115
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { "data-oms-part": "outcome-summary", children: h.phase }),
|
|
2116
|
+
leg?.issue && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { "data-oms-part": "outcome-issue", children: leg.issue.message }),
|
|
2117
|
+
leg && leg.fill.shares > 0 && /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("span", { "data-oms-part": "outcome-fill", children: [
|
|
2118
|
+
leg.fill.shares,
|
|
2119
|
+
" @ ",
|
|
2120
|
+
leg.fill.avgPrice ?? "\u2014"
|
|
2121
|
+
] }),
|
|
2122
|
+
leg?.substituted && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { "data-oms-part": "outcome-substituted", children: "Filled on a different venue than requested \u2014 report this." })
|
|
2123
|
+
] }) });
|
|
2124
|
+
}
|
|
2125
|
+
var OrderSheet = {
|
|
2126
|
+
Root,
|
|
2127
|
+
Venues,
|
|
2128
|
+
Mode,
|
|
2129
|
+
OrderType: OrderTypeToggle,
|
|
2130
|
+
Stake,
|
|
2131
|
+
Price,
|
|
2132
|
+
Quote,
|
|
2133
|
+
Issues,
|
|
2134
|
+
Submit,
|
|
2135
|
+
Hint,
|
|
2136
|
+
Outcome
|
|
2137
|
+
};
|