tradeblocks-mcp 3.8.0 → 3.10.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.
@@ -8209,6 +8209,56 @@ function decomposeGreeks(config) {
8209
8209
  };
8210
8210
  }
8211
8211
 
8212
+ // src/utils/money.ts
8213
+ var SCALE = 1e6;
8214
+ var MONEY_MAX_DOLLARS = Math.floor(Number.MAX_SAFE_INTEGER / SCALE);
8215
+ var MoneyDomainError = class extends Error {
8216
+ };
8217
+ function toMoneyField(amount, field) {
8218
+ if (!Number.isFinite(amount)) {
8219
+ throw new MoneyDomainError(`${field} must be a finite dollar amount`);
8220
+ }
8221
+ const scaled = Math.round(amount * SCALE);
8222
+ if (!Number.isSafeInteger(scaled)) {
8223
+ throw new MoneyDomainError(
8224
+ `${field} is beyond the largest dollar amount this analysis can represent (about ${MONEY_MAX_DOLLARS.toLocaleString("en-US")})`
8225
+ );
8226
+ }
8227
+ return scaled === 0 ? 0 : scaled;
8228
+ }
8229
+ function applyRatioField(amount, ratio, field) {
8230
+ if (!Number.isFinite(ratio)) {
8231
+ throw new MoneyDomainError(`${field} must be a finite number`);
8232
+ }
8233
+ return toMoneyField(fromMoney(amount) * ratio, field);
8234
+ }
8235
+ function fromMoney(value) {
8236
+ return value / SCALE;
8237
+ }
8238
+ function formatMoney(value) {
8239
+ if (value % 1e4 === 0) return (value / SCALE).toFixed(2);
8240
+ return (value / SCALE).toFixed(6).replace(/(\.\d\d[0-9]*?)0+$/, "$1");
8241
+ }
8242
+ function formatPercent2(ratio) {
8243
+ const negative = ratio < 0;
8244
+ const [coefficient, exponentText] = Math.abs(ratio).toString().split("e");
8245
+ const exponent = Number(exponentText ?? 0);
8246
+ const decimalAt = coefficient.indexOf(".");
8247
+ const digits = coefficient.replace(".", "");
8248
+ const shiftedDecimalAt = (decimalAt === -1 ? digits.length : decimalAt) + exponent + 2;
8249
+ let percentage;
8250
+ if (shiftedDecimalAt <= 0) {
8251
+ percentage = `0.${"0".repeat(-shiftedDecimalAt)}${digits}`;
8252
+ } else if (shiftedDecimalAt >= digits.length) {
8253
+ percentage = `${digits}${"0".repeat(shiftedDecimalAt - digits.length)}`;
8254
+ } else {
8255
+ percentage = `${digits.slice(0, shiftedDecimalAt)}.${digits.slice(shiftedDecimalAt)}`;
8256
+ }
8257
+ percentage = percentage.replace(/^0+(?=\d)/, "");
8258
+ if (percentage.includes(".")) percentage = percentage.replace(/0+$/, "").replace(/\.$/, "");
8259
+ return `${negative ? "-" : ""}${percentage}%`;
8260
+ }
8261
+
8212
8262
  // src/utils/exit-triggers.ts
