@tradejs/node 3.1.29-beta.290 → 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.
@@ -1,3 +1,7 @@
1
+ import {
2
+ createMemoryPortfolioStore,
3
+ createPortfolioCoordinator
4
+ } from "./chunk-NJEYNGRU.mjs";
1
5
  import {
2
6
  askAI,
3
7
  buildAiPayload,
@@ -7,7 +11,7 @@ import {
7
11
  resolveStrategyPolicyProfile,
8
12
  runAiPromptLocal,
9
13
  setStrategyRuntimeFactory
10
- } from "./chunk-UL5Z46JL.mjs";
14
+ } from "./chunk-PYJWXAYR.mjs";
11
15
  import {
12
16
  getTradejsProjectCwd,
13
17
  loadTradejsConfig
@@ -72,12 +76,14 @@ var validateEntryExecution = ({
72
76
  throw new EntrySubmissionValidationError(
73
77
  "ENTRY_ECONOMICS_INPUTS_UNAVAILABLE"
74
78
  );
75
- if (limits.minimumNetRiskRatio != null && netRiskRatio <= limits.minimumNetRiskRatio)
76
- throw new EntrySubmissionValidationError(
77
- "ENTRY_RR_BELOW_MIN_AT_SUBMISSION"
78
- );
79
+ const belowMinimumNetRiskRatio = limits.minimumNetRiskRatio != null && netRiskRatio <= limits.minimumNetRiskRatio;
79
80
  const riskExceeded = limits.maxLossValue != null && plannedLossValue > limits.maxLossValue * (1 + 1e-9);
80
- return { netRiskRatio, plannedLossValue, riskExceeded };
81
+ return {
82
+ netRiskRatio,
83
+ plannedLossValue,
84
+ riskExceeded,
85
+ belowMinimumNetRiskRatio
86
+ };
81
87
  };
82
88
 
83
89
  // src/strategy/entrySubmissionValidation.ts
@@ -209,6 +215,15 @@ var createEntrySubmissionValidation = ({
209
215
  takeProfitPrice: direction === "LONG" ? Math.max(...prices) : Math.min(...prices),
210
216
  limits
211
217
  });
218
+ if (economics.belowMinimumNetRiskRatio) {
219
+ logger.warn(
220
+ "entry submission warning: %s %s ENTRY_RR_BELOW_MIN_AT_SUBMISSION (RR %s, threshold %s)",
221
+ symbol,
222
+ direction,
223
+ economics.netRiskRatio,
224
+ limits.minimumNetRiskRatio
225
+ );
226
+ }
212
227
  onValidated?.(snapshot2, economics);
213
228
  };
214
229
  };
@@ -2278,6 +2293,10 @@ import { createRuntimeOrderLinkPrefix } from "@tradejs/core/trade";
2278
2293
  import { logger as logger5 } from "@tradejs/infra/logger";
