@taphubhq/sdk-core 0.25.4 → 0.25.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,6 +1,10 @@
1
1
  # @taphubhq/sdk-core
2
2
 
3
- Framework-agnostic TypeScript SDK for the TabHub platform.
3
+ Framework-agnostic TypeScript SDK for the TapHub platform. Wraps the grid-api and
4
+ user-service GraphQL endpoints plus the MQTT realtime streams behind one client.
5
+
6
+ For React bindings (hooks, providers, i18n), use
7
+ [`@taphubhq/sdk-react`](https://github.com/TaphubHQ/taphub-sdk/blob/main/packages/sdk-react/README.md), which builds on this package.
4
8
 
5
9
  ## Install
6
10
 
@@ -16,9 +20,11 @@ import { TaphubClient } from '@taphubhq/sdk-core';
16
20
  const client = new TaphubClient({
17
21
  agencyId: 'acme-corp',
18
22
  // Shared API base. The SDK derives:
19
- // - grid-api → ${endpoint}/grid-api/grid-gql
23
+ // - grid-api → ${endpoint}/grid-api/grid-gql
20
24
  // - user-service → ${endpoint}/taphub-user-service/th-user-gql
21
25
  endpoint: 'https://builder.taptrading.net/api',
26
+ // Optional — omit to run without realtime (client.realtime stays undefined).
27
+ mqttEndpoint: 'wss://mqtt.taptrading.net/mqtt',
22
28
  });
23
29
 
24
30
  // Session-token login via user-service GraphQL (preferred).
@@ -27,16 +33,276 @@ await client.auth.loginWithSession(upstreamSessionToken);
27
33
  const me = await client.user.me();
28
34
  ```
29
35
 
30
- ### Login methods
36
+ ### Config
37
+
38
+ | Field | Required | Notes |
39
+ |----------------|----------|-------|
40
+ | `agencyId` | yes | Sent as `x-builder-code` on every request. Also namespaces token storage and scopes agency MQTT topics. |
41
+ | `endpoint` | yes | Absolute URL of the shared API base. Must parse as a URL — the constructor throws `TaphubValidationError` (`code: 'InvalidConfig'`) otherwise. |
42
+ | `mqttEndpoint` | no | When absent, `client.realtime` is `undefined`; every other module still works. |
43
+ | `mqttAuth` | no | `{ username, password }`, forwarded verbatim to the broker. For the TapHub/EMQX JWT-in-username setup, pass the session token as `username` and `''` as `password`. Read once when the connection opens — no refresh on reconnect. Ignored without `mqttEndpoint`. |
44
+ | `storage` | no | `TaphubStorageAdapter` for token persistence. Defaults to `localStorage` in the browser, in-memory elsewhere. |
45
+ | `fetch` | no | Custom `fetch` implementation (SSR, instrumentation, test doubles). |
46
+
47
+ ### Naming: `pairId` vs `agencyPairId`
48
+
49
+ - **`pairId`** — the system-wide pair id, e.g. `grid-ETH-USD`. **Slash-free** by
50
+ design so it is safe inside MQTT topics, URLs, and Redis keys. This is what
51
+ every SDK method takes.
52
+ - **`agencyPairId`** — the agency-scoped composite `{agencyId}:{pairId}`, e.g.
53
+ `acme-corp:grid-ETH-USD`. The server composes it from your `x-builder-code` /
54
+ JWT; you only meet it as an MQTT `gameId` and on `PairInfo.agencyPairId`.
55
+ - **`ETH/USD`** is a *display* name (`Pair.pair`, `AgencyPairStats.pair`), never
56
+ an id.
57
+
58
+ The pre-`bid-260602-v2` names `game` / `gameId` / `gamePairId` are retired from
59
+ the public surface.
60
+
61
+ ## Client surface
62
+
63
+ | Accessor | Module | Auth |
64
+ |---------------------|-------------------------|------|
65
+ | `client.auth` | `AuthModule` | — |
66
+ | `client.user` | `UserModule` | JWT |
67
+ | `client.pair` | `PairModule` | JWT (anon-tolerant) |
68
+ | `client.bid` | `BidModule` | JWT |
69
+ | `client.leaderboard`| `LeaderboardModule` | mixed (see below) |
70
+ | `client.agencyPairs`| `AgencyPairModule` | anon (`x-builder-code`) |
71
+ | `client.locale` | `LocaleModule` | anon |
72
+ | `client.realtime` | `RealtimeModule \| undefined` | broker creds |
73
+ | `client.network` | `NetworkQualityMonitor` | — |
74
+
75
+ Token helpers live on the client itself: `getToken()`, `setToken(token, { isDemo })`,
76
+ `isDemo()`.
77
+
78
+ > **Anon vs JWT reads.** `agencyPairs.list()`, `leaderboard.list()` and
79
+ > `leaderboard.rankBoard()` deliberately send **`x-builder-code` only, never the
80
+ > JWT**. A signed-in user can hold a JWT for a different agency than the one in
81
+ > context (a builder's demo user is always issued a `taptrading` JWT); attaching
82
+ > it would make the backend take its "JWT wins" branch and return the wrong
83
+ > agency's data.
84
+
85
+ ## Auth
86
+
87
+ ```ts
88
+ await client.auth.loginWithSession(sessionToken); // → LoginResult
89
+ await client.auth.logout();
90
+ ```
31
91
 
32
- | Method | Transport | Status |
33
- |---------------------------------|--------------------|-------------|
92
+ | Method | Transport | Status |
93
+ |----------------------------------|----------------------|---------------|
34
94
  | `loginWithSession(sessionToken)` | user-service GraphQL | **Preferred** |
35
- | `createDemoUser(username?)` | grid-api GraphQL | Active |
36
- | `loginWithGoogle(idToken)` | grid-api REST | `@deprecated` |
37
- | `demoLogin(username?)` | grid-api REST | `@deprecated` |
95
+ | `createDemoUser(username?, opts?)` | grid-api GraphQL | Active |
96
+ | `loginWithGoogle(idToken)` | grid-api REST | `@deprecated` |
97
+ | `demoLogin(username?)` | grid-api REST | `@deprecated` |
98
+
99
+ REST-backed methods continue to work until the gateway retires the routes in a
100
+ follow-up change.
101
+
102
+ ## User
103
+
104
+ ```ts
105
+ const me = await client.user.me(); // Me
106
+ const pnl = await client.user.pnl({ period: 'weekly' }); // UserPnL — 'weekly' | '30d'
107
+ const wallets = await client.user.wallets(); // UserWallet[]
108
+ const usdc = await client.user.walletByCurrency('USDC.e'); // UserWallet
109
+ const enabled = await client.user.refreshCurrencies(); // Currency[]
110
+ client.user.clearCurrencies(); // drop the cached list
111
+ ```
112
+
113
+ `UserPnL` carries `gain` (= `totalPayout − totalWagered`), the wagered/payout
114
+ totals, bid/win counts, and both competition ranks (`pnlRank`, `volRank`; `0`
115
+ means unranked).
116
+
117
+ > `client.user.wallets()` returns `UserWallet` (`{ id, amount, currency, isEnable }`),
118
+ > which is **not** the `Wallet` returned by the auth module (`{ balance, isEnabled }`).
119
+ > The distinct export alias is deliberate — see the note in `src/index.ts`.
120
+
121
+ ## Pairs
122
+
123
+ ```ts
124
+ const pairs = await client.pair.availableGamePairs({ gameplayId: 'taptrading' }); // PairInfo[]
125
+ const pair = await client.pair.get('grid-ETH-USD'); // Pair
126
+ const bars = await client.pair.chartHistory('grid-ETH-USD'); // Candle[]
127
+ ```
128
+
129
+ - `get(pairId)` throws `TaphubValidationError` with `code: 'GameNotFound'` when
130
+ the pair has no configured game for the agency in context. It also feeds the
131
+ clock-offset estimate (see [Clock sync](#clock-sync)).
132
+ - `chartHistory(pairId, limit?)` defaults to `DEFAULT_CHART_HISTORY_LIMIT` (600);
133
+ the server clamps any request to its cache size (1500).
134
+ - **`Candle.time` from GraphQL is Unix epoch SECONDS.** The MQTT
135
+ `MqttCandleEvent.time` is **milliseconds**. Do not multiply the MQTT value.
136
+ - `PairInfo.agencyPairId` is populated only for authenticated callers, and is the
137
+ value to feed into `realtime.subscribe()`.
138
+ - `Pair.config` carries `gridConfig` (`cellSizeTime`, `cellSizeValue`,
139
+ `candleSize`, `baseline`, `baselineTime`), `constraints`, bid amount bounds, and
140
+ the effective cancel policy (`bidCancelMinSeconds`, `bidCancelRefundRate`).
141
+ Treat `undefined` / `0` on the cancel fields as "use the platform default".
142
+
143
+ ## Bids
144
+
145
+ ```ts
146
+ const bid = await client.bid.placeBid({
147
+ pairId: 'grid-ETH-USD',
148
+ walletId, // required — grid-api's wallet_id is ID!
149
+ time1, time2, // window, unix seconds
150
+ price1, price2, // decimal strings
151
+ coefficient: '2.35',
152
+ amount: '10',
153
+ slippage: 5, // tolerance percent
154
+ });
155
+
156
+ const result = await client.bid.cancelBid(bid.id); // { bid, refundAmount, newBalance }
157
+ const mine = await client.bid.listBids({ statuses: ['pending'], limit: 20, pairId });
158
+ ```
159
+
160
+ `placeBid` auto-stamps `ct` (raw client send time) and `clockOffset` at dispatch
161
+ so the server can record skew-corrected latency into `bid.meta`. Both are
162
+ untrusted tracing data — do not supply them yourself.
163
+
164
+ `listBids` takes `statuses: BidStatus[]` (include-semantics). The singular
165
+ `status` is `@deprecated` and folded into a one-element list; `statuses` wins when
166
+ both are present.
167
+
168
+ Status classifiers avoid string-literal typos:
169
+
170
+ ```ts
171
+ import { isPending, isTerminal, isWin, isLoss, isCancelled } from '@taphubhq/sdk-core';
172
+ ```
173
+
174
+ `Bid.refundRate` / `Bid.refundAmount` are present only on cancelled bids. Prefer
175
+ `refundAmount` for display — it is the exact credited value, so it needs no
176
+ recompute from a config rate. `Bid.meta.pairName` holds the display pair frozen
177
+ at placement; read it instead of mapping `pairId` yourself.
178
+
179
+ ## Leaderboard & rank
180
+
181
+ ```ts
182
+ const board = await client.leaderboard.rankBoard({
183
+ period: 'last_7d', // 'today' | 'yesterday' | 'last_7d' | 'last_30d' | 'all_time' | 'custom'
184
+ sort: 'pnl', // 'pnl' | 'vol'
185
+ sortDir: 'desc',
186
+ limit: 50, offset: 0,
187
+ }); // RankBoard — public, identical for every caller
188
+
189
+ const me = await client.leaderboard.myRank({ period: 'last_7d' }); // MyRank | null
190
+ ```
191
+
192
+ - `period: 'custom'` requires an **hour-aligned** ISO-8601 UTC `range`.
193
+ - The board carries no per-user data by design (`bid-260606-leaderboard-split-my-rank`)
194
+ so it stays cacheable — `myRank()` is the per-user companion and does attach
195
+ the JWT. It returns `null` when nobody is signed in, and `MyRank.entry === null`
196
+ when the caller has no activity in the window.
197
+ - `leaderboard.list({ period, sortBy })` is the v1 query and is `@deprecated` on
198
+ both the SDK and the backend.
199
+
200
+ ## Agency pairs (market list)
201
+
202
+ ```ts
203
+ const rows = await client.agencyPairs.list({ filter: { status: 'active' } }); // AgencyPairStats[]
204
+ ```
205
+
206
+ Returns the agency-in-context's pairs with `currentPrice`, `currentVol24h`
207
+ (sliding 24h bid-amount sum), `maxCoef`, `thumb`, and `ordering`. There is no
208
+ argument to target another agency by design. Seed your UI from this call, then
209
+ keep it live with `realtime.subscribeAgencyPairStats()` — the backend suppresses
210
+ unchanged ticks, so a subscriber may receive nothing until price or volume moves.
211
+
212
+ ## Locale & error copy
213
+
214
+ ```ts
215
+ const res = await client.locale.get('vi', lastSeenVersion);
216
+ // → { lang, version, notModified, translations }
217
+ ```
218
+
219
+ Pass the last-seen `version` as `knownVersion` to get `{ notModified: true,
220
+ translations: null }` when the body is unchanged, and keep your prior state.
221
+
222
+ Error copy is a separate catalog keyed by the backend `extensions.code`:
38
223
 
39
- REST-backed methods continue to work until the gateway retires the routes in a follow-up change.
224
+ ```ts
225
+ await client.locale.loadErrorMessages(navigator.language); // never throws
226
+
227
+ // …later, inside a catch:
228
+ showToast(client.locale.errorMessage(e.code, 'Could not place your bid.'));
229
+
230
+ // with placeholders filled from the error's meta:
231
+ client.locale.errorMessage('Bid_CoefficientMismatch', 'Odds changed — try again.', {
232
+ serverCoef: e.serverCoef,
233
+ });
234
+ ```
235
+
236
+ - `loadErrorMessages(lang)` is background hydration: it **never throws and never
237
+ rejects**. Any failure leaves the last-known-good catalog in place — failing to
238
+ translate an error must not itself become an error. The tag is normalised
239
+ (region dropped, Chinese scripts kept apart), and concurrent calls for the same
240
+ language share one request.
241
+ - `errorMessage(code, fallback, meta?)` is **synchronous by design** — error copy
242
+ is read inside a `catch`, where React hooks are illegal. It returns `fallback`
243
+ whenever the catalog cannot produce a usable sentence, never an empty string and
244
+ never the raw code. `meta` fills `{name}` placeholders; unfilled placeholders are
245
+ stripped so raw braces never reach the user.
246
+ - `getErrorMessages(lang, knownVersion?)` is the throwing sibling, for callers who
247
+ want to see why a fetch failed.
248
+ - `refresh()` is an admin operation — the backend re-pulls the source sheet for
249
+ every supported language and requires an `X-API-Key` header.
250
+
251
+ ## Realtime (MQTT)
252
+
253
+ `client.realtime` is `undefined` unless `mqttEndpoint` was configured. Channels
254
+ are refcounted: N `subscribe()` calls for the same key return the same channel and
255
+ need N matching `unsubscribe()` calls.
256
+
257
+ ```ts
258
+ const channel = client.realtime?.subscribe(agencyPairId, userId); // GameChannel
259
+ channel?.on('candle', (e) => drawCandle(e)); // MqttCandleEvent
260
+ channel?.on('bidAccepted', (e) => addLiveTrade(e)); // MqttBidAcceptedEvent
261
+ channel?.on('bidWon', (e) => celebrate(e));
262
+ channel?.on('bidLost', (e) => dim(e));
263
+ channel?.on('bidCancelled', (e) => remove(e.bidId));
264
+ channel?.on('balanceUpdate', (e) => setBalance(e.balance));
265
+ channel?.on('configUpdate', (e) => setBidBounds(e));
266
+ channel?.on('idealConfigUpdate', (e) => suggestCellSize(e));
267
+ channel?.on('error', (err) => report(err));
268
+ client.realtime?.unsubscribe(agencyPairId, userId);
269
+ ```
270
+
271
+ | Subscription | Key | Emits |
272
+ |---|---|---|
273
+ | `subscribe(gameId, userId?)` | `agencyPairId` (+ user) | `GameChannel` — candle, bid lifecycle, balance, config |
274
+ | `subscribeWallet(userId)` | `userId` | `WalletChannel` — `walletBalanceUpdate` on **every** balance change, no game context |
275
+ | `subscribeUserBids(userId)` | `userId` | `UserBidsChannel` — `bidWon` / `bidLost` across **all** pairs, survives pair switches |
276
+ | `subscribeCandle(pairId, handler)` | `pairId` | raw `MqttCandleEvent`, market-wide public data |
277
+ | `subscribeAgencyPairStats(pairId, handler)` | `pairId` | `MqttAgencyPairStatsEvent` for the client's own agency |
278
+
279
+ Each has a matching `unsubscribe*`; `disconnect()` tears everything down.
280
+
281
+ Gotchas worth reading twice:
282
+
283
+ - **`MqttCandleEvent.time` is MILLISECONDS**, unlike the GraphQL `Candle.time`
284
+ (seconds). Compare against `CANDLE_EVENT.NEW` / `CANDLE_EVENT.UPDATE` rather
285
+ than bare string literals.
286
+ - `volatility` is the **fast** σ that prices coefficients; `slowVolatility` is the
287
+ slow σ used for grid geometry only.
288
+ - `MqttAcceptedBid` mirrors the wire shape, **not** the GraphQL `Bid`:
289
+ `coordinates` is nested, `coefficient` / `price1` / `price2` are numbers, and
290
+ `createdAt` is epoch milliseconds. `gameUuid` is `@deprecated` — read `pairId`.
291
+ - Won/lost payloads carry no pair, so the SDK recovers `pairId` from the result
292
+ topic. `pairIdFromBidResultTopic(topic)` is exported if you parse topics
293
+ yourself; it returns `undefined` on an unrecognised shape rather than throwing.
294
+ - `subscribeAgencyPairStats` throws `TaphubError` (`code: 'AgencyIdRequired'`) if
295
+ the client was built without an `agencyId`.
296
+ - `MqttWalletBalanceEvent.reason` is an open enum — unknown values are passed
297
+ through rather than dropped, so handle the default case.
298
+
299
+ ## Clock sync
300
+
301
+ `client.pair.get()` brackets its request to measure RTT and records a one-shot
302
+ server-clock offset. `placeBid` then stamps that offset alongside the raw client
303
+ send time. This is best-effort: the estimate is set-once and finite-guarded, so it
304
+ never throws and never blocks the pair load. Nothing to configure — just be aware
305
+ that a client which never calls `pair.get()` reports an offset of `0`.
40
306
 
41
307
  ## Error Handling
42
308
 
@@ -81,6 +347,9 @@ The GraphQL transport classifies errors by the first entry's `extensions.code` a
81
347
 
82
348
  `TaphubSlippageError extends TaphubValidationError`, so existing `instanceof TaphubValidationError` catch blocks keep working. Place the `TaphubSlippageError` check first when both branches exist.
83
349
 
350
+ To turn a thrown `code` into a user-facing sentence, use
351
+ [`client.locale.errorMessage()`](#locale--error-copy) rather than hardcoding copy.
352
+
84
353
  ### Slippage error fallback
85
354
 
86
355
  `TaphubSlippageError` is only thrown when `extensions.meta.clientCoef`, `extensions.meta.serverCoef`, and `extensions.meta.slippage` are all finite numbers. The `slippage` field is the client-submitted tolerance percent (e.g. `5` for 5%). If `meta` is missing or malformed, the SDK falls back to a generic `TaphubValidationError` with `code === 'Bid_CoefficientMismatch'`. Consumers SHOULD treat the generic fallback as the same user-facing rejection but without typed coefficient access.
@@ -114,6 +383,79 @@ The monitor is always present, even when `mqttEndpoint` is not configured. In SS
114
383
  - Connection samples from `navigator.connection` are used only as a cold-start fallback; once 3 real samples accumulate they no longer contribute to the EMA.
115
384
  - MQTT QoS1 `puback` timing is not yet plumbed; v1 derives MQTT signal from `connect` / `reconnect` / `disconnect` events only.
116
385
 
386
+ ## Events
387
+
388
+ A small client-wide bus for cross-cutting signals:
389
+
390
+ ```ts
391
+ import type { TaphubEventMap } from '@taphubhq/sdk-core';
392
+ // 'auth-expired' → void
393
+ // 'network:change' → { previous: NetworkQuality; current: NetworkQuality }
394
+ ```
395
+
396
+ ## Storage
397
+
398
+ Token persistence goes through a 3-method adapter, namespaced per agency
399
+ (`taphub:{agencyId}:token`):
400
+
401
+ ```ts
402
+ import { TaphubStorage, autoDetectStorage } from '@taphubhq/sdk-core';
403
+
404
+ new TaphubClient({ ..., storage: TaphubStorage.memory() }); // SSR / tests
405
+ ```
406
+
407
+ `autoDetectStorage()` is the default: `localStorage` in the browser, in-memory
408
+ elsewhere. Implement `TaphubStorageAdapter` (`get` / `set` / `remove`) for
409
+ anything else.
410
+
411
+ ## Coefficient math
412
+
413
+ `placeBid` takes a `coefficient`, so a headless client has to compute one before it
414
+ can bid. `calculateCoefficientWrapper` is the model the server validates against —
415
+ the same first-passage computation the grid chart renders with, and the same one
416
+ grid-api runs at placement:
417
+
418
+ ```ts
419
+ import {
420
+ calculateCoefficientWrapper,
421
+ type CoefficientInput,
422
+ } from '@taphubhq/sdk-core';
423
+
424
+ const coefficient = calculateCoefficientWrapper({
425
+ time1, time2, price1, price2, // the cell you are bidding on
426
+ candleTime, candleClose, volatility, // the live candle
427
+ coefMults, minCoef, // from pair.config
428
+ cellSizeTime, candleSize, // from pair.config
429
+ });
430
+ ```
431
+
432
+ A return of `Number.POSITIVE_INFINITY` means the cell cannot be priced right now —
433
+ do not pass it to `placeBid`.
434
+
435
+ The server stays authoritative: a value that disagrees beyond your `slippage`
436
+ tolerance is rejected with `Bid_CoefficientMismatch` (see
437
+ [Slippage error fallback](#slippage-error-fallback)).
438
+
439
+ **Candle-interval variants.** `candleSize` is part of `CoefficientInput` but does not
440
+ enter the coefficient: the cell width is measured in bars (`cellSizeTime`), and
441
+ `calculateProbHit` already prices the real seconds window, so dividing by
442
+ `candleSize` a second time would double-penalise the 1m/5m variants by roughly
443
+ 60×/300× (`bid-260703`). Pass the value from `pair.config` anyway —
444
+ `computeBaseline` uses it.
445
+
446
+ ### Deprecated helpers
447
+
448
+ `calculateProbWin` and `calculateProbWin_v2` are **deprecated**. They compute a
449
+ one-sided exceedance probability and ignore `price1`, so they do not match the model
450
+ placement validates against. A coefficient derived from them can pass the
451
+ server's one-directional slippage check and lock a lower payout than the cell is
452
+ worth. Use `calculateCoefficientWrapper`. They remain exported, unchanged, for
453
+ existing callers.
454
+
455
+ The lower-level pieces — `calculateProbHit`, `roundCoefToSignificantDigits`,
456
+ `computeBaseline`, `normalCDF`, `normalPDF`, `errorFunction`, `adaptiveSimpson` —
457
+ are also exported for consumers that need to build on them directly.
458
+
117
459
  ## License
118
460
 
119
461
  MIT
package/dist/index.cjs CHANGED
@@ -33,6 +33,8 @@ __export(index_exports, {
33
33
  AgencyPairModule: () => AgencyPairModule,
34
34
  AuthModule: () => AuthModule,
35
35
  BidModule: () => BidModule,
36
+ CANDLE_EVENT: () => CANDLE_EVENT,
37
+ DEFAULT_CHART_HISTORY_LIMIT: () => DEFAULT_CHART_HISTORY_LIMIT,
36
38
  LeaderboardModule: () => LeaderboardModule,
37
39
  LocaleModule: () => LocaleModule,
38
40
  NetworkQualityMonitor: () => NetworkQualityMonitor,
@@ -50,8 +52,11 @@ __export(index_exports, {
50
52
  UserModule: () => UserModule,
51
53
  adaptiveSimpson: () => adaptiveSimpson,
52
54
  autoDetectStorage: () => autoDetectStorage,
55
+ calculateCoefficientWrapper: () => calculateCoefficientWrapper,
56
+ calculateProbHit: () => calculateProbHit,
53
57
  calculateProbWin: () => calculateProbWin,
54
58
  calculateProbWin_v2: () => calculateProbWin_v2,
59
+ computeBaseline: () => computeBaseline,
55
60
  errorFunction: () => errorFunction,
56
61
  isCancelled: () => isCancelled,
57
62
  isLoss: () => isLoss,
@@ -61,7 +66,8 @@ __export(index_exports, {
61
66
  normalCDF: () => normalCDF,
62
67
  normalPDF: () => normalPDF,
63
68
  normaliseLang: () => normaliseLang,
64
- pairIdFromBidResultTopic: () => pairIdFromBidResultTopic
69
+ pairIdFromBidResultTopic: () => pairIdFromBidResultTopic,
70
+ roundCoefToSignificantDigits: () => roundCoefToSignificantDigits
65
71
  });
66
72
  module.exports = __toCommonJS(index_exports);
67
73
 
@@ -1147,6 +1153,9 @@ var BUILDER_AVAILABLE_GAME_PAIRS_QUERY = `query BuilderAvailableGamePairs($gamep
1147
1153
  }
1148
1154
  }`;
1149
1155
 
1156
+ // src/modules/pair/types.ts
1157
+ var DEFAULT_CHART_HISTORY_LIMIT = 600;
1158
+
1150
1159
  // src/modules/pair/index.ts
1151
1160
  var PairModule = class {
1152
1161
  #graphql;
@@ -1204,11 +1213,8 @@ var PairModule = class {
1204
1213
  return body.builderAvailableGamePairs.map(normalisePairInfo);
1205
1214
  }
1206
1215
  // REVIEW[bid-260602-v2]: chartHistory param/variable renamed pairId (was: agencyPairId/gameId)
1207
- async chartHistory(pairId, limit, opts) {
1208
- const variables = { pairId };
1209
- if (limit !== void 0) {
1210
- variables.limit = limit;
1211
- }
1216
+ async chartHistory(pairId, limit = DEFAULT_CHART_HISTORY_LIMIT, opts) {
1217
+ const variables = { pairId, limit };
1212
1218
  const body = await this.#graphql.request(
1213
1219
  CHART_HISTORY_QUERY,
1214
1220
  variables,
@@ -3159,6 +3165,12 @@ var TaphubClient = class {
3159
3165
  }
3160
3166
  };
3161
3167
 
3168
+ // src/modules/realtime/types.ts
3169
+ var CANDLE_EVENT = {
3170
+ NEW: "new",
3171
+ UPDATE: "update"
3172
+ };
3173
+
3162
3174
  // src/utils/coefficient.ts
3163
3175
  function errorFunction(x) {
3164
3176
  const a1 = 0.254829592;
@@ -3243,11 +3255,122 @@ function calculateProbWin_v2(time1, time2, _price1, price2, creationTime, curren
3243
3255
  const prob = 1 - normalCDF(z);
3244
3256
  return Math.max(0, Math.min(1, prob));
3245
3257
  }
3258
+ function simpsonForHit(f, a, b, eps = 3e-5, maxDepth = 14) {
3259
+ function simpsonRule(a2, b2) {
3260
+ const c = (a2 + b2) / 2;
3261
+ const h = b2 - a2;
3262
+ return h / 6 * (f(a2) + 4 * f(c) + f(b2));
3263
+ }
3264
+ function recurse(a2, b2, eps2, whole2, depth) {
3265
+ const c = (a2 + b2) / 2;
3266
+ const left = simpsonRule(a2, c);
3267
+ const right = simpsonRule(c, b2);
3268
+ if (depth >= maxDepth || Math.abs(left + right - whole2) <= 15 * eps2) {
3269
+ return left + right + (left + right - whole2) / 15;
3270
+ }
3271
+ return recurse(a2, c, eps2 / 2, left, depth + 1) + recurse(c, b2, eps2 / 2, right, depth + 1);
3272
+ }
3273
+ const whole = simpsonRule(a, b);
3274
+ return recurse(a, b, eps, whole, 0);
3275
+ }
3276
+ function calculateProbHit(time1, time2, price1, price2, creationTime, currentPrice, volatility) {
3277
+ if (volatility <= 0 || currentPrice <= 0) return 0;
3278
+ const sigma = volatility;
3279
+ const L = Math.log(price1 / currentPrice);
3280
+ const U = Math.log(price2 / currentPrice);
3281
+ let T1 = time1 - creationTime;
3282
+ if (T1 < 0) T1 = 0;
3283
+ const dT = time2 - time1;
3284
+ if (dT <= 0) return 0;
3285
+ let probInside;
3286
+ if (T1 <= 0) {
3287
+ probInside = currentPrice >= price1 && currentPrice <= price2 ? 1 : 0;
3288
+ } else {
3289
+ const sqrtT12 = Math.sqrt(T1);
3290
+ const sigSqrtT12 = sigma * sqrtT12;
3291
+ if (sigSqrtT12 < 1e-12) {
3292
+ probInside = currentPrice >= price1 && currentPrice <= price2 ? 1 : 0;
3293
+ } else {
3294
+ probInside = normalCDF(U / sigSqrtT12) - normalCDF(L / sigSqrtT12);
3295
+ }
3296
+ }
3297
+ if (T1 <= 0) {
3298
+ return Math.max(0, Math.min(1, probInside));
3299
+ }
3300
+ const sqrtT1 = Math.sqrt(T1);
3301
+ const sigSqrtT1 = sigma * sqrtT1;
3302
+ const sqrtRatio = Math.sqrt(T1 / dT);
3303
+ const sigSqrtDT = sigma * Math.sqrt(dT);
3304
+ const zU = U / sigSqrtT1;
3305
+ const integrandAbove = (z) => {
3306
+ return 2 * normalCDF(U / sigSqrtDT - z * sqrtRatio) * normalPDF(z);
3307
+ };
3308
+ const pFromAbove = simpsonForHit(integrandAbove, zU, zU + 7, 3e-5, 14);
3309
+ const zL = L / sigSqrtT1;
3310
+ const integrandBelow = (z) => {
3311
+ return 2 * normalCDF(z * sqrtRatio - L / sigSqrtDT) * normalPDF(z);
3312
+ };
3313
+ const pFromBelow = simpsonForHit(integrandBelow, zL - 7, zL, 3e-5, 14);
3314
+ const result = probInside + pFromAbove + pFromBelow;
3315
+ return Math.max(0, Math.min(1, result));
3316
+ }
3317
+ function calculateCoefficientWrapper(params) {
3318
+ const {
3319
+ time1,
3320
+ time2,
3321
+ price1,
3322
+ price2,
3323
+ candleTime,
3324
+ candleClose,
3325
+ volatility,
3326
+ coefMults,
3327
+ cellSizeTime,
3328
+ minCoef
3329
+ } = params;
3330
+ const currentPrice = candleClose;
3331
+ const creationTime = candleTime;
3332
+ const probability = calculateProbHit(
3333
+ time1,
3334
+ time2,
3335
+ price1,
3336
+ price2,
3337
+ creationTime,
3338
+ currentPrice,
3339
+ volatility
3340
+ );
3341
+ if (probability <= 0) return Number.POSITIVE_INFINITY;
3342
+ const rawCoef = 1 / probability;
3343
+ const rawIndex = rawCoef < 1 ? 0 : Math.floor(Math.log2(rawCoef));
3344
+ const coefMultIndex = Math.min(rawIndex, coefMults.length - 1);
3345
+ const multiplier = coefMults[coefMultIndex] || 1;
3346
+ const timeRatio = cellSizeTime;
3347
+ const adjustedProb = probability * (timeRatio / 5);
3348
+ if (adjustedProb <= 0) return Number.POSITIVE_INFINITY;
3349
+ const floor = typeof minCoef === "number" && minCoef > 0 ? minCoef : 1;
3350
+ const finalCoef = Math.max(floor, multiplier / adjustedProb);
3351
+ return roundCoefToSignificantDigits(finalCoef);
3352
+ }
3353
+ function roundCoefToSignificantDigits(value) {
3354
+ if (!Number.isFinite(value) || value <= 0) return value;
3355
+ const magnitude = Math.floor(Math.log10(value));
3356
+ const leadingDigit = Math.floor(value / 10 ** magnitude);
3357
+ const sigDigits = leadingDigit === 1 ? 3 : 2;
3358
+ const factor = 10 ** (sigDigits - magnitude - 1);
3359
+ return Math.round(value * factor) / factor;
3360
+ }
3361
+ function computeBaseline(candleClose, candleTimeSec, cellSizeValue, cellSizeTime, candleSize) {
3362
+ const baseline = Math.floor(candleClose / cellSizeValue) * cellSizeValue;
3363
+ const cellSizeTimeSec = candleSize * cellSizeTime;
3364
+ const baselineTime = Math.floor(candleTimeSec / cellSizeTimeSec) * cellSizeTimeSec + 0.5;
3365
+ return { baseline, baselineTime };
3366
+ }
3246
3367
  // Annotate the CommonJS export names for ESM import in node:
3247
3368
  0 && (module.exports = {
3248
3369
  AgencyPairModule,
3249
3370
  AuthModule,
3250
3371
  BidModule,
3372
+ CANDLE_EVENT,
3373
+ DEFAULT_CHART_HISTORY_LIMIT,
3251
3374
  LeaderboardModule,
3252
3375
  LocaleModule,
3253
3376
  NetworkQualityMonitor,
@@ -3265,8 +3388,11 @@ function calculateProbWin_v2(time1, time2, _price1, price2, creationTime, curren
3265
3388
  UserModule,
3266
3389
  adaptiveSimpson,
3267
3390
  autoDetectStorage,
3391
+ calculateCoefficientWrapper,
3392
+ calculateProbHit,
3268
3393
  calculateProbWin,
3269
3394
  calculateProbWin_v2,
3395
+ computeBaseline,
3270
3396
  errorFunction,
3271
3397
  isCancelled,
3272
3398
  isLoss,
@@ -3276,5 +3402,6 @@ function calculateProbWin_v2(time1, time2, _price1, price2, creationTime, curren
3276
3402
  normalCDF,
3277
3403
  normalPDF,
3278
3404
  normaliseLang,
3279
- pairIdFromBidResultTopic
3405
+ pairIdFromBidResultTopic,
3406
+ roundCoefToSignificantDigits
3280
3407
  });
package/dist/index.d.mts CHANGED
@@ -709,6 +709,13 @@ interface PairInfo {
709
709
  */
710
710
  agencyPairId: string | null;
711
711
  }
712
+ /**
713
+ * Default number of candles fetched on initial chart load when the caller does
714
+ * not pass an explicit limit. The server clamps any request to its cache size
715
+ * (grid-api services.ChartHistoryMaxLimit = 1500), so this is purely how much
716
+ * history the chart shows by default — kept lower than the cap for faster init.
717
+ */
718
+ declare const DEFAULT_CHART_HISTORY_LIMIT = 600;
712
719
  interface Candle {
713
720
  /** Unix epoch SECONDS (not ms). Multiply by 1000 for JS Date. */
714
721
  time: number;
@@ -906,6 +913,16 @@ interface MqttTransport {
906
913
  close(): void;
907
914
  }
908
915
 
916
+ /**
917
+ * Candle tick kind. `new` = a fresh bar opened; `update` = the current bar was
918
+ * revised. Exported as named constants so consumers compare against these
919
+ * instead of bare string literals (a typo on a literal fails silently).
920
+ */
921
+ declare const CANDLE_EVENT: {
922
+ readonly NEW: "new";
923
+ readonly UPDATE: "update";
924
+ };
925
+ type CandleEventType = (typeof CANDLE_EVENT)[keyof typeof CANDLE_EVENT];
909
926
  /**
910
927
  * Candle tick from MQTT.
911
928
  * WARNING: `time` is Unix epoch MILLISECONDS.
@@ -913,7 +930,7 @@ interface MqttTransport {
913
930
  * Do NOT multiply MQTT candle time by 1000.
914
931
  */
915
932
  interface MqttCandleEvent {
916
- type: 'new' | 'update';
933
+ type: CandleEventType;
917
934
  /** Unix epoch **milliseconds** — differs from GQL chartHistory (seconds) */
918
935
  time: number;
919
936
  o: number;
@@ -1412,7 +1429,75 @@ declare function errorFunction(x: number): number;
1412
1429
  declare function normalCDF(x: number): number;
1413
1430
  declare function normalPDF(x: number): number;
1414
1431
  declare function adaptiveSimpson(f: (x: number) => number, a: number, b: number, tolerance?: number, maxDepth?: number): number;
1432
+ /**
1433
+ * @deprecated Does NOT match the model grid-api validates a bid against. Use
1434
+ * {@link calculateCoefficientWrapper} to derive the `coefficient` argument for
1435
+ * `placeBid`.
1436
+ *
1437
+ * This computes a one-sided exceedance probability — roughly "will the price end up
1438
+ * past `price2`" — and takes `price1` without using it. The game pays out when the
1439
+ * price *visits the band* `[price1, price2]`, which is a first-passage probability;
1440
+ * {@link calculateProbHit} computes that one. Measured against grid-api's reference
1441
+ * inputs, this function returns only 1.0, 0.5 or 0.0 and does not move at all when
1442
+ * `price1` goes from 2869.75 to 2800.00.
1443
+ *
1444
+ * The consequence is not a rejected bid. `placeBid`'s slippage check is
1445
+ * one-directional — it rejects a coefficient *higher* than the server's, never a
1446
+ * lower one — so a coefficient derived from this function (e.g. 1.0 where the server
1447
+ * computes 6.1) is accepted and locked, and the player is paid at the lower
1448
+ * coefficient on a win, with no error raised anywhere.
1449
+ *
1450
+ * Retained unchanged because it is published API (TH-491, Add-Deprecate-Remove).
1451
+ */
1415
1452
  declare function calculateProbWin(time1: number, time2: number, _price1: number, price2: number, creationTime: number, currentPrice: number, volatility: number): number;
1453
+ /**
1454
+ * @deprecated Does NOT match the model grid-api validates a bid against. Use
1455
+ * {@link calculateCoefficientWrapper} to derive the `coefficient` argument for
1456
+ * `placeBid`.
1457
+ *
1458
+ * This computes a one-sided exceedance probability — roughly "will the price end up
1459
+ * past `price2`" — and takes `price1` without using it. The game pays out when the
1460
+ * price *visits the band* `[price1, price2]`, which is a first-passage probability;
1461
+ * {@link calculateProbHit} computes that one. Measured against grid-api's reference
1462
+ * inputs, this function returns only 1.0, 0.5 or 0.0 and does not move at all when
1463
+ * `price1` goes from 2869.75 to 2800.00.
1464
+ *
1465
+ * The consequence is not a rejected bid. `placeBid`'s slippage check is
1466
+ * one-directional — it rejects a coefficient *higher* than the server's, never a
1467
+ * lower one — so a coefficient derived from this function (e.g. 1.0 where the server
1468
+ * computes 6.1) is accepted and locked, and the player is paid at the lower
1469
+ * coefficient on a win, with no error raised anywhere.
1470
+ *
1471
+ * Retained unchanged because it is published API (TH-491, Add-Deprecate-Remove).
1472
+ */
1416
1473
  declare function calculateProbWin_v2(time1: number, time2: number, _price1: number, price2: number, creationTime: number, currentPrice: number, volatility: number): number;
1474
+ interface CoefficientInput {
1475
+ time1: number;
1476
+ time2: number;
1477
+ price1: number;
1478
+ price2: number;
1479
+ candleTime: number;
1480
+ candleClose: number;
1481
+ volatility: number;
1482
+ coefMults: number[];
1483
+ cellSizeTime: number;
1484
+ candleSize: number;
1485
+ minCoef?: number;
1486
+ }
1487
+ /**
1488
+ * First-passage hitting probability: chance the price visits [price1, price2]
1489
+ * at any point during [time1, time2], given GBM dynamics.
1490
+ *
1491
+ * P_hit = P_inside + P_from_above + P_from_below
1492
+ *
1493
+ * Matches Go backend algorithm.
1494
+ */
1495
+ declare function calculateProbHit(time1: number, time2: number, price1: number, price2: number, creationTime: number, currentPrice: number, volatility: number): number;
1496
+ declare function calculateCoefficientWrapper(params: CoefficientInput): number;
1497
+ declare function roundCoefToSignificantDigits(value: number): number;
1498
+ declare function computeBaseline(candleClose: number, candleTimeSec: number, cellSizeValue: number, cellSizeTime: number, candleSize: number): {
1499
+ baseline: number;
1500
+ baselineTime: number;
1501
+ };
1417
1502
 
1418
- export { type AgencyPairFilter, AgencyPairModule, type AgencyPairStats, type AgencyPairStatus, AuthModule, type BackendHealth, type Bid, BidModule, type BidStatus, type CancelBidResult, type Candle, type Constraints, type Currency, type ErrorMessageCatalog, type ErrorMessageMeta, type ErrorMessagesResponse, GameChannel, type GameConfig, type GridConfig, type LeaderboardEntry, LeaderboardModule, type LeaderboardPeriod, type LeaderboardSortBy, LocaleModule, type LocaleRefreshResult, type LocaleResponse, type LocaleTranslations, type LoginResult, type Me, type MqttAcceptedBid, type MqttAgencyPairStatsEvent, type MqttAuthConfig, type MqttBalanceEvent, type MqttBidAcceptedEvent, type MqttBidLostEvent, type MqttBidWonEvent, type MqttCandleEvent, type MqttConfigEvent, type MqttIdealConfigEvent, type MqttWalletBalanceEvent, type MyRank, type NetworkLevel, type NetworkQuality, NetworkQualityMonitor, type Pair, type PairInfo, PairModule, type PlaceBidInput, type RankBoard, type RankEntry, type RankPeriod, type RankSort, RealtimeModule, type Sample, type SampleReason, type SignalSource, type SortDir, TaphubAuthError, TaphubClient, type TaphubClientConfig, TaphubError, TaphubEventBus, type TaphubEventMap, TaphubNetworkError, TaphubServerError, TaphubSlippageError, TaphubStorage, type TaphubStorageAdapter, TaphubValidationError, type User, UserBidsChannel, type UserBidsChannelEvents, UserModule, type UserPnL, type UserPnLPeriod, type Wallet as UserWallet, type Wallet$1 as Wallet, type WalletBalanceReason, WalletChannel, adaptiveSimpson, autoDetectStorage, calculateProbWin, calculateProbWin_v2, errorFunction, isCancelled, isLoss, isPending, isTerminal, isWin, normalCDF, normalPDF, normaliseLang, pairIdFromBidResultTopic };
1503
+ export { type AgencyPairFilter, AgencyPairModule, type AgencyPairStats, type AgencyPairStatus, AuthModule, type BackendHealth, type Bid, BidModule, type BidStatus, CANDLE_EVENT, type CancelBidResult, type Candle, type CandleEventType, type CoefficientInput, type Constraints, type Currency, DEFAULT_CHART_HISTORY_LIMIT, type ErrorMessageCatalog, type ErrorMessageMeta, type ErrorMessagesResponse, GameChannel, type GameConfig, type GridConfig, type LeaderboardEntry, LeaderboardModule, type LeaderboardPeriod, type LeaderboardSortBy, LocaleModule, type LocaleRefreshResult, type LocaleResponse, type LocaleTranslations, type LoginResult, type Me, type MqttAcceptedBid, type MqttAgencyPairStatsEvent, type MqttAuthConfig, type MqttBalanceEvent, type MqttBidAcceptedEvent, type MqttBidLostEvent, type MqttBidWonEvent, type MqttCandleEvent, type MqttConfigEvent, type MqttIdealConfigEvent, type MqttWalletBalanceEvent, type MyRank, type NetworkLevel, type NetworkQuality, NetworkQualityMonitor, type Pair, type PairInfo, PairModule, type PlaceBidInput, type RankBoard, type RankEntry, type RankPeriod, type RankSort, RealtimeModule, type Sample, type SampleReason, type SignalSource, type SortDir, TaphubAuthError, TaphubClient, type TaphubClientConfig, TaphubError, TaphubEventBus, type TaphubEventMap, TaphubNetworkError, TaphubServerError, TaphubSlippageError, TaphubStorage, type TaphubStorageAdapter, TaphubValidationError, type User, UserBidsChannel, type UserBidsChannelEvents, UserModule, type UserPnL, type UserPnLPeriod, type Wallet as UserWallet, type Wallet$1 as Wallet, type WalletBalanceReason, WalletChannel, adaptiveSimpson, autoDetectStorage, calculateCoefficientWrapper, calculateProbHit, calculateProbWin, calculateProbWin_v2, computeBaseline, errorFunction, isCancelled, isLoss, isPending, isTerminal, isWin, normalCDF, normalPDF, normaliseLang, pairIdFromBidResultTopic, roundCoefToSignificantDigits };
package/dist/index.d.ts CHANGED
@@ -709,6 +709,13 @@ interface PairInfo {
709
709
  */
710
710
  agencyPairId: string | null;
711
711
  }
712
+ /**
713
+ * Default number of candles fetched on initial chart load when the caller does
714
+ * not pass an explicit limit. The server clamps any request to its cache size
715
+ * (grid-api services.ChartHistoryMaxLimit = 1500), so this is purely how much
716
+ * history the chart shows by default — kept lower than the cap for faster init.
717
+ */
718
+ declare const DEFAULT_CHART_HISTORY_LIMIT = 600;
712
719
  interface Candle {
713
720
  /** Unix epoch SECONDS (not ms). Multiply by 1000 for JS Date. */
714
721
  time: number;
@@ -906,6 +913,16 @@ interface MqttTransport {
906
913
  close(): void;
907
914
  }
908
915
 
916
+ /**
917
+ * Candle tick kind. `new` = a fresh bar opened; `update` = the current bar was
918
+ * revised. Exported as named constants so consumers compare against these
919
+ * instead of bare string literals (a typo on a literal fails silently).
920
+ */
921
+ declare const CANDLE_EVENT: {
922
+ readonly NEW: "new";
923
+ readonly UPDATE: "update";
924
+ };
925
+ type CandleEventType = (typeof CANDLE_EVENT)[keyof typeof CANDLE_EVENT];
909
926
  /**
910
927
  * Candle tick from MQTT.
911
928
  * WARNING: `time` is Unix epoch MILLISECONDS.
@@ -913,7 +930,7 @@ interface MqttTransport {
913
930
  * Do NOT multiply MQTT candle time by 1000.
914
931
  */
915
932
  interface MqttCandleEvent {
916
- type: 'new' | 'update';
933
+ type: CandleEventType;
917
934
  /** Unix epoch **milliseconds** — differs from GQL chartHistory (seconds) */
918
935
  time: number;
919
936
  o: number;
@@ -1412,7 +1429,75 @@ declare function errorFunction(x: number): number;
1412
1429
  declare function normalCDF(x: number): number;
1413
1430
  declare function normalPDF(x: number): number;
1414
1431
  declare function adaptiveSimpson(f: (x: number) => number, a: number, b: number, tolerance?: number, maxDepth?: number): number;
1432
+ /**
1433
+ * @deprecated Does NOT match the model grid-api validates a bid against. Use
1434
+ * {@link calculateCoefficientWrapper} to derive the `coefficient` argument for
1435
+ * `placeBid`.
1436
+ *
1437
+ * This computes a one-sided exceedance probability — roughly "will the price end up
1438
+ * past `price2`" — and takes `price1` without using it. The game pays out when the
1439
+ * price *visits the band* `[price1, price2]`, which is a first-passage probability;
1440
+ * {@link calculateProbHit} computes that one. Measured against grid-api's reference
1441
+ * inputs, this function returns only 1.0, 0.5 or 0.0 and does not move at all when
1442
+ * `price1` goes from 2869.75 to 2800.00.
1443
+ *
1444
+ * The consequence is not a rejected bid. `placeBid`'s slippage check is
1445
+ * one-directional — it rejects a coefficient *higher* than the server's, never a
1446
+ * lower one — so a coefficient derived from this function (e.g. 1.0 where the server
1447
+ * computes 6.1) is accepted and locked, and the player is paid at the lower
1448
+ * coefficient on a win, with no error raised anywhere.
1449
+ *
1450
+ * Retained unchanged because it is published API (TH-491, Add-Deprecate-Remove).
1451
+ */
1415
1452
  declare function calculateProbWin(time1: number, time2: number, _price1: number, price2: number, creationTime: number, currentPrice: number, volatility: number): number;
1453
+ /**
1454
+ * @deprecated Does NOT match the model grid-api validates a bid against. Use
1455
+ * {@link calculateCoefficientWrapper} to derive the `coefficient` argument for
1456
+ * `placeBid`.
1457
+ *
1458
+ * This computes a one-sided exceedance probability — roughly "will the price end up
1459
+ * past `price2`" — and takes `price1` without using it. The game pays out when the
1460
+ * price *visits the band* `[price1, price2]`, which is a first-passage probability;
1461
+ * {@link calculateProbHit} computes that one. Measured against grid-api's reference
1462
+ * inputs, this function returns only 1.0, 0.5 or 0.0 and does not move at all when
1463
+ * `price1` goes from 2869.75 to 2800.00.
1464
+ *
1465
+ * The consequence is not a rejected bid. `placeBid`'s slippage check is
1466
+ * one-directional — it rejects a coefficient *higher* than the server's, never a
1467
+ * lower one — so a coefficient derived from this function (e.g. 1.0 where the server
1468
+ * computes 6.1) is accepted and locked, and the player is paid at the lower
1469
+ * coefficient on a win, with no error raised anywhere.
1470
+ *
1471
+ * Retained unchanged because it is published API (TH-491, Add-Deprecate-Remove).
1472
+ */
1416
1473
  declare function calculateProbWin_v2(time1: number, time2: number, _price1: number, price2: number, creationTime: number, currentPrice: number, volatility: number): number;
1474
+ interface CoefficientInput {
1475
+ time1: number;
1476
+ time2: number;
1477
+ price1: number;
1478
+ price2: number;
1479
+ candleTime: number;
1480
+ candleClose: number;
1481
+ volatility: number;
1482
+ coefMults: number[];
1483
+ cellSizeTime: number;
1484
+ candleSize: number;
1485
+ minCoef?: number;
1486
+ }
1487
+ /**
1488
+ * First-passage hitting probability: chance the price visits [price1, price2]
1489
+ * at any point during [time1, time2], given GBM dynamics.
1490
+ *
1491
+ * P_hit = P_inside + P_from_above + P_from_below
1492
+ *
1493
+ * Matches Go backend algorithm.
1494
+ */
1495
+ declare function calculateProbHit(time1: number, time2: number, price1: number, price2: number, creationTime: number, currentPrice: number, volatility: number): number;
1496
+ declare function calculateCoefficientWrapper(params: CoefficientInput): number;
1497
+ declare function roundCoefToSignificantDigits(value: number): number;
1498
+ declare function computeBaseline(candleClose: number, candleTimeSec: number, cellSizeValue: number, cellSizeTime: number, candleSize: number): {
1499
+ baseline: number;
1500
+ baselineTime: number;
1501
+ };
1417
1502
 
1418
- export { type AgencyPairFilter, AgencyPairModule, type AgencyPairStats, type AgencyPairStatus, AuthModule, type BackendHealth, type Bid, BidModule, type BidStatus, type CancelBidResult, type Candle, type Constraints, type Currency, type ErrorMessageCatalog, type ErrorMessageMeta, type ErrorMessagesResponse, GameChannel, type GameConfig, type GridConfig, type LeaderboardEntry, LeaderboardModule, type LeaderboardPeriod, type LeaderboardSortBy, LocaleModule, type LocaleRefreshResult, type LocaleResponse, type LocaleTranslations, type LoginResult, type Me, type MqttAcceptedBid, type MqttAgencyPairStatsEvent, type MqttAuthConfig, type MqttBalanceEvent, type MqttBidAcceptedEvent, type MqttBidLostEvent, type MqttBidWonEvent, type MqttCandleEvent, type MqttConfigEvent, type MqttIdealConfigEvent, type MqttWalletBalanceEvent, type MyRank, type NetworkLevel, type NetworkQuality, NetworkQualityMonitor, type Pair, type PairInfo, PairModule, type PlaceBidInput, type RankBoard, type RankEntry, type RankPeriod, type RankSort, RealtimeModule, type Sample, type SampleReason, type SignalSource, type SortDir, TaphubAuthError, TaphubClient, type TaphubClientConfig, TaphubError, TaphubEventBus, type TaphubEventMap, TaphubNetworkError, TaphubServerError, TaphubSlippageError, TaphubStorage, type TaphubStorageAdapter, TaphubValidationError, type User, UserBidsChannel, type UserBidsChannelEvents, UserModule, type UserPnL, type UserPnLPeriod, type Wallet as UserWallet, type Wallet$1 as Wallet, type WalletBalanceReason, WalletChannel, adaptiveSimpson, autoDetectStorage, calculateProbWin, calculateProbWin_v2, errorFunction, isCancelled, isLoss, isPending, isTerminal, isWin, normalCDF, normalPDF, normaliseLang, pairIdFromBidResultTopic };
1503
+ export { type AgencyPairFilter, AgencyPairModule, type AgencyPairStats, type AgencyPairStatus, AuthModule, type BackendHealth, type Bid, BidModule, type BidStatus, CANDLE_EVENT, type CancelBidResult, type Candle, type CandleEventType, type CoefficientInput, type Constraints, type Currency, DEFAULT_CHART_HISTORY_LIMIT, type ErrorMessageCatalog, type ErrorMessageMeta, type ErrorMessagesResponse, GameChannel, type GameConfig, type GridConfig, type LeaderboardEntry, LeaderboardModule, type LeaderboardPeriod, type LeaderboardSortBy, LocaleModule, type LocaleRefreshResult, type LocaleResponse, type LocaleTranslations, type LoginResult, type Me, type MqttAcceptedBid, type MqttAgencyPairStatsEvent, type MqttAuthConfig, type MqttBalanceEvent, type MqttBidAcceptedEvent, type MqttBidLostEvent, type MqttBidWonEvent, type MqttCandleEvent, type MqttConfigEvent, type MqttIdealConfigEvent, type MqttWalletBalanceEvent, type MyRank, type NetworkLevel, type NetworkQuality, NetworkQualityMonitor, type Pair, type PairInfo, PairModule, type PlaceBidInput, type RankBoard, type RankEntry, type RankPeriod, type RankSort, RealtimeModule, type Sample, type SampleReason, type SignalSource, type SortDir, TaphubAuthError, TaphubClient, type TaphubClientConfig, TaphubError, TaphubEventBus, type TaphubEventMap, TaphubNetworkError, TaphubServerError, TaphubSlippageError, TaphubStorage, type TaphubStorageAdapter, TaphubValidationError, type User, UserBidsChannel, type UserBidsChannelEvents, UserModule, type UserPnL, type UserPnLPeriod, type Wallet as UserWallet, type Wallet$1 as Wallet, type WalletBalanceReason, WalletChannel, adaptiveSimpson, autoDetectStorage, calculateCoefficientWrapper, calculateProbHit, calculateProbWin, calculateProbWin_v2, computeBaseline, errorFunction, isCancelled, isLoss, isPending, isTerminal, isWin, normalCDF, normalPDF, normaliseLang, pairIdFromBidResultTopic, roundCoefToSignificantDigits };
package/dist/index.js CHANGED
@@ -1080,6 +1080,9 @@ var BUILDER_AVAILABLE_GAME_PAIRS_QUERY = `query BuilderAvailableGamePairs($gamep
1080
1080
  }
