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

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.mjs CHANGED
@@ -10,11 +10,12 @@ import {
10
10
  evaluateResearchEntryPolicy,
11
11
  matchesResearchEntryRules,
12
12
  parseResearchEntryPolicy
13
- } from "./chunk-PB2KHBGH.mjs";
13
+ } from "./chunk-HRON2EAI.mjs";
14
+ import "./chunk-UUNEWAT4.mjs";
14
15
  import {
15
16
  buildAiPayload,
16
17
  getStrategyCreator
17
- } from "./chunk-UL5Z46JL.mjs";
18
+ } from "./chunk-PYJWXAYR.mjs";
18
19
  import {
19
20
  getTradejsProjectCwd
20
21
  } from "./chunk-LZDXRXIU.mjs";
@@ -1,3 +1,7 @@
1
+ import {
2
+ createMemoryPortfolioStore,
3
+ createPortfolioCoordinator
4
+ } from "./chunk-UUNEWAT4.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,7 +3231,9 @@ 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;
@@ -3182,7 +3306,13 @@ 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
+ await markPortfolioSubmitted?.(context.qty);
3314
+ }
3315
+ } : {}
3186
3316
  });
3187
3317
  const orderAckTime = Date.now();
3188
3318
  const placedQty = typeof signal.orderQty === "number" && Number.isFinite(signal.orderQty) && signal.orderQty > 0 ? signal.orderQty : qty;
@@ -3457,6 +3587,191 @@ var evaluateResearchEntryPolicy = async (policy, signal, onDecision) => {
3457
3587
  return allowed;
3458
3588
  };
3459
3589
 
