@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.
- package/dist/ai.js +109 -48
- package/dist/ai.mjs +1 -1
- package/dist/backtest.js +2594 -1904
- package/dist/backtest.mjs +3 -2
- package/dist/{chunk-PB2KHBGH.mjs → chunk-EVKQ4DI6.mjs} +461 -33
- package/dist/chunk-NJEYNGRU.mjs +226 -0
- package/dist/{chunk-UL5Z46JL.mjs → chunk-PYJWXAYR.mjs} +97 -30
- 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 +2611 -1921
- package/dist/diagnostics.mjs +4 -3
- package/dist/mcp.d.mts +1 -1
- package/dist/mcp.d.ts +1 -1
- package/dist/mcp.js +2634 -1944
- package/dist/mcp.mjs +4 -3
- package/dist/portfolio.d.mts +28 -0
- package/dist/portfolio.d.ts +28 -0
- package/dist/portfolio.js +243 -0
- package/dist/portfolio.mjs +9 -0
- package/dist/registry.js +2614 -1924
- package/dist/registry.mjs +3 -2
- package/dist/runtimeDashboard.js +2335 -1643
- package/dist/runtimeDashboard.mjs +4 -3
- package/dist/runtimeStrategies.js +2615 -1925
- package/dist/runtimeStrategies.mjs +4 -3
- package/dist/strategies.d.mts +3 -1
- package/dist/strategies.d.ts +3 -1
- package/dist/strategies.js +2153 -1463
- package/dist/strategies.mjs +3 -2
- package/package.json +10 -5
|
@@ -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-
|
|
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,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
|
-
|
|
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
|
-
...
|
|
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
|
|
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 =
|
|
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
|
|
5046
|
-
|
|
5047
|
-
|
|
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,
|