@taphubhq/sdk-core 0.26.1 → 0.28.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/README.md CHANGED
@@ -12,6 +12,18 @@ For React bindings (hooks, providers, i18n), use
12
12
  pnpm add @taphubhq/sdk-core
13
13
  ```
14
14
 
15
+ ### Server compatibility
16
+
17
+ The SDK's queries select the fields they need unconditionally, so a release that
18
+ adds a field also raises the minimum grid-api it runs against. Check this before
19
+ upgrading in an environment you do not deploy the server to.
20
+
21
+ | sdk-core | Needs grid-api serving | Or else |
22
+ | --- | --- | --- |
23
+ | 0.27.0+ | `Bid.cellCandle { o h l c final touchSec touchPrice }` | `myBids` fails GraphQL validation and returns **no bids** |
24
+
25
+ Deploy grid-api before the client, on every environment.
26
+
15
27
  ## Minimum Integration
16
28
 
17
29
  ```ts
package/dist/index.cjs CHANGED
@@ -60,6 +60,8 @@ __export(index_exports, {
60
60
  calculateProbHit: () => calculateProbHit,
61
61
  calculateProbWin: () => calculateProbWin,
62
62
  calculateProbWin_v2: () => calculateProbWin_v2,
63
+ coefMultBandIndex: () => coefMultBandIndex,
64
+ coefMultiplierAt: () => coefMultiplierAt,
63
65
  computeBaseline: () => computeBaseline,
64
66
  errorFunction: () => errorFunction,
65
67
  isCancelled: () => isCancelled,
@@ -661,7 +663,22 @@ function normaliseBid(node) {
661
663
  refundAmount: node.refund_amount ?? null,
662
664
  slippage: node.slippage,
663
665
  createdAt: node.created_at || null,
664
- meta: parseMeta(node.meta)
666
+ meta: parseMeta(node.meta),
667
+ // Absent and explicit null both mean "no candle" — an older server, an unselected
668
+ // field, a pending bid and purged ticks must all read the same to a consumer.
669
+ // `final` null (nullable Boolean) collapses to absent = settled, so only a real
670
+ // `false` puts the row in the in-progress presentation.
671
+ cellCandle: node.cellCandle ? {
672
+ o: node.cellCandle.o,
673
+ h: node.cellCandle.h,
674
+ l: node.cellCandle.l,
675
+ c: node.cellCandle.c,
676
+ ...typeof node.cellCandle.final === "boolean" ? { final: node.cellCandle.final } : {},
677
+ // Both legs or neither — see BidCellCandle.touchSec. Nulls (a non-win,
678
+ // or an older server) collapse to absent so a consumer only has to
679
+ // check one of them.
680
+ ...typeof node.cellCandle.touchSec === "number" && typeof node.cellCandle.touchPrice === "string" ? { touchSec: node.cellCandle.touchSec, touchPrice: node.cellCandle.touchPrice } : {}
681
+ } : null
665
682
  };
666
683
  }
667
684
  function normaliseBids(list) {
@@ -682,6 +699,7 @@ var PLACE_BID_MUTATION = `mutation PlaceBid($input: PlaceBidInput!) {
682
699
  var MY_BIDS_QUERY = `query MyBids($statuses: [BidStatus!], $limit: Int, $offset: Int, $pairId: ID) {
683
700
  myBids(statuses: $statuses, limit: $limit, offset: $offset, pairId: $pairId) {
684
701
  id user_id pair_id currency amount coefficient time1 time2 price1 price2 status payout refund_rate refund_amount slippage created_at meta
702
+ cellCandle { o h l c final touchSec touchPrice }
685
703
  }
686
704
  }`;
687
705
  var CANCEL_BID_MUTATION = `mutation CancelBid($input: CancelBidInput!) {
@@ -1238,7 +1256,9 @@ function normaliseConstraints(node) {
1238
1256
  priceMinRange: node.priceMinRange,
1239
1257
  priceMaxRange: node.priceMaxRange,
1240
1258
  coefMults: node.coefMults,
1241
- maxCoef: node.maxCoef
1259
+ maxCoef: node.maxCoef,
1260
+ // bid-260819: pass through untouched — pricing consumes it as-is.
1261
+ coefMultsMatrix: node.coefMultsMatrix ?? null
1242
1262
  };
1243
1263
  }
1244
1264
  function normaliseCandles(list) {
@@ -1291,7 +1311,7 @@ var PAIR_QUERY = `query AgencyPair($pairId: ID!) {
1291
1311
  id pair { id pair source thumb } status createdAt
1292
1312
  config {
1293
1313
  gridConfig { cellSizeTime cellSizeValue candleSize baseline baselineTime }
1294
- constraints { minBetTime maxBetTime slippage priceMinRange priceMaxRange coefMults maxCoef }
1314
+ constraints { minBetTime maxBetTime slippage priceMinRange priceMaxRange coefMults maxCoef coefMultsMatrix { timeBandsSec rows } }
1295
1315
  acceptableBids minBidAmount maxBidAmount
1296
1316
  bidCancelRefundRate bidCancelMinSeconds
1297
1317
  }
@@ -1849,6 +1869,23 @@ function mapWireBidResult(raw, topicPairId) {
1849
1869
  userId: p.user_id
1850
1870
  };
1851
1871
  if (typeof p.balance === "string") data.balance = p.balance;
1872
+ const candle = p.cellCandle;
1873
+ if (candle && typeof candle.o === "string" && typeof candle.h === "string" && typeof candle.l === "string" && typeof candle.c === "string") {
1874
+ data.cellCandle = {
1875
+ o: candle.o,
1876
+ h: candle.h,
1877
+ l: candle.l,
1878
+ c: candle.c,
1879
+ final: candle.final !== "false"
1880
+ };
1881
+ if (typeof candle.touchSec === "string" && typeof candle.touchPrice === "string") {
1882
+ const touchSec = Number.parseInt(candle.touchSec, 10);
1883
+ if (Number.isFinite(touchSec)) {
1884
+ data.cellCandle.touchSec = touchSec;
1885
+ data.cellCandle.touchPrice = candle.touchPrice;
1886
+ }
1887
+ }
1888
+ }
1852
1889
  if (topicPairId !== void 0) data.pairId = topicPairId;
1853
1890
  return { event: "bidWon", data };
1854
1891
  }
@@ -1857,6 +1894,32 @@ function mapWireBidResult(raw, topicPairId) {
1857
1894
  if (topicPairId !== void 0) data.pairId = topicPairId;
1858
1895
  return { event: "bidLost", data };
1859
1896
  }
1897
+ case "cell_candle_final": {
1898
+ const candle = p.cellCandle;
1899
+ if (!candle || typeof candle.o !== "string" || typeof candle.h !== "string" || typeof candle.l !== "string" || typeof candle.c !== "string") {
1900
+ return null;
1901
+ }
1902
+ const data = {
1903
+ bidId: p.bidId,
1904
+ userId: p.user_id,
1905
+ cellCandle: {
1906
+ o: candle.o,
1907
+ h: candle.h,
1908
+ l: candle.l,
1909
+ c: candle.c,
1910
+ final: candle.final !== "false"
1911
+ }
1912
+ };
1913
+ if (typeof candle.touchSec === "string" && typeof candle.touchPrice === "string") {
1914
+ const touchSec = Number.parseInt(candle.touchSec, 10);
1915
+ if (Number.isFinite(touchSec)) {
1916
+ data.cellCandle.touchSec = touchSec;
1917
+ data.cellCandle.touchPrice = candle.touchPrice;
1918
+ }
1919
+ }
1920
+ if (topicPairId !== void 0) data.pairId = topicPairId;
1921
+ return { event: "cellCandleFinal", data };
1922
+ }
1860
1923
  default:
1861
1924
  console.warn(`Unknown bid_result type "${String(p.type)}"`);
1862
1925
  return null;
@@ -1974,6 +2037,7 @@ var RealtimeModule = class extends import_eventemitter35.default {
1974
2037
  else if (mapped.event === "bidAccepted") channel.emit("bidAccepted", mapped.data);
1975
2038
  else if (mapped.event === "bidWon") channel.emit("bidWon", mapped.data);
1976
2039
  else if (mapped.event === "bidLost") channel.emit("bidLost", mapped.data);
2040
+ else if (mapped.event === "cellCandleFinal") channel.emit("cellCandleFinal", mapped.data);
1977
2041
  else if (mapped.event === "balanceUpdate") channel.emit("balanceUpdate", mapped.data);
1978
2042
  else if (mapped.event === "configUpdate") channel.emit("configUpdate", mapped.data);
1979
2043
  else if (mapped.event === "idealConfigUpdate")
@@ -2106,6 +2170,7 @@ var RealtimeModule = class extends import_eventemitter35.default {
2106
2170
  if (!mapped) return;
2107
2171
  if (mapped.event === "bidWon") channel.emit("bidWon", mapped.data);
2108
2172
  else if (mapped.event === "bidLost") channel.emit("bidLost", mapped.data);
2173
+ else if (mapped.event === "cellCandleFinal") channel.emit("cellCandleFinal", mapped.data);
2109
2174
  };
2110
2175
  const onError = (err) => {
2111
2176
  channel.emit("error", err);
@@ -3786,6 +3851,40 @@ function calculateProbWin_v2(time1, time2, _price1, price2, creationTime, curren
3786
3851
  const prob = 1 - normalCDF(z);
3787
3852
  return Math.max(0, Math.min(1, prob));
3788
3853
  }
3854
+ function coefMultBandIndex(timeBandsSec, timeToStartSec) {
3855
+ for (let i = 0; i < timeBandsSec.length; i++) {
3856
+ if (timeToStartSec <= timeBandsSec[i]) return i;
3857
+ }
3858
+ return timeBandsSec.length;
3859
+ }
3860
+ function coefMultiplierAt(timeBandsSec, row, timeToStartSec) {
3861
+ if (row.length === 0) return 1;
3862
+ if (row.length === 1 || timeBandsSec.length === 0) return row[0];
3863
+ let valueCount = row.length;
3864
+ if (valueCount > timeBandsSec.length + 1) valueCount = timeBandsSec.length + 1;
3865
+ const edges = timeBandsSec.slice(0, valueCount - 1);
3866
+ const anchors = new Array(valueCount);
3867
+ anchors[0] = edges[0] / 2;
3868
+ for (let i = 1; i < valueCount - 1; i++) {
3869
+ anchors[i] = (edges[i - 1] + edges[i]) / 2;
3870
+ }
3871
+ const last = valueCount - 1;
3872
+ if (edges.length >= 2) {
3873
+ anchors[last] = edges[last - 1] + (edges[last - 1] - edges[last - 2]) / 2;
3874
+ } else {
3875
+ anchors[last] = 1.5 * edges[0];
3876
+ }
3877
+ const t = timeToStartSec;
3878
+ if (t <= anchors[0]) return row[0];
3879
+ if (t >= anchors[last]) return row[last];
3880
+ for (let i = 1; i <= last; i++) {
3881
+ if (t <= anchors[i]) {
3882
+ const frac = (t - anchors[i - 1]) / (anchors[i] - anchors[i - 1]);
3883
+ return row[i - 1] + (row[i] - row[i - 1]) * frac;
3884
+ }
3885
+ }
3886
+ return row[last];
3887
+ }
3789
3888
  function simpsonForHit(f, a, b, eps = 3e-5, maxDepth = 14) {
3790
3889
  function simpsonRule(a2, b2) {
3791
3890
  const c = (a2 + b2) / 2;
@@ -3855,7 +3954,7 @@ function calculateCoefficientWrapper(params) {
3855
3954
  candleClose,
3856
3955
  volatility,
3857
3956
  coefMults,
3858
- cellSizeTime,
3957
+ coefMultsMatrix,
3859
3958
  minCoef
3860
3959
  } = params;
3861
3960
  const currentPrice = candleClose;
@@ -3872,22 +3971,46 @@ function calculateCoefficientWrapper(params) {
3872
3971
  if (probability <= 0) return Number.POSITIVE_INFINITY;
3873
3972
  const rawCoef = 1 / probability;
3874
3973
  const rawIndex = rawCoef < 1 ? 0 : Math.floor(Math.log2(rawCoef));
3875
- const coefMultIndex = Math.min(rawIndex, coefMults.length - 1);
3876
- const multiplier = coefMults[coefMultIndex] || 1;
3877
- const timeRatio = cellSizeTime;
3878
- const adjustedProb = probability * (timeRatio / 5);
3879
- if (adjustedProb <= 0) return Number.POSITIVE_INFINITY;
3974
+ let multiplier = 1;
3975
+ if (coefMultsMatrix && coefMultsMatrix.rows.length > 0) {
3976
+ const bucketIndex = Math.min(rawIndex, coefMultsMatrix.rows.length - 1);
3977
+ const row = coefMultsMatrix.rows[bucketIndex];
3978
+ if (row && row.length > 0) {
3979
+ multiplier = coefMultiplierAt(coefMultsMatrix.timeBandsSec, row, time1 - candleTime);
3980
+ }
3981
+ } else {
3982
+ const coefMultIndex = Math.min(rawIndex, coefMults.length - 1);
3983
+ multiplier = coefMults[coefMultIndex] || 1;
3984
+ }
3880
3985
  const floor = typeof minCoef === "number" && minCoef > 0 ? minCoef : 1;
3881
- const finalCoef = Math.max(floor, multiplier / adjustedProb);
3986
+ const finalCoef = Math.max(floor, multiplier / probability);
3882
3987
  return roundCoefToSignificantDigits(finalCoef);
3883
3988
  }
3884
3989
  function roundCoefToSignificantDigits(value) {
3885
3990
  if (!Number.isFinite(value) || value <= 0) return value;
3886
- const magnitude = Math.floor(Math.log10(value));
3887
- const leadingDigit = Math.floor(value / 10 ** magnitude);
3888
- const sigDigits = leadingDigit === 1 ? 3 : 2;
3889
- const factor = 10 ** (sigDigits - magnitude - 1);
3890
- return Math.round(value * factor) / factor;
3991
+ let exponent = 0;
3992
+ let scaled = value;
3993
+ while (scaled >= 10) {
3994
+ scaled /= 10;
3995
+ exponent++;
3996
+ }
3997
+ while (scaled < 1) {
3998
+ scaled *= 10;
3999
+ exponent--;
4000
+ }
4001
+ const digits = Math.floor(scaled) === 1 ? 3 : 2;
4002
+ const factorExponent = digits - exponent - 1;
4003
+ if (factorExponent >= 0) {
4004
+ const factor = exactPowerOfTen(factorExponent);
4005
+ return Math.round(value * factor) / factor;
4006
+ }
4007
+ const divisor = exactPowerOfTen(-factorExponent);
4008
+ return Math.round(value / divisor) * divisor;
4009
+ }
4010
+ function exactPowerOfTen(n) {
4011
+ let power = 1;
4012
+ for (let i = 0; i < n; i++) power *= 10;
4013
+ return power;
3891
4014
  }
3892
4015
  function computeBaseline(candleClose, candleTimeSec, cellSizeValue, cellSizeTime, candleSize) {
3893
4016
  const baseline = Math.floor(candleClose / cellSizeValue) * cellSizeValue;
@@ -3927,6 +4050,8 @@ function computeBaseline(candleClose, candleTimeSec, cellSizeValue, cellSizeTime
3927
4050
  calculateProbHit,
3928
4051
  calculateProbWin,
3929
4052
  calculateProbWin_v2,
4053
+ coefMultBandIndex,
4054
+ coefMultiplierAt,
3930
4055
  computeBaseline,
3931
4056
  errorFunction,
3932
4057
  isCancelled,
package/dist/index.d.mts CHANGED
@@ -326,6 +326,39 @@ interface BidMeta {
326
326
  /** Display pair, e.g. "ETH/USD". */
327
327
  pairName: string;
328
328
  }
329
+ /**
330
+ * The OHLC of a bid's cell window — what price did inside [time1, time2] on the bid's
331
+ * own pair, reduced from every stored tick covering it.
332
+ *
333
+ * Values are Decimal strings, not numbers: the archive stores 8 decimal places and
334
+ * parsing to a JS number here would round them before the UI ever sees them.
335
+ */
336
+ interface BidCellCandle {
337
+ o: string;
338
+ h: string;
339
+ l: string;
340
+ c: string;
341
+ /**
342
+ * Whether the candle had CLOSED when it settled the bid.
343
+ *
344
+ * `false` = the values can still move: a 1m/5m win settles the instant the
345
+ * band is touched (the verdict is permanent) but the candle's O/C and extremes
346
+ * keep moving until it closes — delivered live over MQTT, and by `myBids` when
347
+ * the touching candle has not closed at read time. The UI shows such a value
348
+ * in the same not-yet-settled language the pending row uses. Absent (a server
349
+ * that predates the flag) means settled.
350
+ */
351
+ final?: boolean;
352
+ /**
353
+ * The WINNING TICK (TH-146 "success ticker"): the unix SECOND at which price
354
+ * entered the bid's band, and the price inside the band it entered at — what
355
+ * the popover shows as "reached <price> at <time>". Both present or both
356
+ * absent: a bid that did not win has no single moment that won it, and a
357
+ * server predating the field sends neither.
358
+ */
359
+ touchSec?: number;
360
+ touchPrice?: string;
361
+ }
329
362
  interface Bid {
330
363
  id: string;
331
364
  userId: string;
@@ -363,6 +396,16 @@ interface Bid {
363
396
  * display pair instead of mapping `pairId` yourself.
364
397
  */
365
398
  meta?: BidMeta | null;
399
+ /**
400
+ * What price did inside this bid's cell window, served with the bid itself so a
401
+ * consumer never has to fetch it separately (openspec ux-260819-bid-detail-popover).
402
+ *
403
+ * Null in three cases a consumer should treat alike: the bid is still pending, its
404
+ * ticks are no longer stored, or the caller's query did not select the field. A
405
+ * cancelled bid DOES carry one — its window still elapsed — and the server attaches
406
+ * no verdict about what the outcome would have been.
407
+ */
408
+ cellCandle?: BidCellCandle | null;
366
409
  }
367
410
  interface CancelBidResult {
368
411
  bid: Bid;
@@ -823,6 +866,11 @@ interface Constraints {
823
866
  priceMaxRange: number;
824
867
  coefMults: number[];
825
868
  maxCoef: number;
869
+ coefMultsMatrix?: CoefMultsMatrix$1 | null;
870
+ }
871
+ interface CoefMultsMatrix$1 {
872
+ timeBandsSec: number[];
873
+ rows: number[][];
826
874
  }
827
875
  interface PairInfo {
828
876
  /**
@@ -1166,6 +1214,37 @@ interface MqttBidWonEvent {
1166
1214
  payout: string;
1167
1215
  userId: string;
1168
1216
  balance?: string;
1217
+ /**
1218
+ * OHLC of the candle whose band touch declared this win — the bid's
1219
+ * `cellCandle`, shipped with the result so the client never has to refetch the
1220
+ * bid list to learn it (openspec ux-260819, revised D2e).
1221
+ *
1222
+ * Sent on live wins and on D10 recovery alike (recovery hands over the stored
1223
+ * candle whose touch it found). Absent only on any grid-api older than that
1224
+ * change.
1225
+ */
1226
+ cellCandle?: {
1227
+ o: string;
1228
+ h: string;
1229
+ l: string;
1230
+ c: string;
1231
+ /**
1232
+ * Whether that candle had CLOSED when it settled the bid. A 1m/5m win settles
1233
+ * the instant the band is touched — safe, because a touch is permanent — but
1234
+ * the candle's O/C and extremes can still move until it closes. `false` means
1235
+ * the client must present the value as not-yet-settled rather than final.
1236
+ */
1237
+ final: boolean;
1238
+ /**
1239
+ * The WINNING TICK (TH-146 "success ticker"): the unix SECOND at which price
1240
+ * entered the bid's band, and the price inside the band it entered at. The
1241
+ * engine's predicate is a range overlap, so this is neither the candle's
1242
+ * close nor a raw extreme. Both present or both absent — a server that
1243
+ * predates the field sends neither.
1244
+ */
1245
+ touchSec?: number;
1246
+ touchPrice?: string;
1247
+ };
1169
1248
  /**
1170
1249
  * Pair the won bid belongs to, e.g. "grid-ETH-USD". Derived from the result
1171
1250
  * topic (`game/{agencyId}:{pairId}/user/.../bid_result`) since the won payload
@@ -1175,6 +1254,38 @@ interface MqttBidWonEvent {
1175
1254
  */
1176
1255
  pairId?: string;
1177
1256
  }
1257
+ /**
1258
+ * Follow-up patch grid-api publishes when the window of a building-candle win
1259
+ * closes (wire type `cell_candle_final`): the CLOSED o/h/l/c of the candle the
1260
+ * bid settled on, replacing the in-progress values the `bidWon` event carried
1261
+ * with `cellCandle.final: false` (openspec ux-260819, design D2g).
1262
+ *
1263
+ * A patch, not a result: consumers must only update the bid's stored candle —
1264
+ * never replay win side effects (sound, payout, balance).
1265
+ */
1266
+ interface MqttCellCandleFinalEvent {
1267
+ bidId: string;
1268
+ userId: string;
1269
+ cellCandle: {
1270
+ o: string;
1271
+ h: string;
1272
+ l: string;
1273
+ c: string;
1274
+ /** Always true — the whole point of the event is that the candle closed. */
1275
+ final: boolean;
1276
+ /**
1277
+ * The WINNING TICK (TH-146 "success ticker"): the unix SECOND at which price
1278
+ * entered the bid's band, and the price inside the band it entered at. The
1279
+ * engine's predicate is a range overlap, so this is neither the candle's
1280
+ * close nor a raw extreme. Both present or both absent — a server that
1281
+ * predates the field sends neither.
1282
+ */
1283
+ touchSec?: number;
1284
+ touchPrice?: string;
1285
+ };
1286
+ /** Pair the bid belongs to (see {@link MqttBidWonEvent.pairId}). */
1287
+ pairId?: string;
1288
+ }
1178
1289
  interface MqttBidLostEvent {
1179
1290
  bidId: string;
1180
1291
  /** Pair the lost bid belongs to (see {@link MqttBidWonEvent.pairId}). */
@@ -1268,6 +1379,7 @@ interface GameChannelEvents {
1268
1379
  bidAccepted: [MqttBidAcceptedEvent];
1269
1380
  bidWon: [MqttBidWonEvent];
1270
1381
  bidLost: [MqttBidLostEvent];
1382
+ cellCandleFinal: [MqttCellCandleFinalEvent];
1271
1383
  bidCancelled: [MqttBidCancelledEvent];
1272
1384
  balanceUpdate: [MqttBalanceEvent];
1273
1385
  configUpdate: [MqttConfigEvent];
@@ -1298,6 +1410,7 @@ declare class MigrationChannel extends EventEmitter<MigrationChannelEvents> {
1298
1410
  interface UserBidsChannelEvents {
1299
1411
  bidWon: [MqttBidWonEvent];
1300
1412
  bidLost: [MqttBidLostEvent];
1413
+ cellCandleFinal: [MqttCellCandleFinalEvent];
1301
1414
  error: [Error];
1302
1415
  }
1303
1416
  /**
@@ -1828,10 +1941,15 @@ interface CoefficientInput {
1828
1941
  candleClose: number;
1829
1942
  volatility: number;
1830
1943
  coefMults: number[];
1831
- cellSizeTime: number;
1832
- candleSize: number;
1944
+ coefMultsMatrix?: CoefMultsMatrix | null;
1833
1945
  minCoef?: number;
1834
1946
  }
1947
+ interface CoefMultsMatrix {
1948
+ timeBandsSec: number[];
1949
+ rows: number[][];
1950
+ }
1951
+ declare function coefMultBandIndex(timeBandsSec: number[], timeToStartSec: number): number;
1952
+ declare function coefMultiplierAt(timeBandsSec: number[], row: number[], timeToStartSec: number): number;
1835
1953
  /**
1836
1954
  * First-passage hitting probability: chance the price visits [price1, price2]
1837
1955
  * at any point during [time1, time2], given GBM dynamics.
@@ -1848,4 +1966,4 @@ declare function computeBaseline(candleClose: number, candleTimeSec: number, cel
1848
1966
  baselineTime: number;
1849
1967
  };
1850
1968
 
1851
- 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, DEFAULT_PROBE_TTL_SECONDS, DEFAULT_REGION, type ErrorMessageCatalog, type ErrorMessageMeta, type ErrorMessagesResponse, GameChannel, type GameConfig, type GridConfig, KNOWN_REGIONS, type KnownRegion, type LeaderboardEntry, LeaderboardModule, type LeaderboardPeriod, type LeaderboardSortBy, LocaleModule, type LocaleRefreshResult, type LocaleResponse, type LocaleTranslations, type LoginResult, type Me, MigrationChannel, type MigrationChannelEvents, type MigrationCompletedEvent, 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 ProbeNearestRegionOptions, REGION_PROBE_CACHE_KEY, type RankBoard, type RankEntry, type RankPeriod, type RankSort, RealtimeModule, type RefreshTokenResult, type RegionDomainMap, 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, isKnownRegion, isLoss, isPending, isTerminal, isWin, normalCDF, normalPDF, normaliseLang, pairIdFromBidResultTopic, probeNearestRegion, resolveRegionBaseUrl, roundCoefToSignificantDigits };
1969
+ export { type AgencyPairFilter, AgencyPairModule, type AgencyPairStats, type AgencyPairStatus, AuthModule, type BackendHealth, type Bid, type BidCellCandle, BidModule, type BidStatus, CANDLE_EVENT, type CancelBidResult, type Candle, type CandleEventType, type CoefMultsMatrix, type CoefficientInput, type Constraints, type Currency, DEFAULT_CHART_HISTORY_LIMIT, DEFAULT_PROBE_TTL_SECONDS, DEFAULT_REGION, type ErrorMessageCatalog, type ErrorMessageMeta, type ErrorMessagesResponse, GameChannel, type GameConfig, type GridConfig, KNOWN_REGIONS, type KnownRegion, type LeaderboardEntry, LeaderboardModule, type LeaderboardPeriod, type LeaderboardSortBy, LocaleModule, type LocaleRefreshResult, type LocaleResponse, type LocaleTranslations, type LoginResult, type Me, MigrationChannel, type MigrationChannelEvents, type MigrationCompletedEvent, type MqttAcceptedBid, type MqttAgencyPairStatsEvent, type MqttAuthConfig, type MqttBalanceEvent, type MqttBidAcceptedEvent, type MqttBidLostEvent, type MqttBidWonEvent, type MqttCandleEvent, type MqttCellCandleFinalEvent, type MqttConfigEvent, type MqttIdealConfigEvent, type MqttWalletBalanceEvent, type MyRank, type NetworkLevel, type NetworkQuality, NetworkQualityMonitor, type Pair, type PairInfo, PairModule, type PlaceBidInput, type ProbeNearestRegionOptions, REGION_PROBE_CACHE_KEY, type RankBoard, type RankEntry, type RankPeriod, type RankSort, RealtimeModule, type RefreshTokenResult, type RegionDomainMap, 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, coefMultBandIndex, coefMultiplierAt, computeBaseline, errorFunction, isCancelled, isKnownRegion, isLoss, isPending, isTerminal, isWin, normalCDF, normalPDF, normaliseLang, pairIdFromBidResultTopic, probeNearestRegion, resolveRegionBaseUrl, roundCoefToSignificantDigits };
package/dist/index.d.ts CHANGED
@@ -326,6 +326,39 @@ interface BidMeta {
326
326
  /** Display pair, e.g. "ETH/USD". */
327
327
  pairName: string;
328
328
  }
329
+ /**
330
+ * The OHLC of a bid's cell window — what price did inside [time1, time2] on the bid's
331
+ * own pair, reduced from every stored tick covering it.
332
+ *
333
+ * Values are Decimal strings, not numbers: the archive stores 8 decimal places and
334
+ * parsing to a JS number here would round them before the UI ever sees them.
335
+ */
336
+ interface BidCellCandle {
337
+ o: string;
338
+ h: string;
339
+ l: string;
340
+ c: string;
341
+ /**
342
+ * Whether the candle had CLOSED when it settled the bid.
343
+ *
344
+ * `false` = the values can still move: a 1m/5m win settles the instant the
345
+ * band is touched (the verdict is permanent) but the candle's O/C and extremes
346
+ * keep moving until it closes — delivered live over MQTT, and by `myBids` when
347
+ * the touching candle has not closed at read time. The UI shows such a value
348
+ * in the same not-yet-settled language the pending row uses. Absent (a server
349
+ * that predates the flag) means settled.
350
+ */
351
+ final?: boolean;
352
+ /**
353
+ * The WINNING TICK (TH-146 "success ticker"): the unix SECOND at which price
354
+ * entered the bid's band, and the price inside the band it entered at — what
355
+ * the popover shows as "reached <price> at <time>". Both present or both
356
+ * absent: a bid that did not win has no single moment that won it, and a
357
+ * server predating the field sends neither.
358
+ */
359
+ touchSec?: number;
360
+ touchPrice?: string;
361
+ }
329
362
  interface Bid {
330
363
  id: string;
331
364
  userId: string;
@@ -363,6 +396,16 @@ interface Bid {
363
396
  * display pair instead of mapping `pairId` yourself.
364
397
  */
365
398
  meta?: BidMeta | null;
399
+ /**
400
+ * What price did inside this bid's cell window, served with the bid itself so a
401
+ * consumer never has to fetch it separately (openspec ux-260819-bid-detail-popover).
402
+ *
403
+ * Null in three cases a consumer should treat alike: the bid is still pending, its
404
+ * ticks are no longer stored, or the caller's query did not select the field. A
405
+ * cancelled bid DOES carry one — its window still elapsed — and the server attaches
406
+ * no verdict about what the outcome would have been.
407
+ */
408
+ cellCandle?: BidCellCandle | null;
366
409
  }
367
410
  interface CancelBidResult {
368
411
  bid: Bid;
@@ -823,6 +866,11 @@ interface Constraints {
823
866
  priceMaxRange: number;
824
867
  coefMults: number[];
825
868
  maxCoef: number;
869
+ coefMultsMatrix?: CoefMultsMatrix$1 | null;
870
+ }
871
+ interface CoefMultsMatrix$1 {
872
+ timeBandsSec: number[];
873
+ rows: number[][];
826
874
  }
827
875
  interface PairInfo {
828
876
  /**
@@ -1166,6 +1214,37 @@ interface MqttBidWonEvent {
1166
1214
  payout: string;
1167
1215
  userId: string;
1168
1216
  balance?: string;
1217
+ /**
1218
+ * OHLC of the candle whose band touch declared this win — the bid's
1219
+ * `cellCandle`, shipped with the result so the client never has to refetch the
1220
+ * bid list to learn it (openspec ux-260819, revised D2e).
1221
+ *
1222
+ * Sent on live wins and on D10 recovery alike (recovery hands over the stored
1223
+ * candle whose touch it found). Absent only on any grid-api older than that
1224
+ * change.
1225
+ */
1226
+ cellCandle?: {
1227
+ o: string;
1228
+ h: string;
1229
+ l: string;
1230
+ c: string;
1231
+ /**
1232
+ * Whether that candle had CLOSED when it settled the bid. A 1m/5m win settles
1233
+ * the instant the band is touched — safe, because a touch is permanent — but
1234
+ * the candle's O/C and extremes can still move until it closes. `false` means
1235
+ * the client must present the value as not-yet-settled rather than final.
1236
+ */
1237
+ final: boolean;
1238
+ /**
1239
+ * The WINNING TICK (TH-146 "success ticker"): the unix SECOND at which price
1240
+ * entered the bid's band, and the price inside the band it entered at. The
1241
+ * engine's predicate is a range overlap, so this is neither the candle's
1242
+ * close nor a raw extreme. Both present or both absent — a server that
1243
+ * predates the field sends neither.
1244
+ */
1245
+ touchSec?: number;
1246
+ touchPrice?: string;
1247
+ };
1169
1248
  /**
1170
1249
  * Pair the won bid belongs to, e.g. "grid-ETH-USD". Derived from the result
1171
1250
  * topic (`game/{agencyId}:{pairId}/user/.../bid_result`) since the won payload
@@ -1175,6 +1254,38 @@ interface MqttBidWonEvent {
1175
1254
  */
1176
1255
  pairId?: string;
1177
1256
  }
1257
+ /**
1258
+ * Follow-up patch grid-api publishes when the window of a building-candle win
1259
+ * closes (wire type `cell_candle_final`): the CLOSED o/h/l/c of the candle the
1260
+ * bid settled on, replacing the in-progress values the `bidWon` event carried
1261
+ * with `cellCandle.final: false` (openspec ux-260819, design D2g).
1262
+ *
1263
+ * A patch, not a result: consumers must only update the bid's stored candle —
1264
+ * never replay win side effects (sound, payout, balance).
1265
+ */
1266
+ interface MqttCellCandleFinalEvent {
1267
+ bidId: string;
1268
+ userId: string;
1269
+ cellCandle: {
1270
+ o: string;
1271
+ h: string;
1272
+ l: string;
1273
+ c: string;
1274
+ /** Always true — the whole point of the event is that the candle closed. */
1275
+ final: boolean;
1276
+ /**
1277
+ * The WINNING TICK (TH-146 "success ticker"): the unix SECOND at which price
1278
+ * entered the bid's band, and the price inside the band it entered at. The
1279
+ * engine's predicate is a range overlap, so this is neither the candle's
1280
+ * close nor a raw extreme. Both present or both absent — a server that
1281
+ * predates the field sends neither.
1282
+ */
1283
+ touchSec?: number;
1284
+ touchPrice?: string;
1285
+ };
1286
+ /** Pair the bid belongs to (see {@link MqttBidWonEvent.pairId}). */
1287
+ pairId?: string;
1288
+ }
1178
1289
  interface MqttBidLostEvent {
1179
1290
  bidId: string;
1180
1291
  /** Pair the lost bid belongs to (see {@link MqttBidWonEvent.pairId}). */
@@ -1268,6 +1379,7 @@ interface GameChannelEvents {
1268
1379
  bidAccepted: [MqttBidAcceptedEvent];
1269
1380
  bidWon: [MqttBidWonEvent];
1270
1381
  bidLost: [MqttBidLostEvent];
1382
+ cellCandleFinal: [MqttCellCandleFinalEvent];
1271
1383
  bidCancelled: [MqttBidCancelledEvent];
1272
1384
  balanceUpdate: [MqttBalanceEvent];
1273
1385
  configUpdate: [MqttConfigEvent];
@@ -1298,6 +1410,7 @@ declare class MigrationChannel extends EventEmitter<MigrationChannelEvents> {
1298
1410
  interface UserBidsChannelEvents {
1299
1411
  bidWon: [MqttBidWonEvent];
1300
1412
  bidLost: [MqttBidLostEvent];
1413
+ cellCandleFinal: [MqttCellCandleFinalEvent];
1301
1414
  error: [Error];
1302
1415
  }
1303
1416
  /**
@@ -1828,10 +1941,15 @@ interface CoefficientInput {
1828
1941
  candleClose: number;
1829
1942
  volatility: number;
1830
1943
  coefMults: number[];
1831
- cellSizeTime: number;
1832
- candleSize: number;
1944
+ coefMultsMatrix?: CoefMultsMatrix | null;
1833
1945
  minCoef?: number;
1834
1946
  }
1947
+ interface CoefMultsMatrix {
1948
+ timeBandsSec: number[];
1949
+ rows: number[][];
1950
+ }
1951
+ declare function coefMultBandIndex(timeBandsSec: number[], timeToStartSec: number): number;
1952
+ declare function coefMultiplierAt(timeBandsSec: number[], row: number[], timeToStartSec: number): number;
1835
1953
  /**
1836
1954
  * First-passage hitting probability: chance the price visits [price1, price2]
1837
1955
  * at any point during [time1, time2], given GBM dynamics.
@@ -1848,4 +1966,4 @@ declare function computeBaseline(candleClose: number, candleTimeSec: number, cel
1848
1966
  baselineTime: number;
1849
1967
  };
1850
1968
 
1851
- 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, DEFAULT_PROBE_TTL_SECONDS, DEFAULT_REGION, type ErrorMessageCatalog, type ErrorMessageMeta, type ErrorMessagesResponse, GameChannel, type GameConfig, type GridConfig, KNOWN_REGIONS, type KnownRegion, type LeaderboardEntry, LeaderboardModule, type LeaderboardPeriod, type LeaderboardSortBy, LocaleModule, type LocaleRefreshResult, type LocaleResponse, type LocaleTranslations, type LoginResult, type Me, MigrationChannel, type MigrationChannelEvents, type MigrationCompletedEvent, 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 ProbeNearestRegionOptions, REGION_PROBE_CACHE_KEY, type RankBoard, type RankEntry, type RankPeriod, type RankSort, RealtimeModule, type RefreshTokenResult, type RegionDomainMap, 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, isKnownRegion, isLoss, isPending, isTerminal, isWin, normalCDF, normalPDF, normaliseLang, pairIdFromBidResultTopic, probeNearestRegion, resolveRegionBaseUrl, roundCoefToSignificantDigits };
1969
+ export { type AgencyPairFilter, AgencyPairModule, type AgencyPairStats, type AgencyPairStatus, AuthModule, type BackendHealth, type Bid, type BidCellCandle, BidModule, type BidStatus, CANDLE_EVENT, type CancelBidResult, type Candle, type CandleEventType, type CoefMultsMatrix, type CoefficientInput, type Constraints, type Currency, DEFAULT_CHART_HISTORY_LIMIT, DEFAULT_PROBE_TTL_SECONDS, DEFAULT_REGION, type ErrorMessageCatalog, type ErrorMessageMeta, type ErrorMessagesResponse, GameChannel, type GameConfig, type GridConfig, KNOWN_REGIONS, type KnownRegion, type LeaderboardEntry, LeaderboardModule, type LeaderboardPeriod, type LeaderboardSortBy, LocaleModule, type LocaleRefreshResult, type LocaleResponse, type LocaleTranslations, type LoginResult, type Me, MigrationChannel, type MigrationChannelEvents, type MigrationCompletedEvent, type MqttAcceptedBid, type MqttAgencyPairStatsEvent, type MqttAuthConfig, type MqttBalanceEvent, type MqttBidAcceptedEvent, type MqttBidLostEvent, type MqttBidWonEvent, type MqttCandleEvent, type MqttCellCandleFinalEvent, type MqttConfigEvent, type MqttIdealConfigEvent, type MqttWalletBalanceEvent, type MyRank, type NetworkLevel, type NetworkQuality, NetworkQualityMonitor, type Pair, type PairInfo, PairModule, type PlaceBidInput, type ProbeNearestRegionOptions, REGION_PROBE_CACHE_KEY, type RankBoard, type RankEntry, type RankPeriod, type RankSort, RealtimeModule, type RefreshTokenResult, type RegionDomainMap, 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, coefMultBandIndex, coefMultiplierAt, computeBaseline, errorFunction, isCancelled, isKnownRegion, isLoss, isPending, isTerminal, isWin, normalCDF, normalPDF, normaliseLang, pairIdFromBidResultTopic, probeNearestRegion, resolveRegionBaseUrl, roundCoefToSignificantDigits };
package/dist/index.js CHANGED
@@ -581,7 +581,22 @@ function normaliseBid(node) {
581
581
  refundAmount: node.refund_amount ?? null,
582
582
  slippage: node.slippage,
583
583
  createdAt: node.created_at || null,
584
- meta: parseMeta(node.meta)
584
+ meta: parseMeta(node.meta),
585
+ // Absent and explicit null both mean "no candle" — an older server, an unselected
586
+ // field, a pending bid and purged ticks must all read the same to a consumer.
587
+ // `final` null (nullable Boolean) collapses to absent = settled, so only a real
588
+ // `false` puts the row in the in-progress presentation.
589
+ cellCandle: node.cellCandle ? {
590
+ o: node.cellCandle.o,
591
+ h: node.cellCandle.h,
592
+ l: node.cellCandle.l,
593
+ c: node.cellCandle.c,
594
+ ...typeof node.cellCandle.final === "boolean" ? { final: node.cellCandle.final } : {},
595
+ // Both legs or neither — see BidCellCandle.touchSec. Nulls (a non-win,
596
+ // or an older server) collapse to absent so a consumer only has to
597
+ // check one of them.
598
+ ...typeof node.cellCandle.touchSec === "number" && typeof node.cellCandle.touchPrice === "string" ? { touchSec: node.cellCandle.touchSec, touchPrice: node.cellCandle.touchPrice } : {}
599
+ } : null
585
600
  };
586
601
  }
587
602
  function normaliseBids(list) {
@@ -602,6 +617,7 @@ var PLACE_BID_MUTATION = `mutation PlaceBid($input: PlaceBidInput!) {
602
617
  var MY_BIDS_QUERY = `query MyBids($statuses: [BidStatus!], $limit: Int, $offset: Int, $pairId: ID) {
603
618
  myBids(statuses: $statuses, limit: $limit, offset: $offset, pairId: $pairId) {
604
619
  id user_id pair_id currency amount coefficient time1 time2 price1 price2 status payout refund_rate refund_amount slippage created_at meta
620
+ cellCandle { o h l c final touchSec touchPrice }
605
621
  }
606
622
  }`;
607
623
  var CANCEL_BID_MUTATION = `mutation CancelBid($input: CancelBidInput!) {
@@ -1158,7 +1174,9 @@ function normaliseConstraints(node) {
1158
1174
  priceMinRange: node.priceMinRange,
1159
1175
  priceMaxRange: node.priceMaxRange,
1160
1176
  coefMults: node.coefMults,
1161
- maxCoef: node.maxCoef
1177
+ maxCoef: node.maxCoef,
1178
+ // bid-260819: pass through untouched — pricing consumes it as-is.
1179
+ coefMultsMatrix: node.coefMultsMatrix ?? null
1162
1180
  };
1163
1181
  }
1164
1182
  function normaliseCandles(list) {
@@ -1211,7 +1229,7 @@ var PAIR_QUERY = `query AgencyPair($pairId: ID!) {
1211
1229
  id pair { id pair source thumb } status createdAt
1212
1230
  config {
1213
1231
  gridConfig { cellSizeTime cellSizeValue candleSize baseline baselineTime }
1214
- constraints { minBetTime maxBetTime slippage priceMinRange priceMaxRange coefMults maxCoef }
1232
+ constraints { minBetTime maxBetTime slippage priceMinRange priceMaxRange coefMults maxCoef coefMultsMatrix { timeBandsSec rows } }
1215
1233
  acceptableBids minBidAmount maxBidAmount
1216
1234
  bidCancelRefundRate bidCancelMinSeconds
1217
1235
  }
@@ -1769,6 +1787,23 @@ function mapWireBidResult(raw, topicPairId) {
1769
1787
  userId: p.user_id
1770
1788
  };
1771
1789
  if (typeof p.balance === "string") data.balance = p.balance;
1790
+ const candle = p.cellCandle;
1791
+ if (candle && typeof candle.o === "string" && typeof candle.h === "string" && typeof candle.l === "string" && typeof candle.c === "string") {
1792
+ data.cellCandle = {
1793
+ o: candle.o,
1794
+ h: candle.h,
1795
+ l: candle.l,
1796
+ c: candle.c,
1797
+ final: candle.final !== "false"
1798
+ };
1799
+ if (typeof candle.touchSec === "string" && typeof candle.touchPrice === "string") {
1800
+ const touchSec = Number.parseInt(candle.touchSec, 10);
1801
+ if (Number.isFinite(touchSec)) {
1802
+ data.cellCandle.touchSec = touchSec;
1803
+ data.cellCandle.touchPrice = candle.touchPrice;
1804
+ }
1805
+ }
1806
+ }
1772
1807
  if (topicPairId !== void 0) data.pairId = topicPairId;
1773
1808
  return { event: "bidWon", data };
1774
1809
  }
@@ -1777,6 +1812,32 @@ function mapWireBidResult(raw, topicPairId) {
1777
1812
  if (topicPairId !== void 0) data.pairId = topicPairId;
1778
1813
  return { event: "bidLost", data };
1779
1814
  }
1815
+ case "cell_candle_final": {
1816
+ const candle = p.cellCandle;
1817
+ if (!candle || typeof candle.o !== "string" || typeof candle.h !== "string" || typeof candle.l !== "string" || typeof candle.c !== "string") {
1818
+ return null;
1819
+ }
1820
+ const data = {
1821
+ bidId: p.bidId,
1822
+ userId: p.user_id,
1823
+ cellCandle: {
1824
+ o: candle.o,
1825
+ h: candle.h,
1826
+ l: candle.l,
1827
+ c: candle.c,
1828
+ final: candle.final !== "false"
1829
+ }
1830
+ };
1831
+ if (typeof candle.touchSec === "string" && typeof candle.touchPrice === "string") {
1832
+ const touchSec = Number.parseInt(candle.touchSec, 10);
1833
+ if (Number.isFinite(touchSec)) {
1834
+ data.cellCandle.touchSec = touchSec;
1835
+ data.cellCandle.touchPrice = candle.touchPrice;
1836
+ }
1837
+ }
1838
+ if (topicPairId !== void 0) data.pairId = topicPairId;
1839
+ return { event: "cellCandleFinal", data };
1840
+ }
1780
1841
  default:
1781
1842
  console.warn(`Unknown bid_result type "${String(p.type)}"`);
1782
1843
  return null;
@@ -1894,6 +1955,7 @@ var RealtimeModule = class extends EventEmitter5 {
1894
1955
  else if (mapped.event === "bidAccepted") channel.emit("bidAccepted", mapped.data);
1895
1956
  else if (mapped.event === "bidWon") channel.emit("bidWon", mapped.data);
1896
1957
  else if (mapped.event === "bidLost") channel.emit("bidLost", mapped.data);
1958
+ else if (mapped.event === "cellCandleFinal") channel.emit("cellCandleFinal", mapped.data);
1897
1959
  else if (mapped.event === "balanceUpdate") channel.emit("balanceUpdate", mapped.data);
1898
1960
  else if (mapped.event === "configUpdate") channel.emit("configUpdate", mapped.data);
1899
1961
  else if (mapped.event === "idealConfigUpdate")
@@ -2026,6 +2088,7 @@ var RealtimeModule = class extends EventEmitter5 {
2026
2088
  if (!mapped) return;
2027
2089
  if (mapped.event === "bidWon") channel.emit("bidWon", mapped.data);
2028
2090
  else if (mapped.event === "bidLost") channel.emit("bidLost", mapped.data);
2091
+ else if (mapped.event === "cellCandleFinal") channel.emit("cellCandleFinal", mapped.data);
2029
2092
  };
2030
2093
  const onError = (err) => {
2031
2094
  channel.emit("error", err);
@@ -3706,6 +3769,40 @@ function calculateProbWin_v2(time1, time2, _price1, price2, creationTime, curren
3706
3769
  const prob = 1 - normalCDF(z);
3707
3770
  return Math.max(0, Math.min(1, prob));
3708
3771
  }
3772
+ function coefMultBandIndex(timeBandsSec, timeToStartSec) {
3773
+ for (let i = 0; i < timeBandsSec.length; i++) {
3774
+ if (timeToStartSec <= timeBandsSec[i]) return i;
3775
+ }
3776
+ return timeBandsSec.length;
3777
+ }
3778
+ function coefMultiplierAt(timeBandsSec, row, timeToStartSec) {
3779
+ if (row.length === 0) return 1;
3780
+ if (row.length === 1 || timeBandsSec.length === 0) return row[0];
3781
+ let valueCount = row.length;
3782
+ if (valueCount > timeBandsSec.length + 1) valueCount = timeBandsSec.length + 1;
3783
+ const edges = timeBandsSec.slice(0, valueCount - 1);
3784
+ const anchors = new Array(valueCount);
3785
+ anchors[0] = edges[0] / 2;
3786
+ for (let i = 1; i < valueCount - 1; i++) {
3787
+ anchors[i] = (edges[i - 1] + edges[i]) / 2;
3788
+ }
3789
+ const last = valueCount - 1;
3790
+ if (edges.length >= 2) {
3791
+ anchors[last] = edges[last - 1] + (edges[last - 1] - edges[last - 2]) / 2;
3792
+ } else {
3793
+ anchors[last] = 1.5 * edges[0];
3794
+ }
3795
+ const t = timeToStartSec;
3796
+ if (t <= anchors[0]) return row[0];
3797
+ if (t >= anchors[last]) return row[last];
3798
+ for (let i = 1; i <= last; i++) {
3799
+ if (t <= anchors[i]) {
3800
+ const frac = (t - anchors[i - 1]) / (anchors[i] - anchors[i - 1]);
3801
+ return row[i - 1] + (row[i] - row[i - 1]) * frac;
3802
+ }
3803
+ }
3804
+ return row[last];
3805
+ }
3709
3806
  function simpsonForHit(f, a, b, eps = 3e-5, maxDepth = 14) {
3710
3807
  function simpsonRule(a2, b2) {
3711
3808
  const c = (a2 + b2) / 2;
@@ -3775,7 +3872,7 @@ function calculateCoefficientWrapper(params) {
3775
3872
  candleClose,
3776
3873
  volatility,
3777
3874
  coefMults,
3778
- cellSizeTime,
3875
+ coefMultsMatrix,
3779
3876
  minCoef
3780
3877
  } = params;
3781
3878
  const currentPrice = candleClose;
@@ -3792,22 +3889,46 @@ function calculateCoefficientWrapper(params) {
3792
3889
  if (probability <= 0) return Number.POSITIVE_INFINITY;
3793
3890
  const rawCoef = 1 / probability;
3794
3891
  const rawIndex = rawCoef < 1 ? 0 : Math.floor(Math.log2(rawCoef));
3795
- const coefMultIndex = Math.min(rawIndex, coefMults.length - 1);
3796
- const multiplier = coefMults[coefMultIndex] || 1;
3797
- const timeRatio = cellSizeTime;
3798
- const adjustedProb = probability * (timeRatio / 5);
3799
- if (adjustedProb <= 0) return Number.POSITIVE_INFINITY;
3892
+ let multiplier = 1;
3893
+ if (coefMultsMatrix && coefMultsMatrix.rows.length > 0) {
3894
+ const bucketIndex = Math.min(rawIndex, coefMultsMatrix.rows.length - 1);
3895
+ const row = coefMultsMatrix.rows[bucketIndex];
3896
+ if (row && row.length > 0) {
3897
+ multiplier = coefMultiplierAt(coefMultsMatrix.timeBandsSec, row, time1 - candleTime);
3898
+ }
3899
+ } else {
3900
+ const coefMultIndex = Math.min(rawIndex, coefMults.length - 1);
3901
+ multiplier = coefMults[coefMultIndex] || 1;
3902
+ }
3800
3903
  const floor = typeof minCoef === "number" && minCoef > 0 ? minCoef : 1;
3801
- const finalCoef = Math.max(floor, multiplier / adjustedProb);
3904
+ const finalCoef = Math.max(floor, multiplier / probability);
3802
3905
  return roundCoefToSignificantDigits(finalCoef);
3803
3906
  }
3804
3907
  function roundCoefToSignificantDigits(value) {
3805
3908
  if (!Number.isFinite(value) || value <= 0) return value;
3806
- const magnitude = Math.floor(Math.log10(value));
3807
- const leadingDigit = Math.floor(value / 10 ** magnitude);
3808
- const sigDigits = leadingDigit === 1 ? 3 : 2;
3809
- const factor = 10 ** (sigDigits - magnitude - 1);
3810
- return Math.round(value * factor) / factor;
3909
+ let exponent = 0;
3910
+ let scaled = value;
3911
+ while (scaled >= 10) {
3912
+ scaled /= 10;
3913
+ exponent++;
3914
+ }
3915
+ while (scaled < 1) {
3916
+ scaled *= 10;
3917
+ exponent--;
3918
+ }
3919
+ const digits = Math.floor(scaled) === 1 ? 3 : 2;
3920
+ const factorExponent = digits - exponent - 1;
3921
+ if (factorExponent >= 0) {
3922
+ const factor = exactPowerOfTen(factorExponent);
3923
+ return Math.round(value * factor) / factor;
3924
+ }
3925
+ const divisor = exactPowerOfTen(-factorExponent);
3926
+ return Math.round(value / divisor) * divisor;
3927
+ }
3928
+ function exactPowerOfTen(n) {
3929
+ let power = 1;
3930
+ for (let i = 0; i < n; i++) power *= 10;
3931
+ return power;
3811
3932
  }
3812
3933
  function computeBaseline(candleClose, candleTimeSec, cellSizeValue, cellSizeTime, candleSize) {
3813
3934
  const baseline = Math.floor(candleClose / cellSizeValue) * cellSizeValue;
@@ -3846,6 +3967,8 @@ export {
3846
3967
  calculateProbHit,
3847
3968
  calculateProbWin,
3848
3969
  calculateProbWin_v2,
3970
+ coefMultBandIndex,
3971
+ coefMultiplierAt,
3849
3972
  computeBaseline,
3850
3973
  errorFunction,
3851
3974
  isCancelled,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@taphubhq/sdk-core",
3
- "version": "0.26.1",
3
+ "version": "0.28.0",
4
4
  "description": "Core SDK for building on the TabHub platform",
5
5
  "license": "MIT",
6
6
  "main": "./dist/index.cjs",