@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/ai.js +109 -48
- package/dist/ai.mjs +1 -1
- package/dist/backtest.js +2491 -1871
- package/dist/backtest.mjs +3 -2
- package/dist/{chunk-PB2KHBGH.mjs → chunk-HRON2EAI.mjs} +413 -22
- package/dist/{chunk-UL5Z46JL.mjs → chunk-PYJWXAYR.mjs} +97 -30
- package/dist/chunk-UUNEWAT4.mjs +193 -0
- package/dist/{chunk-BO42QWAB.mjs → chunk-W6PPAADK.mjs} +1 -1
- package/dist/cli.js +834 -773
- package/dist/cli.mjs +1 -1
- package/dist/diagnostics.js +2511 -1891
- package/dist/diagnostics.mjs +4 -3
- package/dist/mcp.d.mts +1 -1
- package/dist/mcp.d.ts +1 -1
- package/dist/mcp.js +2537 -1917
- package/dist/mcp.mjs +4 -3
- package/dist/portfolio.d.mts +28 -0
- package/dist/portfolio.d.ts +28 -0
- package/dist/portfolio.js +210 -0
- package/dist/portfolio.mjs +9 -0
- package/dist/registry.js +2429 -1809
- package/dist/registry.mjs +3 -2
- package/dist/runtimeDashboard.js +2236 -1614
- package/dist/runtimeDashboard.mjs +4 -3
- package/dist/runtimeStrategies.js +2052 -1432
- package/dist/runtimeStrategies.mjs +4 -3
- package/dist/strategies.d.mts +3 -1
- package/dist/strategies.d.ts +3 -1
- package/dist/strategies.js +2085 -1465
- package/dist/strategies.mjs +3 -2
- package/package.json +10 -5
package/dist/backtest.mjs
CHANGED
|
@@ -10,11 +10,12 @@ import {
|
|
|
10
10
|
evaluateResearchEntryPolicy,
|
|
11
11
|
matchesResearchEntryRules,
|
|
12
12
|
parseResearchEntryPolicy
|
|
13
|
-
} from "./chunk-
|
|
13
|
+
} from "./chunk-HRON2EAI.mjs";
|
|
14
|
+
import "./chunk-UUNEWAT4.mjs";
|
|
14
15
|
import {
|
|
15
16
|
buildAiPayload,
|
|
16
17
|
getStrategyCreator
|
|
17
|
-
} from "./chunk-
|
|
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-
|
|
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
|
-
|
|
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 {
|
|
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
|
|
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
|
-
|
|
2495
|
-
|
|
2496
|
-
|
|
2497
|
-
|
|
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
|
-
|
|
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
|
|
2935
|
-
if (!Array.isArray(
|
|
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
|
|
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
|
-
...
|
|
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
|
|
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,
|