@tradejs/node 3.1.29-beta.291 → 3.1.29-beta.292

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/dist/registry.js CHANGED
@@ -928,39 +928,72 @@ function createPortfolioCoordinator({
928
928
  );
929
929
  if (admitted.result) return { reason: admitted.result };
930
930
  let submitted = false;
931
+ let retry = 0;
932
+ let attemptId = entry.id;
933
+ let needsReservation = false;
931
934
  try {
932
935
  const value = await send(
933
936
  async (qty) => {
934
937
  if (!Number.isFinite(qty) || qty <= 0)
935
938
  throw new Error("Invalid submitted portfolio quantity");
936
- submitted = true;
937
- const update = await updateReservation(now2, entry.id, true, qty);
939
+ if (needsReservation) {
940
+ attemptId = `${entry.id.slice(0, 30)}r${++retry}`;
941
+ const admission = await transact(
942
+ now2,
943
+ (s, o) => {
944
+ const r = (0, import_portfolio.reservePortfolioEntry)(policy, strategy, s, o, {
945
+ ...entry,
946
+ id: attemptId,
947
+ qty,
948
+ submitted: true,
949
+ oppositeHandled: false
950
+ });
951
+ return { state: r.state, result: r.reason };
952
+ },
953
+ true
954
+ );
955
+ if (admission.result) throw new Error(admission.result);
956
+ needsReservation = false;
957
+ submitted = true;
958
+ return attemptId;
959
+ }
960
+ const update = await updateReservation(now2, attemptId, true, qty);
938
961
  if (!update.result)
939
962
  throw new Error("PORTFOLIO_SUBMISSION_NO_LONGER_ADMITTED");
963
+ submitted = true;
964
+ return attemptId;
940
965
  },
941
- async () => {
966
+ async (retrying = false) => {
942
967
  await transact(
943
968
  now2,
944
969
  (s) => ({
945
970
  state: {
946
971
  ...s,
947
- reservations: s.reservations.filter((r) => r.id !== entry.id),
972
+ reservations: s.reservations.filter(
973
+ (r) => r.id !== attemptId
974
+ ),
948
975
  completedSignals: {
949
- ...s.completedSignals,
950
- [entry.signalId]: now2
976
+ ...Object.fromEntries(
977
+ Object.entries(s.completedSignals).filter(
978
+ ([id]) => !retrying || id !== entry.signalId
979
+ )
980
+ ),
981
+ ...retrying ? {} : { [entry.signalId]: now2 }
951
982
  }
952
983
  },
953
984
  result: void 0
954
985
  }),
955
986
  true
956
987
  );
988
+ submitted = false;
989
+ needsReservation = retrying;
957
990
  }
958
991
  );
959
- if (!submitted) await updateReservation(now2, entry.id, false);
992
+ if (!submitted) await updateReservation(now2, attemptId, false);
960
993
  else await tick(now2, true);
961
994
  return { value };
962
995
  } catch (error) {
963
- if (!submitted) await updateReservation(now2, entry.id, false);
996
+ if (!submitted) await updateReservation(now2, attemptId, false);
964
997
  throw error;
965
998
  }
966
999
  }
@@ -1328,7 +1361,9 @@ function getPortfolioRuntime({
1328
1361
  env,
1329
1362
  userName,
1330
1363
  accountId,
1331
- deploymentId
1364
+ deploymentId,
1365
+ symbol,
1366
+ canClose
1332
1367
  }) {
1333
1368
  const policy = (0, import_portfolio2.parsePortfolioPolicy)(value);
1334
1369
  if (!policy) return void 0;
@@ -1359,8 +1394,13 @@ function getPortfolioRuntime({
1359
1394
  if (existing) {
1360
1395
  if (existing.fingerprint !== fingerprint3)
1361
1396
  throw new Error("Conflicting portfolio configuration for the same scope");
1397
+ if (symbol && canClose)
1398
+ existing.permissions.set(JSON.stringify([strategy, symbol]), canClose);
1362
1399
  return existing.coordinator;
1363
1400
  }
1401
+ const permissions = /* @__PURE__ */ new Map();
1402
+ if (symbol && canClose)
1403
+ permissions.set(JSON.stringify([strategy, symbol]), canClose);
1364
1404
  const coordinator = createPortfolioCoordinator({
1365
1405
  policy,
1366
1406
  strategy,
@@ -1381,6 +1421,10 @@ function getPortfolioRuntime({
1381
1421
  },
1382
1422
  observe: (asOf, reservations) => replay ? connector.getPortfolioObservation(asOf, reservations) : observeRuntimePortfolio(connector, binding, asOf, reservations),
1383
1423
  async close(position, intent, asOf) {
1424
+ const permission = permissions.get(
1425
+ JSON.stringify([position.strategy, position.symbol])
1426
+ );
1427
+ if (!permission || !await permission(position, intent, asOf)) return;
1384
1428
  if (!replay) {
1385
1429
  const trade = await getActiveRuntimeTrade({
1386
1430
  ...binding,
@@ -1415,7 +1459,7 @@ function getPortfolioRuntime({
1415
1459
  }
1416
1460
  }
1417
1461
  });
1418
- registry.set(cacheKey, { fingerprint: fingerprint3, coordinator });
1462
+ registry.set(cacheKey, { fingerprint: fingerprint3, coordinator, permissions });
1419
1463
  return coordinator;
1420
1464
  }
1421
1465
 
@@ -6274,7 +6318,7 @@ var executeEntryOrder = async ({
6274
6318
  }) => {
6275
6319
  await beforePlaceOrder?.();
6276
6320
  const previousPosition = positionIntent === "increase" ? await connector.getPosition(symbol) : null;
6277
- const orderId = signal.orderId || createRuntimeOrderId(signal.strategy);
6321
+ let orderId = signal.orderId || createRuntimeOrderId(signal.strategy);
6278
6322
  const signalTimestamp = signal.timestamp;
6279
6323
  const signalClosePrice = currentPrice;
6280
6324
  signal.orderId = orderId;
@@ -6347,11 +6391,12 @@ var executeEntryOrder = async ({
6347
6391
  ...beforeSubmit || markPortfolioSubmitted ? {
6348
6392
  beforeSubmit: async (context) => {
6349
6393
  await beforeSubmit?.(context);
6350
- await markPortfolioSubmitted?.(context.qty);
6394
+ return markPortfolioSubmitted?.(context.qty);
6351
6395
  }
6352
6396
  } : {}
6353
6397
  });
6354
6398
  const orderAckTime = Date.now();
6399
+ if (markPortfolioSubmitted && signal.orderId) orderId = signal.orderId;
6355
6400
  const placedQty = typeof signal.orderQty === "number" && Number.isFinite(signal.orderQty) && signal.orderQty > 0 ? signal.orderQty : qty;
6356
6401
  const currentPosition = await connector.getPosition(symbol);
6357
6402
  const fillTime = Date.now();
@@ -7639,7 +7684,27 @@ var createStrategyRuntime = ({
7639
7684
  const projectConfig = await loadTradejsConfig(projectRoot);
7640
7685
  const projectHooks = projectConfig.hooks;
7641
7686
  const env = String(config.ENV ?? "BACKTEST");
7687
+ let portfolioMarket;
7642
7688
  const portfolio = getPortfolioRuntime({
7689
+ symbol,
7690
+ canClose: async (position, intent, asOf) => {
7691
+ if (!portfolioMarket || portfolioMarket.candle.timestamp > asOf || !isRuntimeOrderExecutionEnabled({ config, env, connector }))
7692
+ return false;
7693
+ const gate = await authorizeClose(
7694
+ {
7695
+ kind: "exit",
7696
+ code: intent.reason,
7697
+ closePlan: {
7698
+ direction: position.direction,
7699
+ price: position.markPrice,
7700
+ timestamp: asOf
7701
+ }
7702
+ },
7703
+ portfolioMarket,
7704
+ position.strategy
7705
+ );
7706
+ return gate?.allow !== false;
7707
+ },
7643
7708
  connectorName,
7644
7709
  value: baseConfig?.PORTFOLIO_POLICY ?? config.PORTFOLIO_POLICY,
7645
7710
  strategy: strategyName,
@@ -7814,7 +7879,10 @@ var createStrategyRuntime = ({
7814
7879
  return invokeHook(stage, hook, params, errorContext);
7815
7880
  };
7816
7881
  const invokeGateHooks = async (stage, hook, params, errorContext = {}) => {
7817
- const failureResult = stage === "beforeEntryGate" ? { allow: false, reason: "ERROR" } : void 0;
7882
+ const failureResult = {
7883
+ allow: false,
7884
+ reason: "ERROR"
7885
+ };
7818
7886
  const projectResults = await invokeProjectHooks(
7819
7887
  stage,
7820
7888
  params,
@@ -7835,6 +7903,12 @@ var createStrategyRuntime = ({
7835
7903
  failureResult
7836
7904
  );
7837
7905
  };
7906
+ const authorizeClose = (decision, market, owner = strategyName) => invokeGateHooks(
7907
+ "beforeClosePosition",
7908
+ resolveManifest(owner)?.hooks?.beforeClosePosition,
7909
+ { ctx: getHookCtx(owner), market, decision },
7910
+ { decision, market }
7911
+ );
7838
7912
  const applyProjectAfterCoreDecisionHooks = async ({
7839
7913
  hookCtx,
7840
7914
  market,
@@ -8148,6 +8222,7 @@ var createStrategyRuntime = ({
8148
8222
  });
8149
8223
  };
8150
8224
  const runWithDecisionOverride = async (candle, btcCandle, options = {}) => {
8225
+ portfolioMarket = { candle, btcCandle };
8151
8226
  if (portfolioContext)
8152
8227
  await portfolioContext(
8153
8228
  env === "BACKTEST" || env === "PARITY" ? candle.timestamp : Date.now()
@@ -8254,15 +8329,10 @@ var createStrategyRuntime = ({
8254
8329
  if (!makeOrdersEnabled) {
8255
8330
  return decision.code;
8256
8331
  }
8257
- const closeGate = await invokeGateHooks(
8258
- "beforeClosePosition",
8259
- decisionManifest?.hooks?.beforeClosePosition,
8260
- {
8261
- ctx: decisionHookCtx,
8262
- market,
8263
- decision
8264
- },
8265
- { decision, market }
8332
+ const closeGate = await authorizeClose(
8333
+ decision,
8334
+ market,
8335
+ decisionHookCtx.strategyName
8266
8336
  );
8267
8337
  if (closeGate?.allow === false) {
8268
8338
  return closeGate.reason ? `CLOSE_BLOCKED_BY_HOOK:${closeGate.reason}` : "CLOSE_BLOCKED_BY_HOOK";
package/dist/registry.mjs CHANGED
@@ -1,6 +1,6 @@
1
1
  import "./chunk-JQOLTLEY.mjs";
2
- import "./chunk-HRON2EAI.mjs";
3
- import "./chunk-UUNEWAT4.mjs";
2
+ import "./chunk-EVKQ4DI6.mjs";
3
+ import "./chunk-NJEYNGRU.mjs";
4
4
  import {
5
5
  ensureIndicatorPluginsLoaded,
6
6
  ensureStrategyPluginsLoaded,
@@ -2225,39 +2225,72 @@ function createPortfolioCoordinator({
2225
2225
  );
2226
2226
  if (admitted.result) return { reason: admitted.result };
2227
2227
  let submitted = false;
2228
+ let retry = 0;
2229
+ let attemptId = entry.id;
2230
+ let needsReservation = false;
2228
2231
  try {
2229
2232
  const value = await send(
2230
2233
  async (qty) => {
2231
2234
  if (!Number.isFinite(qty) || qty <= 0)
2232
2235
  throw new Error("Invalid submitted portfolio quantity");
2233
- submitted = true;
2234
- const update = await updateReservation(now2, entry.id, true, qty);
2236
+ if (needsReservation) {
2237
+ attemptId = `${entry.id.slice(0, 30)}r${++retry}`;
2238
+ const admission = await transact(
2239
+ now2,
2240
+ (s, o) => {
2241
+ const r = (0, import_portfolio.reservePortfolioEntry)(policy, strategy, s, o, {
2242
+ ...entry,
2243
+ id: attemptId,
2244
+ qty,
2245
+ submitted: true,
2246
+ oppositeHandled: false
2247
+ });
2248
+ return { state: r.state, result: r.reason };
2249
+ },
2250
+ true
2251
+ );
2252
+ if (admission.result) throw new Error(admission.result);
2253
+ needsReservation = false;
2254
+ submitted = true;
2255
+ return attemptId;
2256
+ }
2257
+ const update = await updateReservation(now2, attemptId, true, qty);
2235
2258
  if (!update.result)
2236
2259
  throw new Error("PORTFOLIO_SUBMISSION_NO_LONGER_ADMITTED");
2260
+ submitted = true;
2261
+ return attemptId;
2237
2262
  },
2238
- async () => {
2263
+ async (retrying = false) => {
2239
2264
  await transact(
2240
2265
  now2,
2241
2266
  (s) => ({
2242
2267
  state: {
2243
2268
  ...s,
2244
- reservations: s.reservations.filter((r) => r.id !== entry.id),
2269
+ reservations: s.reservations.filter(
2270
+ (r) => r.id !== attemptId
2271
+ ),
2245
2272
  completedSignals: {
2246
- ...s.completedSignals,
2247
- [entry.signalId]: now2
2273
+ ...Object.fromEntries(
2274
+ Object.entries(s.completedSignals).filter(
2275
+ ([id]) => !retrying || id !== entry.signalId
2276
+ )
2277
+ ),
2278
+ ...retrying ? {} : { [entry.signalId]: now2 }
2248
2279
  }
2249
2280
  },
2250
2281
  result: void 0
2251
2282
  }),
2252
2283
  true
2253
2284
  );
2285
+ submitted = false;
2286
+ needsReservation = retrying;
2254
2287
  }
2255
2288
  );
2256
- if (!submitted) await updateReservation(now2, entry.id, false);
2289
+ if (!submitted) await updateReservation(now2, attemptId, false);
2257
2290
  else await tick(now2, true);
2258
2291
  return { value };
2259
2292
  } catch (error) {
2260
- if (!submitted) await updateReservation(now2, entry.id, false);
2293
+ if (!submitted) await updateReservation(now2, attemptId, false);
2261
2294
  throw error;
2262
2295
  }
2263
2296
  }
@@ -2625,7 +2658,9 @@ function getPortfolioRuntime({
2625
2658
  env,
2626
2659
  userName,
2627
2660
  accountId,
2628
- deploymentId
2661
+ deploymentId,
2662
+ symbol,
2663
+ canClose
2629
2664
  }) {
2630
2665
  const policy = (0, import_portfolio2.parsePortfolioPolicy)(value);
2631
2666
  if (!policy) return void 0;
@@ -2656,8 +2691,13 @@ function getPortfolioRuntime({
2656
2691
  if (existing) {
2657
2692
  if (existing.fingerprint !== fingerprint3)
2658
2693
  throw new Error("Conflicting portfolio configuration for the same scope");
2694
+ if (symbol && canClose)
2695
+ existing.permissions.set(JSON.stringify([strategy, symbol]), canClose);
2659
2696
  return existing.coordinator;
2660
2697
  }
2698
+ const permissions = /* @__PURE__ */ new Map();
2699
+ if (symbol && canClose)
2700
+ permissions.set(JSON.stringify([strategy, symbol]), canClose);
2661
2701
  const coordinator = createPortfolioCoordinator({
2662
2702
  policy,
2663
2703
  strategy,
@@ -2678,6 +2718,10 @@ function getPortfolioRuntime({
2678
2718
  },
2679
2719
  observe: (asOf, reservations) => replay ? connector.getPortfolioObservation(asOf, reservations) : observeRuntimePortfolio(connector, binding, asOf, reservations),
2680
2720
  async close(position, intent, asOf) {
2721
+ const permission = permissions.get(
2722
+ JSON.stringify([position.strategy, position.symbol])
2723
+ );
2724
+ if (!permission || !await permission(position, intent, asOf)) return;
2681
2725
  if (!replay) {
2682
2726
  const trade = await getActiveRuntimeTrade({
2683
2727
  ...binding,
@@ -2712,7 +2756,7 @@ function getPortfolioRuntime({
2712
2756
  }
2713
2757
  }
2714
2758
  });
2715
- registry.set(cacheKey, { fingerprint: fingerprint3, coordinator });
2759
+ registry.set(cacheKey, { fingerprint: fingerprint3, coordinator, permissions });
2716
2760
  return coordinator;
2717
2761
  }
2718
2762
 
@@ -7260,7 +7304,7 @@ var executeEntryOrder = async ({
7260
7304
  }) => {
7261
7305
  await beforePlaceOrder?.();
7262
7306
  const previousPosition = positionIntent === "increase" ? await connector.getPosition(symbol) : null;
7263
- const orderId = signal.orderId || createRuntimeOrderId(signal.strategy);
7307
+ let orderId = signal.orderId || createRuntimeOrderId(signal.strategy);
7264
7308
  const signalTimestamp = signal.timestamp;
7265
7309
  const signalClosePrice = currentPrice;
7266
7310
  signal.orderId = orderId;
@@ -7333,11 +7377,12 @@ var executeEntryOrder = async ({
7333
7377
  ...beforeSubmit || markPortfolioSubmitted ? {
7334
7378
  beforeSubmit: async (context) => {
7335
7379
  await beforeSubmit?.(context);
7336
- await markPortfolioSubmitted?.(context.qty);
7380
+ return markPortfolioSubmitted?.(context.qty);
7337
7381
  }
7338
7382
  } : {}
7339
7383
  });
7340
7384
  const orderAckTime = Date.now();
7385
+ if (markPortfolioSubmitted && signal.orderId) orderId = signal.orderId;
7341
7386
  const placedQty = typeof signal.orderQty === "number" && Number.isFinite(signal.orderQty) && signal.orderQty > 0 ? signal.orderQty : qty;
7342
7387
  const currentPosition = await connector.getPosition(symbol);
7343
7388
  const fillTime = Date.now();
@@ -8625,7 +8670,27 @@ var createStrategyRuntime = ({
8625
8670
  const projectConfig = await loadTradejsConfig(projectRoot);
8626
8671
  const projectHooks = projectConfig.hooks;
8627
8672
  const env = String(config.ENV ?? "BACKTEST");
8673
+ let portfolioMarket;
8628
8674
  const portfolio = getPortfolioRuntime({
8675
+ symbol,
8676
+ canClose: async (position, intent, asOf) => {
8677
+ if (!portfolioMarket || portfolioMarket.candle.timestamp > asOf || !isRuntimeOrderExecutionEnabled({ config, env, connector }))
8678
+ return false;
8679
+ const gate = await authorizeClose(
8680
+ {
8681
+ kind: "exit",
8682
+ code: intent.reason,
8683
+ closePlan: {
8684
+ direction: position.direction,
8685
+ price: position.markPrice,
8686
+ timestamp: asOf
8687
+ }
8688
+ },
8689
+ portfolioMarket,
8690
+ position.strategy
8691
+ );
8692
+ return gate?.allow !== false;
8693
+ },
8629
8694
  connectorName,
8630
8695
  value: baseConfig?.PORTFOLIO_POLICY ?? config.PORTFOLIO_POLICY,
8631
8696
  strategy: strategyName,
@@ -8800,7 +8865,10 @@ var createStrategyRuntime = ({
8800
8865
  return invokeHook(stage, hook, params, errorContext);
8801
8866
  };
8802
8867
  const invokeGateHooks = async (stage, hook, params, errorContext = {}) => {
8803
- const failureResult = stage === "beforeEntryGate" ? { allow: false, reason: "ERROR" } : void 0;
8868
+ const failureResult = {
8869
+ allow: false,
8870
+ reason: "ERROR"
8871
+ };
8804
8872
  const projectResults = await invokeProjectHooks(
8805
8873
  stage,
8806
8874
  params,
@@ -8821,6 +8889,12 @@ var createStrategyRuntime = ({
8821
8889
  failureResult
8822
8890
  );
8823
8891
  };
8892
+ const authorizeClose = (decision, market, owner = strategyName) => invokeGateHooks(
8893
+ "beforeClosePosition",
8894
+ resolveManifest(owner)?.hooks?.beforeClosePosition,
8895
+ { ctx: getHookCtx(owner), market, decision },
8896
+ { decision, market }
8897
+ );
8824
8898
  const applyProjectAfterCoreDecisionHooks = async ({
8825
8899
  hookCtx,
8826
8900
  market,
@@ -9134,6 +9208,7 @@ var createStrategyRuntime = ({
9134
9208
  });
9135
9209
  };
9136
9210
  const runWithDecisionOverride = async (candle, btcCandle, options = {}) => {
9211
+ portfolioMarket = { candle, btcCandle };
9137
9212
  if (portfolioContext)
9138
9213
  await portfolioContext(
9139
9214
  env === "BACKTEST" || env === "PARITY" ? candle.timestamp : Date.now()
@@ -9240,15 +9315,10 @@ var createStrategyRuntime = ({
9240
9315
  if (!makeOrdersEnabled) {
9241
9316
  return decision.code;
9242
9317
  }
9243
- const closeGate = await invokeGateHooks(
9244
- "beforeClosePosition",
9245
- decisionManifest?.hooks?.beforeClosePosition,
9246
- {
9247
- ctx: decisionHookCtx,
9248
- market,
9249
- decision
9250
- },
9251
- { decision, market }
9318
+ const closeGate = await authorizeClose(
9319
+ decision,
9320
+ market,
9321
+ decisionHookCtx.strategyName
9252
9322
  );
9253
9323
  if (closeGate?.allow === false) {
9254
9324
  return closeGate.reason ? `CLOSE_BLOCKED_BY_HOOK:${closeGate.reason}` : "CLOSE_BLOCKED_BY_HOOK";
@@ -9,8 +9,8 @@ import {
9
9
  listRuntimeDeployments,
10
10
  loadResolvedRuntimeStrategies
11
11
  } from "./chunk-W6PPAADK.mjs";
12
- import "./chunk-HRON2EAI.mjs";
13
- import "./chunk-UUNEWAT4.mjs";
12
+ import "./chunk-EVKQ4DI6.mjs";
13
+ import "./chunk-NJEYNGRU.mjs";
14
14
  import "./chunk-PYJWXAYR.mjs";
15
15
  import "./chunk-LZDXRXIU.mjs";
16
16
  import "./chunk-YR55Q355.mjs";