@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/backtest.js CHANGED
@@ -937,39 +937,72 @@ function createPortfolioCoordinator({
937
937
  );
938
938
  if (admitted.result) return { reason: admitted.result };
939
939
  let submitted = false;
940
+ let retry = 0;
941
+ let attemptId = entry.id;
942
+ let needsReservation = false;
940
943
  try {
941
944
  const value = await send(
942
945
  async (qty) => {
943
946
  if (!Number.isFinite(qty) || qty <= 0)
944
947
  throw new Error("Invalid submitted portfolio quantity");
945
- submitted = true;
946
- const update = await updateReservation(now2, entry.id, true, qty);
948
+ if (needsReservation) {
949
+ attemptId = `${entry.id.slice(0, 30)}r${++retry}`;
950
+ const admission = await transact(
951
+ now2,
952
+ (s, o) => {
953
+ const r = (0, import_portfolio.reservePortfolioEntry)(policy, strategy, s, o, {
954
+ ...entry,
955
+ id: attemptId,
956
+ qty,
957
+ submitted: true,
958
+ oppositeHandled: false
959
+ });
960
+ return { state: r.state, result: r.reason };
961
+ },
962
+ true
963
+ );
964
+ if (admission.result) throw new Error(admission.result);
965
+ needsReservation = false;
966
+ submitted = true;
967
+ return attemptId;
968
+ }
969
+ const update = await updateReservation(now2, attemptId, true, qty);
947
970
  if (!update.result)
948
971
  throw new Error("PORTFOLIO_SUBMISSION_NO_LONGER_ADMITTED");
972
+ submitted = true;
973
+ return attemptId;
949
974
  },
950
- async () => {
975
+ async (retrying = false) => {
951
976
  await transact(
952
977
  now2,
953
978
  (s) => ({
954
979
  state: {
955
980
  ...s,
956
- reservations: s.reservations.filter((r) => r.id !== entry.id),
981
+ reservations: s.reservations.filter(
982
+ (r) => r.id !== attemptId
983
+ ),
957
984
  completedSignals: {
958
- ...s.completedSignals,
959
- [entry.signalId]: now2
985
+ ...Object.fromEntries(
986
+ Object.entries(s.completedSignals).filter(
987
+ ([id]) => !retrying || id !== entry.signalId
988
+ )
989
+ ),
990
+ ...retrying ? {} : { [entry.signalId]: now2 }
960
991
  }
961
992
  },
962
993
  result: void 0
963
994
  }),
964
995
  true
965
996
  );
997
+ submitted = false;
998
+ needsReservation = retrying;
966
999
  }
967
1000
  );
968
- if (!submitted) await updateReservation(now2, entry.id, false);
1001
+ if (!submitted) await updateReservation(now2, attemptId, false);
969
1002
  else await tick(now2, true);
970
1003
  return { value };
971
1004
  } catch (error) {
972
- if (!submitted) await updateReservation(now2, entry.id, false);
1005
+ if (!submitted) await updateReservation(now2, attemptId, false);
973
1006
  throw error;
974
1007
  }
975
1008
  }
@@ -1337,7 +1370,9 @@ function getPortfolioRuntime({
1337
1370
  env,
1338
1371
  userName,
1339
1372
  accountId,
1340
- deploymentId
1373
+ deploymentId,
1374
+ symbol,
1375
+ canClose
1341
1376
  }) {
1342
1377
  const policy = (0, import_portfolio2.parsePortfolioPolicy)(value);
1343
1378
  if (!policy) return void 0;
@@ -1368,8 +1403,13 @@ function getPortfolioRuntime({
1368
1403
  if (existing) {
1369
1404
  if (existing.fingerprint !== fingerprint3)
1370
1405
  throw new Error("Conflicting portfolio configuration for the same scope");
1406
+ if (symbol && canClose)
1407
+ existing.permissions.set(JSON.stringify([strategy, symbol]), canClose);
1371
1408
  return existing.coordinator;
1372
1409
  }
1410
+ const permissions = /* @__PURE__ */ new Map();
1411
+ if (symbol && canClose)
1412
+ permissions.set(JSON.stringify([strategy, symbol]), canClose);
1373
1413
  const coordinator = createPortfolioCoordinator({
1374
1414
  policy,
1375
1415
  strategy,
@@ -1390,6 +1430,10 @@ function getPortfolioRuntime({
1390
1430
  },
1391
1431
  observe: (asOf, reservations) => replay ? connector.getPortfolioObservation(asOf, reservations) : observeRuntimePortfolio(connector, binding, asOf, reservations),
1392
1432
  async close(position, intent, asOf) {
1433
+ const permission = permissions.get(
1434
+ JSON.stringify([position.strategy, position.symbol])
1435
+ );
1436
+ if (!permission || !await permission(position, intent, asOf)) return;
1393
1437
  if (!replay) {
1394
1438
  const trade = await getActiveRuntimeTrade({
1395
1439
  ...binding,
@@ -1424,7 +1468,7 @@ function getPortfolioRuntime({
1424
1468
  }
1425
1469
  }
1426
1470
  });
1427
- registry.set(cacheKey, { fingerprint: fingerprint3, coordinator });
1471
+ registry.set(cacheKey, { fingerprint: fingerprint3, coordinator, permissions });
1428
1472
  return coordinator;
1429
1473
  }
1430
1474
 
@@ -6241,7 +6285,7 @@ var executeEntryOrder = async ({
6241
6285
  }) => {
6242
6286
  await beforePlaceOrder?.();
6243
6287
  const previousPosition = positionIntent === "increase" ? await connector.getPosition(symbol) : null;
6244
- const orderId = signal.orderId || createRuntimeOrderId(signal.strategy);
6288
+ let orderId = signal.orderId || createRuntimeOrderId(signal.strategy);
6245
6289
  const signalTimestamp = signal.timestamp;
6246
6290
  const signalClosePrice = currentPrice;
6247
6291
  signal.orderId = orderId;
@@ -6314,11 +6358,12 @@ var executeEntryOrder = async ({
6314
6358
  ...beforeSubmit || markPortfolioSubmitted ? {
6315
6359
  beforeSubmit: async (context) => {
6316
6360
  await beforeSubmit?.(context);
6317
- await markPortfolioSubmitted?.(context.qty);
6361
+ return markPortfolioSubmitted?.(context.qty);
6318
6362
  }
6319
6363
  } : {}
6320
6364
  });
6321
6365
  const orderAckTime = Date.now();
6366
+ if (markPortfolioSubmitted && signal.orderId) orderId = signal.orderId;
6322
6367
  const placedQty = typeof signal.orderQty === "number" && Number.isFinite(signal.orderQty) && signal.orderQty > 0 ? signal.orderQty : qty;
6323
6368
  const currentPosition = await connector.getPosition(symbol);
6324
6369
  const fillTime = Date.now();
@@ -7609,7 +7654,27 @@ var createStrategyRuntime = ({
7609
7654
  const projectConfig = await loadTradejsConfig(projectRoot);
7610
7655
  const projectHooks = projectConfig.hooks;
7611
7656
  const env = String(config.ENV ?? "BACKTEST");
7657
+ let portfolioMarket;
7612
7658
  const portfolio = getPortfolioRuntime({
7659
+ symbol,
7660
+ canClose: async (position, intent, asOf) => {
7661
+ if (!portfolioMarket || portfolioMarket.candle.timestamp > asOf || !isRuntimeOrderExecutionEnabled({ config, env, connector }))
7662
+ return false;
7663
+ const gate = await authorizeClose(
7664
+ {
7665
+ kind: "exit",
7666
+ code: intent.reason,
7667
+ closePlan: {
7668
+ direction: position.direction,
7669
+ price: position.markPrice,
7670
+ timestamp: asOf
7671
+ }
7672
+ },
7673
+ portfolioMarket,
7674
+ position.strategy
7675
+ );
7676
+ return gate?.allow !== false;
7677
+ },
7613
7678
  connectorName,
7614
7679
  value: baseConfig?.PORTFOLIO_POLICY ?? config.PORTFOLIO_POLICY,
7615
7680
  strategy: strategyName,
@@ -7784,7 +7849,10 @@ var createStrategyRuntime = ({
7784
7849
  return invokeHook(stage, hook, params, errorContext);
7785
7850
  };
7786
7851
  const invokeGateHooks = async (stage, hook, params, errorContext = {}) => {
7787
- const failureResult = stage === "beforeEntryGate" ? { allow: false, reason: "ERROR" } : void 0;
7852
+ const failureResult = {
7853
+ allow: false,
7854
+ reason: "ERROR"
7855
+ };
7788
7856
  const projectResults = await invokeProjectHooks(
7789
7857
  stage,
7790
7858
  params,
@@ -7805,6 +7873,12 @@ var createStrategyRuntime = ({
7805
7873
  failureResult
7806
7874
  );
7807
7875
  };
7876
+ const authorizeClose = (decision, market, owner = strategyName) => invokeGateHooks(
7877
+ "beforeClosePosition",
7878
+ resolveManifest(owner)?.hooks?.beforeClosePosition,
7879
+ { ctx: getHookCtx(owner), market, decision },
7880
+ { decision, market }
7881
+ );
7808
7882
  const applyProjectAfterCoreDecisionHooks = async ({
7809
7883
  hookCtx,
7810
7884
  market,
@@ -8118,6 +8192,7 @@ var createStrategyRuntime = ({
8118
8192
  });
8119
8193
  };
8120
8194
  const runWithDecisionOverride = async (candle, btcCandle, options = {}) => {
8195
+ portfolioMarket = { candle, btcCandle };
8121
8196
  if (portfolioContext)
8122
8197
  await portfolioContext(
8123
8198
  env === "BACKTEST" || env === "PARITY" ? candle.timestamp : Date.now()
@@ -8224,15 +8299,10 @@ var createStrategyRuntime = ({
8224
8299
  if (!makeOrdersEnabled) {
8225
8300
  return decision.code;
8226
8301
  }
8227
- const closeGate = await invokeGateHooks(
8228
- "beforeClosePosition",
8229
- decisionManifest?.hooks?.beforeClosePosition,
8230
- {
8231
- ctx: decisionHookCtx,
8232
- market,
8233
- decision
8234
- },
8235
- { decision, market }
8302
+ const closeGate = await authorizeClose(
8303
+ decision,
8304
+ market,
8305
+ decisionHookCtx.strategyName
8236
8306
  );
8237
8307
  if (closeGate?.allow === false) {
8238
8308
  return closeGate.reason ? `CLOSE_BLOCKED_BY_HOOK:${closeGate.reason}` : "CLOSE_BLOCKED_BY_HOOK";
package/dist/backtest.mjs CHANGED
@@ -10,8 +10,8 @@ import {
10
10
  evaluateResearchEntryPolicy,
11
11
  matchesResearchEntryRules,
12
12
  parseResearchEntryPolicy
13
- } from "./chunk-HRON2EAI.mjs";
14
- import "./chunk-UUNEWAT4.mjs";
13
+ } from "./chunk-EVKQ4DI6.mjs";
14
+ import "./chunk-NJEYNGRU.mjs";
15
15
  import {
16
16
  buildAiPayload,
17
17
  getStrategyCreator
@@ -1,7 +1,7 @@
1
1
  import {
2
2
  createMemoryPortfolioStore,
3
3
  createPortfolioCoordinator
4
- } from "./chunk-UUNEWAT4.mjs";
4
+ } from "./chunk-NJEYNGRU.mjs";
5
5
  import {
6
6
  askAI,
7
7
  buildAiPayload,
@@ -3237,7 +3237,7 @@ var executeEntryOrder = async ({
3237
3237
  }) => {
3238
3238
  await beforePlaceOrder?.();
3239
3239
  const previousPosition = positionIntent === "increase" ? await connector.getPosition(symbol) : null;
3240
- const orderId = signal.orderId || createRuntimeOrderId(signal.strategy);
3240
+ let orderId = signal.orderId || createRuntimeOrderId(signal.strategy);
3241
3241
  const signalTimestamp = signal.timestamp;
3242
3242
  const signalClosePrice = currentPrice;
3243
3243
  signal.orderId = orderId;
@@ -3310,11 +3310,12 @@ var executeEntryOrder = async ({
3310
3310
  ...beforeSubmit || markPortfolioSubmitted ? {
3311
3311
  beforeSubmit: async (context) => {
3312
3312
  await beforeSubmit?.(context);
3313
- await markPortfolioSubmitted?.(context.qty);
3313
+ return markPortfolioSubmitted?.(context.qty);
3314
3314
  }
3315
3315
  } : {}
3316
3316
  });
3317
3317
  const orderAckTime = Date.now();
3318
+ if (markPortfolioSubmitted && signal.orderId) orderId = signal.orderId;
3318
3319
  const placedQty = typeof signal.orderQty === "number" && Number.isFinite(signal.orderQty) && signal.orderQty > 0 ? signal.orderQty : qty;
3319
3320
  const currentPosition = await connector.getPosition(symbol);
3320
3321
  const fillTime = Date.now();
@@ -3681,7 +3682,9 @@ function getPortfolioRuntime({
3681
3682
  env,
3682
3683
  userName,
3683
3684
  accountId,
3684
- deploymentId
3685
+ deploymentId,
3686
+ symbol,
3687
+ canClose
3685
3688
  }) {
3686
3689
  const policy = parsePortfolioPolicy(value);
3687
3690
  if (!policy) return void 0;
@@ -3712,8 +3715,13 @@ function getPortfolioRuntime({
3712
3715
  if (existing) {
3713
3716
  if (existing.fingerprint !== fingerprint3)
3714
3717
  throw new Error("Conflicting portfolio configuration for the same scope");
3718
+ if (symbol && canClose)
3719
+ existing.permissions.set(JSON.stringify([strategy, symbol]), canClose);
3715
3720
  return existing.coordinator;
3716
3721
  }
3722
+ const permissions = /* @__PURE__ */ new Map();
3723
+ if (symbol && canClose)
3724
+ permissions.set(JSON.stringify([strategy, symbol]), canClose);
3717
3725
  const coordinator = createPortfolioCoordinator({
3718
3726
  policy,
3719
3727
  strategy,
@@ -3734,6 +3742,10 @@ function getPortfolioRuntime({
3734
3742
  },
3735
3743
  observe: (asOf, reservations) => replay ? connector.getPortfolioObservation(asOf, reservations) : observeRuntimePortfolio(connector, binding, asOf, reservations),
3736
3744
  async close(position, intent, asOf) {
3745
+ const permission = permissions.get(
3746
+ JSON.stringify([position.strategy, position.symbol])
3747
+ );
3748
+ if (!permission || !await permission(position, intent, asOf)) return;
3737
3749
  if (!replay) {
3738
3750
  const trade = await getActiveRuntimeTrade({
3739
3751
  ...binding,
@@ -3768,7 +3780,7 @@ function getPortfolioRuntime({
3768
3780
  }
3769
3781
  }
3770
3782
  });
3771
- registry.set(cacheKey, { fingerprint: fingerprint3, coordinator });
3783
+ registry.set(cacheKey, { fingerprint: fingerprint3, coordinator, permissions });
3772
3784
  return coordinator;
3773
3785
  }
3774
3786
 
@@ -4809,7 +4821,27 @@ var createStrategyRuntime = ({
4809
4821
  const projectConfig = await loadTradejsConfig(projectRoot);
4810
4822
  const projectHooks = projectConfig.hooks;
4811
4823
  const env = String(config.ENV ?? "BACKTEST");
4824
+ let portfolioMarket;
4812
4825
  const portfolio = getPortfolioRuntime({
4826
+ symbol,
4827
+ canClose: async (position, intent, asOf) => {
4828
+ if (!portfolioMarket || portfolioMarket.candle.timestamp > asOf || !isRuntimeOrderExecutionEnabled({ config, env, connector }))
4829
+ return false;
4830
+ const gate = await authorizeClose(
4831
+ {
4832
+ kind: "exit",
4833
+ code: intent.reason,
4834
+ closePlan: {
4835
+ direction: position.direction,
4836
+ price: position.markPrice,
4837
+ timestamp: asOf
4838
+ }
4839
+ },
4840
+ portfolioMarket,
4841
+ position.strategy
4842
+ );
4843
+ return gate?.allow !== false;
4844
+ },
4813
4845
  connectorName,
4814
4846
  value: baseConfig?.PORTFOLIO_POLICY ?? config.PORTFOLIO_POLICY,
4815
4847
  strategy: strategyName,
@@ -4984,7 +5016,10 @@ var createStrategyRuntime = ({
4984
5016
  return invokeHook(stage, hook, params, errorContext);
4985
5017
  };
4986
5018
  const invokeGateHooks = async (stage, hook, params, errorContext = {}) => {
4987
- const failureResult = stage === "beforeEntryGate" ? { allow: false, reason: "ERROR" } : void 0;
5019
+ const failureResult = {
5020
+ allow: false,
5021
+ reason: "ERROR"
5022
+ };
4988
5023
  const projectResults = await invokeProjectHooks(
4989
5024
  stage,
4990
5025
  params,
@@ -5005,6 +5040,12 @@ var createStrategyRuntime = ({
5005
5040
  failureResult
5006
5041
  );
5007
5042
  };
5043
+ const authorizeClose = (decision, market, owner = strategyName) => invokeGateHooks(
5044
+ "beforeClosePosition",
5045
+ resolveManifest(owner)?.hooks?.beforeClosePosition,
5046
+ { ctx: getHookCtx(owner), market, decision },
5047
+ { decision, market }
5048
+ );
5008
5049
  const applyProjectAfterCoreDecisionHooks = async ({
5009
5050
  hookCtx,
5010
5051
  market,
@@ -5318,6 +5359,7 @@ var createStrategyRuntime = ({
5318
5359
  });
5319
5360
  };
5320
5361
  const runWithDecisionOverride = async (candle, btcCandle, options = {}) => {
5362
+ portfolioMarket = { candle, btcCandle };
5321
5363
  if (portfolioContext)
5322
5364
  await portfolioContext(
5323
5365
  env === "BACKTEST" || env === "PARITY" ? candle.timestamp : Date.now()
@@ -5424,15 +5466,10 @@ var createStrategyRuntime = ({
5424
5466
  if (!makeOrdersEnabled) {
5425
5467
  return decision.code;
5426
5468
  }
5427
- const closeGate = await invokeGateHooks(
5428
- "beforeClosePosition",
5429
- decisionManifest?.hooks?.beforeClosePosition,
5430
- {
5431
- ctx: decisionHookCtx,
5432
- market,
5433
- decision
5434
- },
5435
- { decision, market }
5469
+ const closeGate = await authorizeClose(
5470
+ decision,
5471
+ market,
5472
+ decisionHookCtx.strategyName
5436
5473
  );
5437
5474
  if (closeGate?.allow === false) {
5438
5475
  return closeGate.reason ? `CLOSE_BLOCKED_BY_HOOK:${closeGate.reason}` : "CLOSE_BLOCKED_BY_HOOK";
@@ -137,39 +137,72 @@ function createPortfolioCoordinator({
137
137
  );
138
138
  if (admitted.result) return { reason: admitted.result };
139
139
  let submitted = false;
140
+ let retry = 0;
141
+ let attemptId = entry.id;
142
+ let needsReservation = false;
140
143
  try {
141
144
  const value = await send(
142
145
  async (qty) => {
143
146
  if (!Number.isFinite(qty) || qty <= 0)
144
147
  throw new Error("Invalid submitted portfolio quantity");
145
- submitted = true;
146
- const update = await updateReservation(now, entry.id, true, qty);
148
+ if (needsReservation) {
149
+ attemptId = `${entry.id.slice(0, 30)}r${++retry}`;
150
+ const admission = await transact(
151
+ now,
152
+ (s, o) => {
153
+ const r = reservePortfolioEntry(policy, strategy, s, o, {
154
+ ...entry,
155
+ id: attemptId,
156
+ qty,
157
+ submitted: true,
158
+ oppositeHandled: false
159
+ });
160
+ return { state: r.state, result: r.reason };
161
+ },
162
+ true
163
+ );
164
+ if (admission.result) throw new Error(admission.result);
165
+ needsReservation = false;
166
+ submitted = true;
167
+ return attemptId;
168
+ }
169
+ const update = await updateReservation(now, attemptId, true, qty);
147
170
  if (!update.result)
148
171
  throw new Error("PORTFOLIO_SUBMISSION_NO_LONGER_ADMITTED");
172
+ submitted = true;
173
+ return attemptId;
149
174
  },
150
- async () => {
175
+ async (retrying = false) => {
151
176
  await transact(
152
177
  now,
153
178
  (s) => ({
154
179
  state: {
155
180
  ...s,
156
- reservations: s.reservations.filter((r) => r.id !== entry.id),
181
+ reservations: s.reservations.filter(
182
+ (r) => r.id !== attemptId
183
+ ),
157
184
  completedSignals: {
158
- ...s.completedSignals,
159
- [entry.signalId]: now
185
+ ...Object.fromEntries(
186
+ Object.entries(s.completedSignals).filter(
187
+ ([id]) => !retrying || id !== entry.signalId
188
+ )
189
+ ),
190
+ ...retrying ? {} : { [entry.signalId]: now }
160
191
  }
161
192
  },
162
193
  result: void 0
163
194
  }),
164
195
  true
165
196
  );
197
+ submitted = false;
198
+ needsReservation = retrying;
166
199
  }
167
200
  );
168
- if (!submitted) await updateReservation(now, entry.id, false);
201
+ if (!submitted) await updateReservation(now, attemptId, false);
169
202
  else await tick(now, true);
170
203
  return { value };
171
204
  } catch (error) {
172
- if (!submitted) await updateReservation(now, entry.id, false);
205
+ if (!submitted) await updateReservation(now, attemptId, false);
173
206
  throw error;
174
207
  }
175
208
  }