3590
+ // src/strategy/portfolioRuntime.ts
3591
+ import { createHash as createHash3 } from "crypto";
3592
+ import {
3593
+ parsePortfolioPolicy,
3594
+ assertPortfolioState,
3595
+ portfolioFingerprint
3596
+ } from "@tradejs/core/portfolio";
3597
+ import { portfolioStateStorage } from "@tradejs/infra/redis";
3598
+ var coordinators = /* @__PURE__ */ new WeakMap();
3599
+ async function observeRuntimePortfolio(connector, binding, asOf, reservations, observedNow = Date.now) {
3600
+ if (!connector.getOpenPositionPnl || !connector.getEntryExecutions)
3601
+ throw new Error(
3602
+ "Portfolio policies require position marks and entry executions"
3603
+ );
3604
+ const orders = await Promise.all(
3605
+ reservations.map(async (r) => {
3606
+ if (r.submitted && connector.getEntryOrderObservation) {
3607
+ const fact = await connector.getEntryOrderObservation({
3608
+ symbol: r.symbol,
3609
+ orderId: r.id
3610
+ });
3611
+ if (fact) return fact;
3612
+ }
3613
+ const executions = r.submitted ? await connector.getEntryExecutions({
3614
+ symbol: r.symbol,
3615
+ startTime: r.timestamp,
3616
+ endTime: asOf
3617
+ }) : [];
3618
+ const filledQty = executions.filter(
3619
+ (e) => (e.orderLinkId === r.id || e.orderId === r.id) && e.direction === r.direction
3620
+ ).reduce((sum, e) => sum + e.qty, 0);
3621
+ return {
3622
+ id: r.id,
3623
+ status: filledQty >= r.qty ? "filled" : filledQty > 0 ? "partial" : "pending",
3624
+ filledQty,
3625
+ positionId: r.id
3626
+ };
3627
+ })
3628
+ );
3629
+ const snapshots2 = await connector.getOpenPositionPnl();
3630
+ const positions = await Promise.all(
3631
+ snapshots2.map(async (p) => {
3632
+ const reserved = reservations.find(
3633
+ (r) => r.symbol === p.symbol && r.direction === p.direction && orders.some(
3634
+ (o) => o.id === r.id && o.filledQty > 0 && p.qty <= o.filledQty * (1 + 1e-8)
3635
+ )
3636
+ );
3637
+ const trade = await getActiveRuntimeTrade({
3638
+ ...binding,
3639
+ symbol: p.symbol
3640
+ });
3641
+ 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;
3642
+ return {
3643
+ id: reserved?.id ?? owner?.orderId ?? `unknown:${p.symbol}:${p.direction}`,
3644
+ strategy: reserved?.strategy ?? owner?.strategy ?? null,
3645
+ symbol: p.symbol,
3646
+ direction: p.direction,
3647
+ qty: p.qty,
3648
+ entryPrice: p.price,
3649
+ markPrice: p.currentPrice,
3650
+ markedAt: p.observedAt ?? null,
3651
+ stopLossPrice: p.slPrice ?? p.stopLossPrice ?? null
3652
+ };
3653
+ })
3654
+ );
3655
+ return { asOf: Math.max(asOf, observedNow()), positions, orders };
3656
+ }
3657
+ function persistentStore(key) {
3658
+ const raw = /* @__PURE__ */ new WeakMap();
3659
+ return {
3660
+ async read() {
3661
+ const value = await portfolioStateStorage.read(key);
3662
+ if (value === null) return null;
3663
+ const state = JSON.parse(value);
3664
+ assertPortfolioState(state);
3665
+ raw.set(state, value);
3666
+ return state;
3667
+ },
3668
+ async compare(previous, next) {
3669
+ const expected = previous === null ? null : raw.get(previous);
3670
+ if (expected === void 0)
3671
+ throw new Error("Portfolio CAS requires a storage snapshot");
3672
+ return portfolioStateStorage.compare(key, expected, JSON.stringify(next));
3673
+ }
3674
+ };
3675
+ }
3676
+ function getPortfolioRuntime({
3677
+ value,
3678
+ connectorName,
3679
+ strategy,
3680
+ connector,
3681
+ env,
3682
+ userName,
3683
+ accountId,
3684
+ deploymentId
3685
+ }) {
3686
+ const policy = parsePortfolioPolicy(value);
3687
+ if (!policy) return void 0;
3688
+ const replay = (env === "BACKTEST" || env === "PARITY") && Reflect.get(connector, "__tradejsReplayConnector") === true;
3689
+ if ((env === "BACKTEST" || env === "PARITY") && (!replay || !connector.getPortfolioObservation))
3690
+ throw new Error(
3691
+ "Portfolio policies require shared portfolio replay; isolated symbol backtests are unsupported"
3692
+ );
3693
+ if (!replay && (!userName || !accountId || !deploymentId || !connector.getOpenPositionPnl || !connector.getEntryExecutions))
3694
+ throw new Error(
3695
+ "Portfolio runtime requires exact user/account/deployment binding, marks and executions"
3696
+ );
3697
+ const binding = {
3698
+ userName,
3699
+ accountId,
3700
+ deploymentId
3701
+ };
3702
+ const scope = policy.scope === "strategy" ? strategy : policy.scope === "account" ? "account" : [...policy.scope.strategies].sort();
3703
+ const key = createHash3("sha256").update(JSON.stringify([binding, connectorName, connector.universe, scope])).digest("hex");
3704
+ const fingerprint3 = portfolioFingerprint(policy, strategy);
3705
+ const cacheKey = key;
3706
+ let registry = coordinators.get(connector);
3707
+ if (!registry) {
3708
+ registry = /* @__PURE__ */ new Map();
3709
+ coordinators.set(connector, registry);
3710
+ }
3711
+ const existing = registry.get(cacheKey);
3712
+ if (existing) {
3713
+ if (existing.fingerprint !== fingerprint3)
3714
+ throw new Error("Conflicting portfolio configuration for the same scope");
3715
+ return existing.coordinator;
3716
+ }
3717
+ const coordinator = createPortfolioCoordinator({
3718
+ policy,
3719
+ strategy,
3720
+ store: replay ? createMemoryPortfolioStore() : persistentStore(key),
3721
+ execution: {
3722
+ async confirmClosed(intent, asOf) {
3723
+ if (replay) return;
3724
+ const position = intent.position;
3725
+ await markRuntimeTradeClosed({
3726
+ ...binding,
3727
+ strategy: position.strategy,
3728
+ expectedOrderId: position.id,
3729
+ symbol: position.symbol,
3730
+ exitPrice: position.markPrice,
3731
+ exitTimestamp: asOf,
3732
+ exitType: "exit"
3733
+ });
3734
+ },
3735
+ observe: (asOf, reservations) => replay ? connector.getPortfolioObservation(asOf, reservations) : observeRuntimePortfolio(connector, binding, asOf, reservations),
3736
+ async close(position, intent, asOf) {
3737
+ if (!replay) {
3738
+ const trade = await getActiveRuntimeTrade({
3739
+ ...binding,
3740
+ symbol: position.symbol
3741
+ });
3742
+ if (!trade || trade.orderId !== position.id)
3743
+ throw new Error("Portfolio close ownership changed");
3744
+ }
3745
+ await connector.closePosition({
3746
+ symbol: position.symbol,
3747
+ direction: position.direction,
3748
+ price: position.markPrice,
3749
+ timestamp: asOf,
3750
+ signal: {
3751
+ signalId: `${position.id}:${intent.reason}`,
3752
+ strategy: position.strategy,
3753
+ symbol: position.symbol,
3754
+ direction: position.direction,
3755
+ interval: "1",
3756
+ timestamp: asOf,
3757
+ prices: {
3758
+ riskRatio: 0,
3759
+ currentPrice: position.markPrice,
3760
+ takeProfitPrice: position.markPrice,
3761
+ stopLossPrice: position.markPrice
3762
+ },
3763
+ figures: {},
3764
+ indicators: {},
3765
+ additionalIndicators: { portfolioExitReason: intent.reason }
3766
+ }
3767
+ });
3768
+ }
3769
+ }
3770
+ });
3771
+ registry.set(cacheKey, { fingerprint: fingerprint3, coordinator });
3772
+ return coordinator;
3773
+ }
3774
+
3460
3775
  // src/strategyRuntime.ts