1081
1081
  }`;
1082
1082
 
1083
+ // src/modules/pair/types.ts
1084
+ var DEFAULT_CHART_HISTORY_LIMIT = 600;
1085
+
1083
1086
  // src/modules/pair/index.ts
1084
1087
  var PairModule = class {
1085
1088
  #graphql;
@@ -1137,11 +1140,8 @@ var PairModule = class {
1137
1140
  return body.builderAvailableGamePairs.map(normalisePairInfo);
1138
1141
  }
1139
1142
  // REVIEW[bid-260602-v2]: chartHistory param/variable renamed pairId (was: agencyPairId/gameId)
1140
- async chartHistory(pairId, limit, opts) {
1141
- const variables = { pairId };
1142
- if (limit !== void 0) {
1143
- variables.limit = limit;
1144
- }
1143
+ async chartHistory(pairId, limit = DEFAULT_CHART_HISTORY_LIMIT, opts) {
1144
+ const variables = { pairId, limit };
1145
1145
  const body = await this.#graphql.request(
1146
1146
  CHART_HISTORY_QUERY,
1147
1147
  variables,
@@ -3092,6 +3092,12 @@ var TaphubClient = class {
3092
3092
  }
3093
3093
  };
3094
3094
 
3095
+ // src/modules/realtime/types.ts
3096
+ var CANDLE_EVENT = {
3097
+ NEW: "new",
3098
+ UPDATE: "update"
3099
+ };
3100
+
3095
3101
  // src/utils/coefficient.ts
3096
3102
  function errorFunction(x) {
3097
3103
  const a1 = 0.254829592;
@@ -3176,10 +3182,121 @@ function calculateProbWin_v2(time1, time2, _price1, price2, creationTime, curren
3176
3182
  const prob = 1 - normalCDF(z);
3177
3183
  return Math.max(0, Math.min(1, prob));
3178
3184
  }
3185
+ function simpsonForHit(f, a, b, eps = 3e-5, maxDepth = 14) {
3186
+ function simpsonRule(a2, b2) {
3187
+ const c = (a2 + b2) / 2;
3188
+ const h = b2 - a2;
3189
+ return h / 6 * (f(a2) + 4 * f(c) + f(b2));
3190
+ }
3191
+ function recurse(a2, b2, eps2, whole2, depth) {
3192
+ const c = (a2 + b2) / 2;
3193
+ const left = simpsonRule(a2, c);
3194
+ const right = simpsonRule(c, b2);
3195
+ if (depth >= maxDepth || Math.abs(left + right - whole2) <= 15 * eps2) {
3196
+ return left + right + (left + right - whole2) / 15;
3197
+ }
3198
+ return recurse(a2, c, eps2 / 2, left, depth + 1) + recurse(c, b2, eps2 / 2, right, depth + 1);
3199
+ }
3200
+ const whole = simpsonRule(a, b);
3201
+ return recurse(a, b, eps, whole, 0);
3202
+ }
3203
+ function calculateProbHit(time1, time2, price1, price2, creationTime, currentPrice, volatility) {
3204
+ if (volatility <= 0 || currentPrice <= 0) return 0;
3205
+ const sigma = volatility;
3206
+ const L = Math.log(price1 / currentPrice);
3207
+ const U = Math.log(price2 / currentPrice);
3208
+ let T1 = time1 - creationTime;
3209
+ if (T1 < 0) T1 = 0;
3210
+ const dT = time2 - time1;
3211
+ if (dT <= 0) return 0;
3212
+ let probInside;
3213
+ if (T1 <= 0) {
3214
+ probInside = currentPrice >= price1 && currentPrice <= price2 ? 1 : 0;
3215
+ } else {
3216
+ const sqrtT12 = Math.sqrt(T1);
3217
+ const sigSqrtT12 = sigma * sqrtT12;
3218
+ if (sigSqrtT12 < 1e-12) {
3219
+ probInside = currentPrice >= price1 && currentPrice <= price2 ? 1 : 0;
3220
+ } else {
3221
+ probInside = normalCDF(U / sigSqrtT12) - normalCDF(L / sigSqrtT12);
3222
+ }
3223
+ }
3224
+ if (T1 <= 0) {
3225
+ return Math.max(0, Math.min(1, probInside));
3226
+ }
3227
+ const sqrtT1 = Math.sqrt(T1);
3228
+ const sigSqrtT1 = sigma * sqrtT1;
3229
+ const sqrtRatio = Math.sqrt(T1 / dT);
3230
+ const sigSqrtDT = sigma * Math.sqrt(dT);
3231
+ const zU = U / sigSqrtT1;
3232
+ const integrandAbove = (z) => {
3233
+ return 2 * normalCDF(U / sigSqrtDT - z * sqrtRatio) * normalPDF(z);
3234
+ };
3235
+ const pFromAbove = simpsonForHit(integrandAbove, zU, zU + 7, 3e-5, 14);
3236
+ const zL = L / sigSqrtT1;
3237
+ const integrandBelow = (z) => {
3238
+ return 2 * normalCDF(z * sqrtRatio - L / sigSqrtDT) * normalPDF(z);
3239
+ };
3240
+ const pFromBelow = simpsonForHit(integrandBelow, zL - 7, zL, 3e-5, 14);
3241
+ const result = probInside + pFromAbove + pFromBelow;
3242
+ return Math.max(0, Math.min(1, result));
3243
+ }
3244
+ function calculateCoefficientWrapper(params) {
3245
+ const {
3246
+ time1,
3247
+ time2,
3248
+ price1,
3249
+ price2,
3250
+ candleTime,
3251
+ candleClose,
3252
+ volatility,
3253
+ coefMults,
3254
+ cellSizeTime,
3255
+ minCoef
3256
+ } = params;
3257
+ const currentPrice = candleClose;
3258
+ const creationTime = candleTime;
3259
+ const probability = calculateProbHit(
3260
+ time1,
3261
+ time2,
3262
+ price1,
3263
+ price2,
3264
+ creationTime,
3265
+ currentPrice,
3266
+ volatility
3267
+ );
3268
+ if (probability <= 0) return Number.POSITIVE_INFINITY;
3269
+ const rawCoef = 1 / probability;
3270
+ const rawIndex = rawCoef < 1 ? 0 : Math.floor(Math.log2(rawCoef));
3271
+ const coefMultIndex = Math.min(rawIndex, coefMults.length - 1);
3272
+ const multiplier = coefMults[coefMultIndex] || 1;
3273
+ const timeRatio = cellSizeTime;
3274
+ const adjustedProb = probability * (timeRatio / 5);
3275
+ if (adjustedProb <= 0) return Number.POSITIVE_INFINITY;
3276
+ const floor = typeof minCoef === "number" && minCoef > 0 ? minCoef : 1;
3277
+ const finalCoef = Math.max(floor, multiplier / adjustedProb);
3278
+ return roundCoefToSignificantDigits(finalCoef);
3279
+ }
3280
+ function roundCoefToSignificantDigits(value) {
3281
+ if (!Number.isFinite(value) || value <= 0) return value;
3282
+ const magnitude = Math.floor(Math.log10(value));
3283
+ const leadingDigit = Math.floor(value / 10 ** magnitude);
3284
+ const sigDigits = leadingDigit === 1 ? 3 : 2;
3285
+ const factor = 10 ** (sigDigits - magnitude - 1);
3286
+ return Math.round(value * factor) / factor;
3287
+ }
3288
+ function computeBaseline(candleClose, candleTimeSec, cellSizeValue, cellSizeTime, candleSize) {
3289
+ const baseline = Math.floor(candleClose / cellSizeValue) * cellSizeValue;
3290
+ const cellSizeTimeSec = candleSize * cellSizeTime;
3291
+ const baselineTime = Math.floor(candleTimeSec / cellSizeTimeSec) * cellSizeTimeSec + 0.5;
3292
+ return { baseline, baselineTime };
3293
+ }
3179
3294
  export {
3180
3295
  AgencyPairModule,
3181
3296
  AuthModule,
3182
3297
  BidModule,
3298
+ CANDLE_EVENT,
3299
+ DEFAULT_CHART_HISTORY_LIMIT,
3183
3300
  LeaderboardModule,
3184
3301
  LocaleModule,
3185
3302
  NetworkQualityMonitor,
@@ -3197,8 +3314,11 @@ export {
3197
3314
  UserModule,
3198
3315
  adaptiveSimpson,
3199
3316
  autoDetectStorage,
3317
+ calculateCoefficientWrapper,
3318
+ calculateProbHit,
3200
3319
  calculateProbWin,
3201
3320
  calculateProbWin_v2,
3321
+ computeBaseline,
3202
3322
  errorFunction,
3203
3323
  isCancelled,
3204
3324
  isLoss,
@@ -3208,5 +3328,6 @@ export {
3208
3328
  normalCDF,
3209
3329
  normalPDF,
3210
3330
  normaliseLang,
3211
- pairIdFromBidResultTopic
3331
+ pairIdFromBidResultTopic,
3332
+ roundCoefToSignificantDigits
3212
3333
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@taphubhq/sdk-core",
3
- "version": "0.25.4",
3
+ "version": "0.25.6",
4
4
  "description": "Core SDK for building on the TabHub platform",
5
5
  "license": "MIT",
6
6
  "main": "./dist/index.cjs",