2279
2294
  import {
2280
2295
  delKey,
2296
+ deleteRuntimeTradeReference,
2297
+ getDataStrict,
2298
+ setDataStrict,
2299
+ setHashJsonFieldStrict,
2281
2300
  getData,
2282
2301
  redisKeys,
2283
2302
  setData,
@@ -2444,6 +2463,7 @@ var getActiveRuntimeTrade = async (params) => {
2444
2463
  var markRuntimeTradeClosed = async (params) => {
2445
2464
  const {
2446
2465
  userName,
2466
+ expectedOrderId,
2447
2467
  symbol,
2448
2468
  strategy,
2449
2469
  exitPrice,
@@ -2456,13 +2476,15 @@ var markRuntimeTradeClosed = async (params) => {
2456
2476
  if (!userName) {
2457
2477
  return null;
2458
2478
  }
2459
- const existing = await getActiveRuntimeTrade({
2479
+ const existing = expectedOrderId ? await getDataStrict(
2480
+ redisKeys.runtimeTrade(userName, expectedOrderId)
2481
+ ) : await getActiveRuntimeTrade({
2460
2482
  userName,
2461
2483
  symbol,
2462
2484
  accountId,
2463
2485
  deploymentId
2464
2486
  });
2465
- if (!existing) {
2487
+ if (!existing || expectedOrderId && existing.orderId !== expectedOrderId) {
2466
2488
  return null;
2467
2489
  }
2468
2490
  const orderId = existing.orderId;
@@ -2476,7 +2498,7 @@ var markRuntimeTradeClosed = async (params) => {
2476
2498
  exitPrice: resolvedExitPrice,
2477
2499
  qty: existing.qty
2478
2500
  }) : existing.closedPnl ?? existing.currentPnl ?? null;
2479
- const next = {
2501
+ const next = expectedOrderId && existing.status === "closed" ? existing : {
2480
2502
  ...existing,
2481
2503
  status: "closed",
2482
2504
  currentPrice: resolvedExitPrice,
@@ -2491,22 +2513,33 @@ var markRuntimeTradeClosed = async (params) => {
2491
2513
  const closeDayKey = getRuntimeStorageDayKey(next.exitTimestamp);
2492
2514
  try {
2493
2515
  await Promise.all([
2494
- setData(redisKeys.runtimeTrade(userName, orderId), next, {
2495
- expire: TTL_1M
2496
- }),
2497
- setHashJsonField(
2516
+ (expectedOrderId ? setDataStrict : setData)(
2517
+ redisKeys.runtimeTrade(userName, orderId),
2518
+ next,
2519
+ {
2520
+ expire: TTL_1M
2521
+ }
2522
+ ),
2523
+ (expectedOrderId ? setHashJsonFieldStrict : setHashJsonField)(
2498
2524
  redisKeys.runtimeTradeBucket(userName, dayKey),
2499
2525
  orderId,
2500
2526
  next,
2501
2527
  { expire: TTL_1M }
2502
2528
  ),
2503
- setHashJsonField(
2529
+ (expectedOrderId ? setHashJsonFieldStrict : setHashJsonField)(
2504
2530
  redisKeys.runtimeClosedTradeBucket(userName, closeDayKey),
2505
2531
  orderId,
2506
2532
  next,
2507
2533
  { expire: TTL_1M }
2508
2534
  ),
2509
- delKey(
2535
+ expectedOrderId ? deleteRuntimeTradeReference(
2536
+ redisKeys.runtimeActiveTrade(
2537
+ userName,
2538
+ symbol,
2539
+ deploymentId ?? accountId
2540
+ ),
2541
+ expectedOrderId
2542
+ ) : delKey(
2510
2543
  redisKeys.runtimeActiveTrade(
2511
2544
  userName,
2512
2545
  symbol,
@@ -2515,6 +2548,7 @@ var markRuntimeTradeClosed = async (params) => {
2515
2548
  )
2516
2549
  ]);
2517
2550
  } catch (error) {
2551
+ if (expectedOrderId) throw error;
2518
2552
  logger5.error(
2519
2553
  "runtime trade close journal failed: %s %s",
2520
2554
  symbol,
@@ -2524,6 +2558,93 @@ var markRuntimeTradeClosed = async (params) => {
2524
2558
  return next;
2525
2559
  };
2526
2560
 
2561
+ // src/strategyHelpers/tickerProfiles.ts
2562
+ import { readTickerProfiles } from "@tradejs/infra/tickerProfiles";
2563
+ import {
2564
+ isTickerProfileSnapshot,
2565
+ tickerProfileMonth
2566
+ } from "@tradejs/core/tickerProfiles";
2567
+ var snapshots = /* @__PURE__ */ new Map();
2568
+ var resolveTickerProfileSource = (strategy) => {
2569
+ const declared = getStrategyManifest(strategy)?.contextRequirements?.tickerProfiles;
2570
+ if (declared) return declared;
2571
+ const provider = process.env.TICKER_PROFILES_PROVIDER?.trim();
2572
+ const universeId = process.env.TICKER_PROFILES_UNIVERSE?.trim();
2573
+ if (!provider && !universeId) return void 0;
2574
+ if (!provider || !universeId)
2575
+ throw new Error(
2576
+ "Set both TICKER_PROFILES_PROVIDER and TICKER_PROFILES_UNIVERSE"
2577
+ );
2578
+ return { provider, universeId };
2579
+ };
2580
+ var loadTickerProfileContext = async ({
2581
+ projectRoot,
2582
+ provider,
2583
+ universeId,
2584
+ symbol,
2585
+ timestamp
2586
+ }) => {
2587
+ const asOf = tickerProfileMonth(timestamp), key = JSON.stringify([projectRoot, provider, universeId, asOf]);
2588
+ let cached = snapshots.get(key);
2589
+ if (!cached || cached.expires < Date.now()) {
2590
+ const value = readTickerProfiles(projectRoot, provider, universeId, asOf).then((envelope) => {
2591
+ if (!envelope) return null;
2592
+ const s = envelope.snapshot;
2593
+ if (!isTickerProfileSnapshot(s) || s.asOf !== asOf || s.provider !== provider || s.universeId !== universeId)
2594
+ throw new Error("Ticker profile identity/schema mismatch");
2595
+ const entry = snapshots.get(key);
2596
+ if (entry) entry.expires = Infinity;
2597
+ return new Map(
2598
+ s.profiles.map((p) => [
2599
+ p.symbol,
2600
+ Object.freeze({
2601
+ ...p,
2602
+ definition: s.definition,
2603
+ provider,
2604
+ universeId,
2605
+ snapshotId: envelope.snapshotId
2606
+ })
2607
+ ])
2608
+ );
2609
+ }).catch((error) => {
2610
+ snapshots.delete(key);
2611
+ throw error;
2612
+ });
2613
+ cached = { expires: Date.now() + 6e4, value };
2614
+ snapshots.set(key, cached);
2615
+ if (snapshots.size > 24) {
2616
+ const oldest = snapshots.keys().next().value;
2617
+ if (oldest) snapshots.delete(oldest);
2618
+ }
2619
+ }
2620
+ return (await cached.value)?.get(symbol);
2621
+ };
2622
+ var withTickerProfile = (baseContext, profile) => Object.defineProperty(
2623
+ Object.create(
2624
+ Object.getPrototypeOf(baseContext),
2625
+ Object.getOwnPropertyDescriptors(baseContext)
2626
+ ),
2627
+ "tickerProfile",
2628
+ { enumerable: true, configurable: true, value: profile }
2629
+ );
2630
+ var enrichSignalWithTickerProfile = async (signal) => {
2631
+ const source = resolveTickerProfileSource(signal.strategy);
2632
+ if (!source || signal.universe === "tradfi" || !signal.symbol.endsWith("USDT"))
2633
+ return;
2634
+ const profile = await loadTickerProfileContext({
2635
+ ...source,
2636
+ projectRoot: getTradejsProjectCwd(),
2637
+ symbol: signal.symbol,
2638
+ timestamp: signal.timestamp
2639
+ });
2640
+ if (profile) {
2641
+ signal.additionalIndicators = {
2642
+ ...signal.additionalIndicators,
2643
+ tickerProfile: profile
2644
+ };
2645
+ }
2646
+ };
2647
+
2527
2648
  // src/strategyHelpers/marketContextStages.ts
2528
2649
  import { logger as logger7 } from "@tradejs/infra/logger";
2529
2650
 
@@ -2884,6 +3005,7 @@ var enrichSignalWithMarketContextStages = async ({
2884
3005
  })
2885
3006
  });
2886
3007
  }
3008
+ await enrichSignalWithTickerProfile(signal);
2887
3009
  const results = [];
2888
3010
  for (const stage of stages) {
2889
3011
  if (abortSignal?.aborted) break;
@@ -2931,12 +3053,12 @@ var findReplayAiAnalysis = ({
2931
3053
  direction,
2932
3054
  ai
2933
3055
  }) => {
2934
- const snapshots = ai?.replayAnalyses;
2935
- if (!Array.isArray(snapshots) || !snapshots.length) {
3056
+ const snapshots2 = ai?.replayAnalyses;
3057
+ if (!Array.isArray(snapshots2) || !snapshots2.length) {
2936
3058
  return void 0;
2937
3059
  }
2938
3060
  let best = null;
2939
- for (const snapshot2 of snapshots) {
3061
+ for (const snapshot2 of snapshots2) {
2940
3062
  if (snapshot2.symbol !== signal.symbol || snapshot2.direction !== direction || snapshot2.strategy && snapshot2.strategy !== signal.strategy) {
2941
3063
  continue;
2942
3064
  }
@@ -3109,11 +3231,13 @@ var executeEntryOrder = async ({
3109
3231
  beforePlaceOrder,
3110
3232
  recordRuntimeTrade = true,
3111
3233
  leverage,
3112
- executionLimits
3234
+ executionLimits,
3235
+ markPortfolioSubmitted,
3236
+ rejectPortfolioSubmission
3113
3237
  }) => {
3114
3238
  await beforePlaceOrder?.();
3115
3239
  const previousPosition = positionIntent === "increase" ? await connector.getPosition(symbol) : null;
3116
- const orderId = signal.orderId || createRuntimeOrderId(signal.strategy);
3240
+ let orderId = signal.orderId || createRuntimeOrderId(signal.strategy);
3117
3241
  const signalTimestamp = signal.timestamp;
3118
3242
  const signalClosePrice = currentPrice;
3119
3243
  signal.orderId = orderId;
@@ -3182,9 +3306,16 @@ var executeEntryOrder = async ({
3182
3306
  ...typeof leverage === "number" && Number.isFinite(leverage) ? { leverage } : {},
3183
3307
  orderId,
3184
3308
  signal,
3185
- ...beforeSubmit ? { beforeSubmit } : {}
3309
+ ...rejectPortfolioSubmission ? { onRejected: rejectPortfolioSubmission } : {},
3310
+ ...beforeSubmit || markPortfolioSubmitted ? {
3311
+ beforeSubmit: async (context) => {
3312
+ await beforeSubmit?.(context);
3313
+ return markPortfolioSubmitted?.(context.qty);
3314
+ }
3315
+ } : {}
3186
3316
  });
3187
3317
  const orderAckTime = Date.now();
3318
+ if (markPortfolioSubmitted && signal.orderId) orderId = signal.orderId;
3188
3319
  const placedQty = typeof signal.orderQty === "number" && Number.isFinite(signal.orderQty) && signal.orderQty > 0 ? signal.orderQty : qty;
3189
3320
  const currentPosition = await connector.getPosition(symbol);
3190
3321
  const fillTime = Date.now();
@@ -3457,6 +3588,202 @@ var evaluateResearchEntryPolicy = async (policy, signal, onDecision) => {
3457
3588
  return allowed;
3458
3589
  };
3459
3590
 
3591
+ // src/strategy/portfolioRuntime.ts
3592
+ import { createHash as createHash3 } from "crypto";
3593
+ import {
3594
+ parsePortfolioPolicy,
3595
+ assertPortfolioState,
3596
+ portfolioFingerprint
3597
+ } from "@tradejs/core/portfolio";
3598
+ import { portfolioStateStorage } from "@tradejs/infra/redis";
3599
+ var coordinators = /* @__PURE__ */ new WeakMap();
3600
+ async function observeRuntimePortfolio(connector, binding, asOf, reservations, observedNow = Date.now) {
3601
+ if (!connector.getOpenPositionPnl || !connector.getEntryExecutions)
3602
+ throw new Error(
3603
+ "Portfolio policies require position marks and entry executions"
3604
+ );
3605
+ const orders = await Promise.all(
3606
+ reservations.map(async (r) => {
3607
+ if (r.submitted && connector.getEntryOrderObservation) {
3608
+ const fact = await connector.getEntryOrderObservation({
3609
+ symbol: r.symbol,
3610
+ orderId: r.id
3611
+ });
3612
+ if (fact) return fact;
3613
+ }
3614
+ const executions = r.submitted ? await connector.getEntryExecutions({
3615
+ symbol: r.symbol,
3616
+ startTime: r.timestamp,
3617
+ endTime: asOf
3618
+ }) : [];
3619
+ const filledQty = executions.filter(
3620
+ (e) => (e.orderLinkId === r.id || e.orderId === r.id) && e.direction === r.direction
3621
+ ).reduce((sum, e) => sum + e.qty, 0);
3622
+ return {
3623
+ id: r.id,
3624
+ status: filledQty >= r.qty ? "filled" : filledQty > 0 ? "partial" : "pending",
3625
+ filledQty,
3626
+ positionId: r.id
3627
+ };
3628
+ })
3629
+ );
3630
+ const snapshots2 = await connector.getOpenPositionPnl();
3631
+ const positions = await Promise.all(
3632
+ snapshots2.map(async (p) => {
3633
+ const reserved = reservations.find(
3634
+ (r) => r.symbol === p.symbol && r.direction === p.direction && orders.some(
3635
+ (o) => o.id === r.id && o.filledQty > 0 && p.qty <= o.filledQty * (1 + 1e-8)
3636
+ )
3637
+ );
3638
+ const trade = await getActiveRuntimeTrade({
3639
+ ...binding,
3640
+ symbol: p.symbol
3641
+ });
3642
+ const owner = trade && trade.direction === p.direction && p.qty <= trade.qty * (1 + 1e-8) && Math.abs(p.price - (trade.actualEntryPrice ?? trade.entryPrice)) <= p.price * 1e-8 ? trade : null;
3643
+ return {
3644
+ id: reserved?.id ?? owner?.orderId ?? `unknown:${p.symbol}:${p.direction}`,
3645
+ strategy: reserved?.strategy ?? owner?.strategy ?? null,
3646
+ symbol: p.symbol,
3647
+ direction: p.direction,
3648
+ qty: p.qty,
3649
+ entryPrice: p.price,
3650
+ markPrice: p.currentPrice,
3651
+ markedAt: p.observedAt ?? null,
3652
+ stopLossPrice: p.slPrice ?? p.stopLossPrice ?? null
3653
+ };
3654
+ })
3655
+ );
3656
+ return { asOf: Math.max(asOf, observedNow()), positions, orders };
3657
+ }
3658
+ function persistentStore(key) {
3659
+ const raw = /* @__PURE__ */ new WeakMap();
3660
+ return {
3661
+ async read() {
3662
+ const value = await portfolioStateStorage.read(key);
3663
+ if (value === null) return null;
3664
+ const state = JSON.parse(value);
3665
+ assertPortfolioState(state);
3666
+ raw.set(state, value);
3667
+ return state;
3668
+ },
3669
+ async compare(previous, next) {
3670
+ const expected = previous === null ? null : raw.get(previous);
3671
+ if (expected === void 0)
3672
+ throw new Error("Portfolio CAS requires a storage snapshot");
3673
+ return portfolioStateStorage.compare(key, expected, JSON.stringify(next));
3674
+ }
3675
+ };
3676
+ }
3677
+ function getPortfolioRuntime({
3678
+ value,
3679
+ connectorName,
3680
+ strategy,
3681
+ connector,
3682
+ env,
3683
+ userName,
3684
+ accountId,
3685
+ deploymentId,
3686
+ symbol,
3687
+ canClose
3688
+ }) {
3689
+ const policy = parsePortfolioPolicy(value);
3690
+ if (!policy) return void 0;
3691
+ const replay = (env === "BACKTEST" || env === "PARITY") && Reflect.get(connector, "__tradejsReplayConnector") === true;
3692
+ if ((env === "BACKTEST" || env === "PARITY") && (!replay || !connector.getPortfolioObservation))
3693
+ throw new Error(
3694
+ "Portfolio policies require shared portfolio replay; isolated symbol backtests are unsupported"
3695
+ );
3696
+ if (!replay && (!userName || !accountId || !deploymentId || !connector.getOpenPositionPnl || !connector.getEntryExecutions))
3697
+ throw new Error(
3698
+ "Portfolio runtime requires exact user/account/deployment binding, marks and executions"
3699
+ );
3700
+ const binding = {
3701
+ userName,
3702
+ accountId,
3703
+ deploymentId
3704
+ };
3705
+ const scope = policy.scope === "strategy" ? strategy : policy.scope === "account" ? "account" : [...policy.scope.strategies].sort();
3706
+ const key = createHash3("sha256").update(JSON.stringify([binding, connectorName, connector.universe, scope])).digest("hex");
3707
+ const fingerprint3 = portfolioFingerprint(policy, strategy);
3708
+ const cacheKey = key;
3709
+ let registry = coordinators.get(connector);
3710
+ if (!registry) {
3711
+ registry = /* @__PURE__ */ new Map();
3712
+ coordinators.set(connector, registry);
3713
+ }
3714
+ const existing = registry.get(cacheKey);
3715
+ if (existing) {
3716
+ if (existing.fingerprint !== fingerprint3)
3717
+ throw new Error("Conflicting portfolio configuration for the same scope");
3718
+ if (symbol && canClose)
3719
+ existing.permissions.set(JSON.stringify([strategy, symbol]), canClose);
3720
+ return existing.coordinator;
3721
+ }
3722
+ const permissions = /* @__PURE__ */ new Map();
3723
+ if (symbol && canClose)
3724
+ permissions.set(JSON.stringify([strategy, symbol]), canClose);
3725
+ const coordinator = createPortfolioCoordinator({
3726
+ policy,
3727
+ strategy,
3728
+ store: replay ? createMemoryPortfolioStore() : persistentStore(key),
3729
+ execution: {
3730
+ async confirmClosed(intent, asOf) {
3731
+ if (replay) return;
3732
+ const position = intent.position;
3733
+ await markRuntimeTradeClosed({
3734
+ ...binding,
3735
+ strategy: position.strategy,
3736
+ expectedOrderId: position.id,
3737
+ symbol: position.symbol,
3738
+ exitPrice: position.markPrice,
3739
+ exitTimestamp: asOf,
3740
+ exitType: "exit"
3741
+ });
3742
+ },
3743
+ observe: (asOf, reservations) => replay ? connector.getPortfolioObservation(asOf, reservations) : observeRuntimePortfolio(connector, binding, asOf, reservations),
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;
3749
+ if (!replay) {
3750
+ const trade = await getActiveRuntimeTrade({
3751
+ ...binding,
3752
+ symbol: position.symbol
3753
+ });
3754
+ if (!trade || trade.orderId !== position.id)
3755
+ throw new Error("Portfolio close ownership changed");
3756
+ }
3757
+ await connector.closePosition({
3758
+ symbol: position.symbol,
3759
+ direction: position.direction,
3760
+ price: position.markPrice,
3761
+ timestamp: asOf,
3762
+ signal: {
3763
+ signalId: `${position.id}:${intent.reason}`,
3764
+ strategy: position.strategy,
3765
+ symbol: position.symbol,
3766
+ direction: position.direction,
3767
+ interval: "1",
3768
+ timestamp: asOf,
3769
+ prices: {
3770
+ riskRatio: 0,
3771
+ currentPrice: position.markPrice,
3772
+ takeProfitPrice: position.markPrice,
3773
+ stopLossPrice: position.markPrice
3774
+ },
3775
+ figures: {},
3776
+ indicators: {},
3777
+ additionalIndicators: { portfolioExitReason: intent.reason }
3778
+ }
3779
+ });
3780
+ }
3781
+ }
3782
+ });
3783
+ registry.set(cacheKey, { fingerprint: fingerprint3, coordinator, permissions });
3784
+ return coordinator;
3785
+ }
3786
+
3460
3787
  // src/strategyRuntime.ts
3461
3788
  import { parseJevConfig } from "@tradejs/core/jev";
3462
3789
  import {
@@ -4192,7 +4519,7 @@ var handleProtectDecision = async ({
4192
4519
  }
4193
4520
  return decision.code;
4194
4521
  };
4195
- var executeEntryDecision = async ({
4522
+ var executeAdmittedEntryDecision = async ({
4196
4523
  connector,
4197
4524
  symbol,
4198
4525
  decision,
@@ -4206,7 +4533,9 @@ var executeEntryDecision = async ({
4206
4533
  ai,
4207
4534
  recordRuntimeJournal,
4208
4535
  invokeStageHooks,
4209
- notifyRuntimeError
4536
+ notifyRuntimeError,
4537
+ markPortfolioSubmitted,
4538
+ rejectPortfolioSubmission
4210
4539
  }) => {
4211
4540
  const signal = decision.signal;
4212
4541
  const beforePlaceOrder = async () => {
@@ -4253,6 +4582,8 @@ var executeEntryDecision = async ({
4253
4582
  ...Number.isFinite(Number(hookCtx.strategyConfig.LEVERAGE)) ? { leverage: Number(hookCtx.strategyConfig.LEVERAGE) } : {},
4254
4583
  signal,
4255
4584
  beforePlaceOrder,
4585
+ markPortfolioSubmitted,
4586
+ rejectPortfolioSubmission,
4256
4587
  recordRuntimeTrade: recordRuntimeJournal,
4257
4588
  executionLimits: resolveEntryExecutionLimits(
4258
4589
  hookCtx.strategyConfig,
@@ -4393,6 +4724,37 @@ var executeEntryDecision = async ({
4393
4724
  }
4394
4725
  return signal ?? decision.code;
4395
4726
  };
4727
+ async function executeEntryDecision(params) {
4728
+ if (!params.portfolio) return executeAdmittedEntryDecision(params);
4729
+ const { decision, portfolio } = params;
4730
+ const signal = decision.signal;
4731
+ if (!signal || decision.orderPlan.positionIntent === "increase")
4732
+ return "PORTFOLIO_REQUIRES_NEW_SIGNAL_POSITION";
4733
+ signal.orderId ??= createRuntimeOrderId(signal.strategy);
4734
+ const result = await portfolio.submit(
4735
+ params.portfolioTime ?? decision.entryContext.timestamp,
4736
+ {
4737
+ id: signal.orderId,
4738
+ signalId: signal.signalId,
4739
+ strategy: signal.strategy,
4740
+ symbol: params.symbol,
4741
+ direction: decision.entryContext.direction,
4742
+ qty: decision.orderPlan.qty,
4743
+ timestamp: decision.entryContext.timestamp
4744
+ },
4745
+ (markPortfolioSubmitted, rejectPortfolioSubmission) => executeAdmittedEntryDecision({
4746
+ ...params,
4747
+ markPortfolioSubmitted,
4748
+ rejectPortfolioSubmission
4749
+ })
4750
+ );
4751
+ if (result.reason) {
4752
+ signal.orderStatus = "failed";
4753
+ signal.orderFailureReason = result.reason;
4754
+ return signal;
4755
+ }
4756
+ return result.value;
4757
+ }
4396
4758
 
4397
4759
  // src/strategyRuntime.ts
4398
4760
  var cloneWithPropertyDescriptors = (value) => Object.create(
@@ -4420,6 +4782,7 @@ var createStrategyRuntime = ({
4420
4782
  };
4421
4783
  const creator = async ({
4422
4784
  userName,
4785
+ connectorName,
4423
4786
  config: baseConfig,
4424
4787
  symbol,
4425
4788
  universe: requestedUniverse,
@@ -4458,6 +4821,37 @@ var createStrategyRuntime = ({
4458
4821
  const projectConfig = await loadTradejsConfig(projectRoot);
4459
4822
  const projectHooks = projectConfig.hooks;
4460
4823
  const env = String(config.ENV ?? "BACKTEST");
4824
+ let portfolioMarket;
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
+ },
4845
+ connectorName,
4846
+ value: baseConfig?.PORTFOLIO_POLICY ?? config.PORTFOLIO_POLICY,
4847
+ strategy: strategyName,
4848
+ connector,
4849
+ env,
4850
+ userName,
4851
+ accountId,
4852
+ deploymentId
4853
+ });
4854
+ const portfolioContext = portfolio ? (now2) => isRuntimeOrderExecutionEnabled({ config, env, connector }) ? portfolio.tick(now2) : portfolio.context(now2) : void 0;
4461
4855
  const researchPolicyValue = baseConfig?.RESEARCH_ENTRY_POLICY ?? config.RESEARCH_ENTRY_POLICY;
4462
4856
  const researchEntryPolicy = researchPolicyValue == null ? void 0 : parseResearchEntryPolicy(researchPolicyValue);
4463
4857
  if (researchEntryPolicy && (env !== "BACKTEST" || Reflect.get(connector, "__tradejsTestConnector") !== true))
@@ -4622,7 +5016,10 @@ var createStrategyRuntime = ({
4622
5016
  return invokeHook(stage, hook, params, errorContext);
4623
5017
  };
4624
5018
  const invokeGateHooks = async (stage, hook, params, errorContext = {}) => {
4625
- const failureResult = stage === "beforeEntryGate" ? { allow: false, reason: "ERROR" } : void 0;
5019
+ const failureResult = {
5020
+ allow: false,
5021
+ reason: "ERROR"
5022
+ };
4626
5023
  const projectResults = await invokeProjectHooks(
4627
5024
  stage,
4628
5025
  params,
@@ -4643,6 +5040,12 @@ var createStrategyRuntime = ({
4643
5040
  failureResult
4644
5041
  );
4645
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
+ );
4646
5049
  const applyProjectAfterCoreDecisionHooks = async ({
4647
5050
  hookCtx,
4648
5051
  market,
@@ -4756,6 +5159,20 @@ var createStrategyRuntime = ({
4756
5159
  interval: decisionInterval
4757
5160
  }) => {
4758
5161
  if (!baseContext) return void 0;
5162
+ const profileSource = strategyManifest?.contextRequirements?.tickerProfiles;
5163
+ if (profileSource) {
5164
+ const profile = await loadTickerProfileContext({
5165
+ ...profileSource,
5166
+ projectRoot,
5167
+ symbol: decisionSymbol,
5168
+ timestamp: candle.timestamp
5169
+ });
5170
+ if (!profile)
5171
+ throw new Error(
5172
+ "Required ticker profile is unavailable at decision time"
5173
+ );
5174
+ baseContext = withTickerProfile(baseContext, profile);
5175
+ }
4759
5176
  if (!coreContextRequirements.has("hyperliquidWhales")) {
4760
5177
  return baseContext;
4761
5178
  }
@@ -4923,6 +5340,8 @@ var createStrategyRuntime = ({
4923
5340
  runtime: pending.runtime
4924
5341
  });
4925
5342
  return executeEntryDecision({
5343
+ portfolio,
5344
+ portfolioTime: env === "BACKTEST" || env === "PARITY" ? candle.timestamp : Date.now(),
4926
5345
  connector,
4927
5346
  symbol,
4928
5347
  decision: pending.decision,
@@ -4940,6 +5359,11 @@ var createStrategyRuntime = ({
4940
5359
  });
4941
5360
  };
4942
5361
  const runWithDecisionOverride = async (candle, btcCandle, options = {}) => {
5362
+ portfolioMarket = { candle, btcCandle };
5363
+ if (portfolioContext)
5364
+ await portfolioContext(
5365
+ env === "BACKTEST" || env === "PARITY" ? candle.timestamp : Date.now()
5366
+ );
4943
5367
  appendCurrentMarketData(candle, btcCandle, options.ethCandle);
4944
5368
  const ethCandle = resolveEthCandle(candle, options.ethCandle);
4945
5369
  indicatorsState.setCurrentBar(candle, btcCandle, ethCandle);
@@ -5042,15 +5466,10 @@ var createStrategyRuntime = ({
5042
5466
  if (!makeOrdersEnabled) {
5043
5467
  return decision.code;
5044
5468
  }
5045
- const closeGate = await invokeGateHooks(
5046
- "beforeClosePosition",
5047
- decisionManifest?.hooks?.beforeClosePosition,
5048
- {
5049
- ctx: decisionHookCtx,
5050
- market,
5051
- decision
5052
- },
5053
- { decision, market }
5469
+ const closeGate = await authorizeClose(
5470
+ decision,
5471
+ market,
5472
+ decisionHookCtx.strategyName
5054
5473
  );
5055
5474
  if (closeGate?.allow === false) {
5056
5475
  return closeGate.reason ? `CLOSE_BLOCKED_BY_HOOK:${closeGate.reason}` : "CLOSE_BLOCKED_BY_HOOK";
@@ -5110,6 +5529,13 @@ var createStrategyRuntime = ({
5110
5529
  });
5111
5530
  const signal = decision.signal;
5112
5531
  if (signal) {
5532
+ if (portfolio)
5533
+ signal.additionalIndicators = {
5534
+ ...signal.additionalIndicators,
5535
+ portfolioContext: await portfolioContext(
5536
+ env === "BACKTEST" || env === "PARITY" ? decision.entryContext.timestamp : Date.now()
5537
+ )
5538
+ };
5113
5539
  if (runtimeLineage) signal.runtimeLineage = runtimeLineage;
5114
5540
  if (universe) signal.universe = universe;
5115
5541
  if (assetClass) signal.assetClass = assetClass;
@@ -5349,6 +5775,8 @@ var createStrategyRuntime = ({
5349
5775
  });
5350
5776
  }
5351
5777
  return executeEntryDecision({
5778
+ portfolio,
5779
+ portfolioTime: env === "BACKTEST" || env === "PARITY" ? candle.timestamp : Date.now(),
5352
5780
  connector,
5353
5781
  symbol,
5354
5782
  decision,