3461
3776
  import { parseJevConfig } from "@tradejs/core/jev";
3462
3777
  import {
@@ -4192,7 +4507,7 @@ var handleProtectDecision = async ({
4192
4507
  }
4193
4508
  return decision.code;
4194
4509
  };
4195
- var executeEntryDecision = async ({
4510
+ var executeAdmittedEntryDecision = async ({
4196
4511
  connector,
4197
4512
  symbol,
4198
4513
  decision,
@@ -4206,7 +4521,9 @@ var executeEntryDecision = async ({
4206
4521
  ai,
4207
4522
  recordRuntimeJournal,
4208
4523
  invokeStageHooks,
4209
- notifyRuntimeError
4524
+ notifyRuntimeError,
4525
+ markPortfolioSubmitted,
4526
+ rejectPortfolioSubmission
4210
4527
  }) => {
4211
4528
  const signal = decision.signal;
4212
4529
  const beforePlaceOrder = async () => {
@@ -4253,6 +4570,8 @@ var executeEntryDecision = async ({
4253
4570
  ...Number.isFinite(Number(hookCtx.strategyConfig.LEVERAGE)) ? { leverage: Number(hookCtx.strategyConfig.LEVERAGE) } : {},
4254
4571
  signal,
4255
4572
  beforePlaceOrder,
4573
+ markPortfolioSubmitted,
4574
+ rejectPortfolioSubmission,
4256
4575
  recordRuntimeTrade: recordRuntimeJournal,
4257
4576
  executionLimits: resolveEntryExecutionLimits(
4258
4577
  hookCtx.strategyConfig,
@@ -4393,6 +4712,37 @@ var executeEntryDecision = async ({
4393
4712
  }
4394
4713
  return signal ?? decision.code;
4395
4714
  };
4715
+ async function executeEntryDecision(params) {
4716
+ if (!params.portfolio) return executeAdmittedEntryDecision(params);
4717
+ const { decision, portfolio } = params;
4718
+ const signal = decision.signal;
4719
+ if (!signal || decision.orderPlan.positionIntent === "increase")
4720
+ return "PORTFOLIO_REQUIRES_NEW_SIGNAL_POSITION";
4721
+ signal.orderId ??= createRuntimeOrderId(signal.strategy);
4722
+ const result = await portfolio.submit(
4723
+ params.portfolioTime ?? decision.entryContext.timestamp,
4724
+ {
4725
+ id: signal.orderId,
4726
+ signalId: signal.signalId,
4727
+ strategy: signal.strategy,
4728
+ symbol: params.symbol,
4729
+ direction: decision.entryContext.direction,
4730
+ qty: decision.orderPlan.qty,
4731
+ timestamp: decision.entryContext.timestamp
4732
+ },
4733
+ (markPortfolioSubmitted, rejectPortfolioSubmission) => executeAdmittedEntryDecision({
4734
+ ...params,
4735
+ markPortfolioSubmitted,
4736
+ rejectPortfolioSubmission
4737
+ })
4738
+ );
4739
+ if (result.reason) {
4740
+ signal.orderStatus = "failed";
4741
+ signal.orderFailureReason = result.reason;
4742
+ return signal;
4743
+ }
4744
+ return result.value;
4745
+ }
4396
4746
 
4397
4747
  // src/strategyRuntime.ts
4398
4748
  var cloneWithPropertyDescriptors = (value) => Object.create(
@@ -4420,6 +4770,7 @@ var createStrategyRuntime = ({
4420
4770
  };
4421
4771
  const creator = async ({
4422
4772
  userName,
4773
+ connectorName,
4423
4774
  config: baseConfig,
4424
4775
  symbol,
4425
4776
  universe: requestedUniverse,
@@ -4458,6 +4809,17 @@ var createStrategyRuntime = ({
4458
4809
  const projectConfig = await loadTradejsConfig(projectRoot);
4459
4810
  const projectHooks = projectConfig.hooks;
4460
4811
  const env = String(config.ENV ?? "BACKTEST");
4812
+ const portfolio = getPortfolioRuntime({
4813
+ connectorName,
4814
+ value: baseConfig?.PORTFOLIO_POLICY ?? config.PORTFOLIO_POLICY,
4815
+ strategy: strategyName,
4816
+ connector,
4817
+ env,
4818
+ userName,
4819
+ accountId,
4820
+ deploymentId
4821
+ });
4822
+ const portfolioContext = portfolio ? (now2) => isRuntimeOrderExecutionEnabled({ config, env, connector }) ? portfolio.tick(now2) : portfolio.context(now2) : void 0;
4461
4823
  const researchPolicyValue = baseConfig?.RESEARCH_ENTRY_POLICY ?? config.RESEARCH_ENTRY_POLICY;
4462
4824
  const researchEntryPolicy = researchPolicyValue == null ? void 0 : parseResearchEntryPolicy(researchPolicyValue);
4463
4825
  if (researchEntryPolicy && (env !== "BACKTEST" || Reflect.get(connector, "__tradejsTestConnector") !== true))
@@ -4756,6 +5118,20 @@ var createStrategyRuntime = ({
4756
5118
  interval: decisionInterval
4757
5119
  }) => {
4758
5120
  if (!baseContext) return void 0;
5121
+ const profileSource = strategyManifest?.contextRequirements?.tickerProfiles;
5122
+ if (profileSource) {
5123
+ const profile = await loadTickerProfileContext({
5124
+ ...profileSource,
5125
+ projectRoot,
5126
+ symbol: decisionSymbol,
5127
+ timestamp: candle.timestamp
5128
+ });
5129
+ if (!profile)
5130
+ throw new Error(
5131
+ "Required ticker profile is unavailable at decision time"
5132
+ );
5133
+ baseContext = withTickerProfile(baseContext, profile);
5134
+ }
4759
5135
  if (!coreContextRequirements.has("hyperliquidWhales")) {
4760
5136
  return baseContext;
4761
5137
  }
@@ -4923,6 +5299,8 @@ var createStrategyRuntime = ({
4923
5299
  runtime: pending.runtime
4924
5300
  });
4925
5301
  return executeEntryDecision({
5302
+ portfolio,
5303
+ portfolioTime: env === "BACKTEST" || env === "PARITY" ? candle.timestamp : Date.now(),
4926
5304
  connector,
4927
5305
  symbol,
4928
5306
  decision: pending.decision,
@@ -4940,6 +5318,10 @@ var createStrategyRuntime = ({
4940
5318
  });
4941
5319
  };
4942
5320
  const runWithDecisionOverride = async (candle, btcCandle, options = {}) => {
5321
+ if (portfolioContext)
5322
+ await portfolioContext(
5323
+ env === "BACKTEST" || env === "PARITY" ? candle.timestamp : Date.now()
5324
+ );
4943
5325
  appendCurrentMarketData(candle, btcCandle, options.ethCandle);
4944
5326
  const ethCandle = resolveEthCandle(candle, options.ethCandle);
4945
5327
  indicatorsState.setCurrentBar(candle, btcCandle, ethCandle);
@@ -5110,6 +5492,13 @@ var createStrategyRuntime = ({
5110
5492
  });
5111
5493
  const signal = decision.signal;
5112
5494
  if (signal) {
5495
+ if (portfolio)
5496
+ signal.additionalIndicators = {
5497
+ ...signal.additionalIndicators,
5498
+ portfolioContext: await portfolioContext(
5499
+ env === "BACKTEST" || env === "PARITY" ? decision.entryContext.timestamp : Date.now()
5500
+ )
5501
+ };
5113
5502
  if (runtimeLineage) signal.runtimeLineage = runtimeLineage;
5114
5503
  if (universe) signal.universe = universe;
5115
5504
  if (assetClass) signal.assetClass = assetClass;
@@ -5349,6 +5738,8 @@ var createStrategyRuntime = ({
5349
5738
  });
5350
5739
  }
5351
5740
  return executeEntryDecision({
5741
+ portfolio,
5742
+ portfolioTime: env === "BACKTEST" || env === "PARITY" ? candle.timestamp : Date.now(),
5352
5743
  connector,
5353
5744
  symbol,
5354
5745
  decision,