8213
8263
  function parseDate(dateStr) {
8214
8264
  const [y, m, d] = dateStr.split("-").map(Number);
@@ -8257,9 +8307,11 @@ function evaluateProfitAction(trigger, pnlPath, _legs) {
8257
8307
  return { fireEvent: null, partialCloses };
8258
8308
  }
8259
8309
  const scale = trigger.unit === "percent" ? Math.abs(trigger.entryCost) : 1;
8310
+ const entryCostMoney = trigger.unit === "percent" ? toMoneyField(scale, "entry cost") : 0;
8311
+ const stepDollars = (value, field) => trigger.unit === "percent" ? fromMoney(applyRatioField(entryCostMoney, value, field)) : fromMoney(toMoneyField(value, field));
8260
8312
  const normalizedSteps = [...trigger.steps].sort((a, b) => a.armAt - b.armAt).map((step) => ({
8261
- armAt: step.armAt * scale,
8262
- stopAt: step.stopAt * scale,
8313
+ armAt: stepDollars(step.armAt, "steps.armAt"),
8314
+ stopAt: stepDollars(step.stopAt, "steps.stopAt"),
8263
8315
  closeAllocationPct: step.closeAllocationPct
8264
8316
  }));
8265
8317
  let remainingAllocation = 1;
@@ -8271,7 +8323,7 @@ function evaluateProfitAction(trigger, pnlPath, _legs) {
8271
8323
  if (pnl > runningMaxPnl) runningMaxPnl = pnl;
8272
8324
  for (let s = 0; s < normalizedSteps.length; s++) {
8273
8325
  const step = normalizedSteps[s];
8274
- if (!stepPartialFired[s] && step.closeAllocationPct && runningMaxPnl >= step.armAt) {
8326
+ if (!stepPartialFired[s] && step.closeAllocationPct && Number.isFinite(runningMaxPnl) && runningMaxPnl >= step.armAt) {
8275
8327
  stepPartialFired[s] = true;
8276
8328
  const closeAmt = remainingAllocation * step.closeAllocationPct;
8277
8329
  partialCloses.push({
@@ -8284,14 +8336,16 @@ function evaluateProfitAction(trigger, pnlPath, _legs) {
8284
8336
  }
8285
8337
  }
8286
8338
  let activeFloor = -Infinity;
8287
- for (const step of normalizedSteps) {
8288
- if (runningMaxPnl >= step.armAt) {
8289
- activeFloor = Math.max(activeFloor, step.stopAt);
8339
+ if (Number.isFinite(runningMaxPnl)) {
8340
+ for (const step of normalizedSteps) {
8341
+ if (runningMaxPnl >= step.armAt) {
8342
+ activeFloor = Math.max(activeFloor, step.stopAt);
8343
+ }
8290
8344
  }
8291
8345
  }
8292
8346
  if (activeFloor > -Infinity && remainingAllocation > 0 && pnl <= activeFloor) {
8293
8347
  const effectivePnl = pnl * remainingAllocation;
8294
- const detail = trigger.unit === "percent" ? `Profit action: stop adjusted to ${(activeFloor / scale * 100).toFixed(0)}% ($${activeFloor.toFixed(2)}) at max P&L $${runningMaxPnl.toFixed(2)}, hit at $${pnl.toFixed(2)} (remaining ${(remainingAllocation * 100).toFixed(0)}%)` : `Profit action: stop adjusted to $${activeFloor.toFixed(2)} at max P&L $${runningMaxPnl.toFixed(2)}, hit at $${pnl.toFixed(2)} (remaining ${(remainingAllocation * 100).toFixed(0)}%)`;
8348
+ const detail = trigger.unit === "percent" ? `Profit action: stop adjusted to ${formatPercent2(activeFloor / scale)} ($${formatMoney(toMoneyField(activeFloor, "steps.stopAt"))}) at max P&L $${runningMaxPnl.toFixed(2)}, hit at $${pnl.toFixed(2)} (remaining ${(remainingAllocation * 100).toFixed(0)}%)` : `Profit action: stop adjusted to $${formatMoney(toMoneyField(activeFloor, "steps.stopAt"))} at max P&L $${runningMaxPnl.toFixed(2)}, hit at $${pnl.toFixed(2)} (remaining ${(remainingAllocation * 100).toFixed(0)}%)`;
8295
8349
  return {
8296
8350
  fireEvent: {
8297
8351
  type: "profitAction",
@@ -8325,12 +8379,16 @@ function evaluateTrigger(trigger, pnlPath, legs) {
8325
8379
  case "profitTarget": {
8326
8380
  if (trigger.unit === "percent" && trigger.entryCost == null) break;
8327
8381
  const requiredHits = trigger.requiredHits ?? 2;
8328
- const dollarThresholdPT = trigger.unit === "percent" ? threshold * Math.abs(trigger.entryCost) : threshold;
8329
- if (pnl >= dollarThresholdPT) {
8382
+ const ptThresholdMoney = trigger.unit === "percent" ? applyRatioField(
8383
+ toMoneyField(Math.abs(trigger.entryCost), "entry cost"),
8384
+ threshold,
8385
+ "threshold"
8386
+ ) : toMoneyField(threshold, "threshold");
8387
+ if (pnl >= fromMoney(ptThresholdMoney)) {
8330
8388
  if (point.allLegsSync !== false) profitTargetHits++;
8331
8389
  if (profitTargetHits < requiredHits) break;
8332
8390
  fired = true;
8333
- detail = trigger.unit === "percent" ? `P&L $${pnl.toFixed(2)} >= ${(threshold * 100).toFixed(0)}% of $${Math.abs(trigger.entryCost).toFixed(2)} ($${dollarThresholdPT.toFixed(2)})` : `P&L $${pnl.toFixed(2)} >= target $${dollarThresholdPT.toFixed(2)}`;
8391
+ detail = trigger.unit === "percent" ? `P&L $${pnl.toFixed(2)} >= ${formatPercent2(threshold)} of $${formatMoney(toMoneyField(Math.abs(trigger.entryCost), "entry cost"))} ($${formatMoney(ptThresholdMoney)})` : `P&L $${pnl.toFixed(2)} >= target $${formatMoney(ptThresholdMoney)}`;
8334
8392
  } else if (point.allLegsSync !== false) {
8335
8393
  profitTargetHits = 0;
8336
8394
  }
@@ -8339,19 +8397,24 @@ function evaluateTrigger(trigger, pnlPath, legs) {
8339
8397
  case "stopLoss": {
8340
8398
  const absThreshold = Math.abs(threshold);
8341
8399
  if (trigger.unit === "percent" && trigger.entryCost == null) break;
8342
- const dollarThresholdSL = trigger.unit === "percent" ? absThreshold * Math.abs(trigger.entryCost) : absThreshold;
8343
- if (pnl <= -dollarThresholdSL) {
8400
+ const slThresholdMoney = trigger.unit === "percent" ? applyRatioField(
8401
+ toMoneyField(Math.abs(trigger.entryCost), "entry cost"),
8402
+ absThreshold,
8403
+ "threshold"
8404
+ ) : toMoneyField(absThreshold, "threshold");
8405
+ if (pnl <= -fromMoney(slThresholdMoney)) {
8344
8406
  fired = true;
8345
- detail = trigger.unit === "percent" ? `P&L $${pnl.toFixed(2)} <= -${(absThreshold * 100).toFixed(0)}% of $${Math.abs(trigger.entryCost).toFixed(2)} (-$${dollarThresholdSL.toFixed(2)})` : `P&L $${pnl.toFixed(2)} <= stop -$${dollarThresholdSL.toFixed(2)}`;
8407
+ detail = trigger.unit === "percent" ? `P&L $${pnl.toFixed(2)} <= -${formatPercent2(absThreshold)} of $${formatMoney(toMoneyField(Math.abs(trigger.entryCost), "entry cost"))} (-$${formatMoney(slThresholdMoney)})` : `P&L $${pnl.toFixed(2)} <= stop -$${formatMoney(slThresholdMoney)}`;
8346
8408
  }
8347
8409
  break;
8348
8410
  }
8349
8411
  case "trailingStop": {
8350
8412
  const trailAmt = trigger.trailAmount ?? threshold;
8351
- const dropdown = runningMaxPnl - pnl;
8352
- if (dropdown >= trailAmt && runningMaxPnl > -Infinity) {
8413
+ const trailArmed = Number.isFinite(runningMaxPnl);
8414
+ const dropdown = trailArmed ? runningMaxPnl - pnl : 0;
8415
+ if (trailArmed && dropdown >= fromMoney(toMoneyField(trailAmt, "trailAmount"))) {
8353
8416
  fired = true;
8354
- detail = `Dropdown $${dropdown.toFixed(2)} from max $${runningMaxPnl.toFixed(2)} >= trail $${trailAmt.toFixed(2)}`;
8417
+ detail = `Dropdown $${dropdown.toFixed(2)} from max $${runningMaxPnl.toFixed(2)} >= trail $${formatMoney(toMoneyField(trailAmt, "trailAmount"))}`;
8355
8418
  }
8356
8419
  break;
8357
8420
  }
@@ -8610,8 +8673,8 @@ function analyzeExitTriggers(config) {
8610
8673
  if (!firstToFire) {
8611
8674
  summary = `No triggers fired across ${pnlPath.length} data points.`;
8612
8675
  } else if (actualExit) {
8613
- const betterWorse = actualExit.pnlDifference > 0 ? "better" : "worse";
8614
- summary = `${firstToFire.type} fired at ${firstToFire.firedAt} (P&L $${firstToFire.pnlAtFire.toFixed(2)}). Actual exit at ${actualExit.timestamp} (P&L $${actualExit.pnl.toFixed(2)}). Trigger was $${Math.abs(actualExit.pnlDifference).toFixed(2)} ${betterWorse}.`;
8676
+ const betterWorse = actualExit.pnlDifference > 0 ? "better" : actualExit.pnlDifference < 0 ? "worse" : "the same";
8677
+ summary = `${firstToFire.type} fired at ${firstToFire.firedAt} (P&L $${firstToFire.pnlAtFire.toFixed(2)}). Actual exit at ${actualExit.timestamp} (P&L $${actualExit.pnl.toFixed(2)}). Trigger was ${actualExit.pnlDifference === 0 ? "the same" : `$${Math.abs(actualExit.pnlDifference).toFixed(2)} ${betterWorse}`}.`;
8615
8678
  } else {
8616
8679
  summary = `${firstToFire.type} fired first at ${firstToFire.firedAt} (P&L $${firstToFire.pnlAtFire.toFixed(2)}). ${fireEvents.length} trigger(s) fired total.`;
8617
8680
  }
@@ -12476,7 +12539,10 @@ function isNonTradingDay(asOf) {
12476
12539
  const d = /* @__PURE__ */ new Date(`${asOf}T12:00:00Z`);
12477
12540
  const dow = d.getUTCDay();
12478
12541
  if (dow === 0 || dow === 6) return true;
12479
- return false;
12542
+ if (asOf < XNYS_SESSION_CALENDAR_SUPPORTED_FROM || asOf > XNYS_SESSION_CALENDAR_SUPPORTED_THROUGH) {
12543
+ return false;
12544
+ }
12545
+ return !isXnysSessionDate(asOf);
12480
12546
  }
12481
12547
  function unsupportedProviderResult(provider, operation, target, reason, originalError) {
12482
12548
  return {
@@ -12683,6 +12749,44 @@ var MarketIngestor = class {
12683
12749
  return result;
12684
12750
  }
12685
12751
  }
12752
+ async enforceSpotRefreshCompleteness(symbol, asOf, result) {
12753
+ if (result.rowsWritten > 0 || result.status !== "ok") return result;
12754
+ try {
12755
+ const coverage = await this.deps.stores.spot.getCoverage(symbol.toUpperCase(), asOf, asOf);
12756
+ if (coverage.totalDates > 0) {
12757
+ return {
12758
+ status: "skipped",
12759
+ rowsWritten: 0,
12760
+ dateRange: { from: asOf, to: asOf },
12761
+ details: {
12762
+ ...result.details ?? {},
12763
+ reason: "using_cached_coverage",
12764
+ dataset: "spot",
12765
+ symbol: symbol.toUpperCase(),
12766
+ originalStatus: result.status,
12767
+ cachedCoverage: {
12768
+ totalDates: coverage.totalDates,
12769
+ earliest: coverage.earliest,
12770
+ latest: coverage.latest
12771
+ }
12772
+ }
12773
+ };
12774
+ }
12775
+ } catch {
12776
+ }
12777
+ return {
12778
+ status: "error",
12779
+ rowsWritten: 0,
12780
+ error: `Spot refresh for ${symbol.toUpperCase()} on ${asOf} returned zero rows and exact-date coverage remains absent`,
12781
+ details: {
12782
+ ...result.details ?? {},
12783
+ reason: "zero_rows",
12784
+ dataset: "spot",
12785
+ symbol: symbol.toUpperCase(),
12786
+ asOf
12787
+ }
12788
+ };
12789
+ }
12686
12790
  quoteGreeksSourceForProvider(provider) {
12687
12791
  if (provider.name === "massive" || provider.name === "thetadata") {
12688
12792
  return provider.name;
@@ -13380,7 +13484,8 @@ var MarketIngestor = class {
13380
13484
  timespan: "1m",
13381
13485
  provider: opts.provider
13382
13486
  });
13383
- const result = await this.applyCoverageFallback("spot", ticker, opts.asOf, rawResult);
13487
+ const fallbackResult = await this.applyCoverageFallback("spot", ticker, opts.asOf, rawResult);
13488
+ const result = await this.enforceSpotRefreshCompleteness(ticker, opts.asOf, fallbackResult);
13384
13489
  spotResults.push(result);
13385
13490
  if (result.status === "error") errors.push(`spot ${ticker}: ${result.error}`);
13386
13491
  }