@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.
@@ -784,7 +784,7 @@ var import_time4 = require("@tradejs/core/time");
784
784
  var import_logger15 = require("@tradejs/infra/logger");
785
785
  var import_runtimeDeploymentEvents = require("@tradejs/infra/runtimeDeploymentEvents");
786
786
  var import_tradingAccounts3 = require("@tradejs/infra/tradingAccounts");
787
- var import_redis4 = require("@tradejs/infra/redis");
787
+ var import_redis5 = require("@tradejs/infra/redis");
788
788
 
789
789
  // src/connectorsRegistry.ts
790
790
  var import_logger4 = require("@tradejs/infra/logger");
@@ -2083,1376 +2083,2112 @@ var syncRuntimeTrades = async ({
2083
2083
 
2084
2084
  // src/runtimeStrategies.ts
2085
2085
  var import_jev8 = require("@tradejs/core/jev");
2086
- var import_node_crypto4 = require("crypto");
2086
+ var import_node_crypto5 = require("crypto");
2087
2087
  var import_promises2 = require("fs/promises");
2088
2088
  var import_node_path2 = __toESM(require("path"));
2089
2089
  var import_runtimeControls = require("@tradejs/infra/runtimeControls");
2090
2090
  var import_tradingAccounts2 = require("@tradejs/infra/tradingAccounts");
2091
2091
 
2092
- // src/strategyRuntime.ts
2093
- var import_jev6 = require("@tradejs/core/jev");
2092
+ // src/strategy/portfolioRuntime.ts
2093
+ var import_node_crypto2 = require("crypto");
2094
+ var import_portfolio2 = require("@tradejs/core/portfolio");
2095
+ var import_redis3 = require("@tradejs/infra/redis");
2094
2096
 
2095
- // src/jev.ts
2096
- var import_promises = __toESM(require("fs/promises"));
2097
- var import_node_path = __toESM(require("path"));
2098
- var import_jev4 = require("@tradejs/core/jev");
2099
- var import_jev5 = require("@tradejs/infra/jev");
2100
- var import_userSettings = require("@tradejs/infra/userSettings");
2101
- init_jevInput();
2102
- init_jevInput();
2103
- init_jevTraining();
2104
- var resolveJevProvider = async (userName) => {
2105
- const settings = await (0, import_userSettings.getUserSettings)(userName);
2106
- const provider = {
2107
- endpoint: settings.JEV_API_ENDPOINT || import_jev4.JEV_PROVIDERS[0].endpoint,
2108
- model: settings.JEV_MODEL || import_jev4.JEV_PROVIDERS[0].model
2109
- };
2110
- (0, import_jev4.parseJevConfig)({ source: "provider", mode: "observe", provider });
2111
- if (!settings.JEV_API_KEY)
2112
- throw new Error("Configure the Jev API key in Account settings");
2113
- return provider;
2114
- };
2115
- var emptyScores = () => Object.fromEntries(import_jev4.JEV_DIMENSIONS.map((key) => [key, null]));
2116
- var pending = /* @__PURE__ */ new Map();
2117
- var pendingResponses = /* @__PURE__ */ new Map();
2118
- var evaluateJevInput = async ({
2119
- input,
2120
- config,
2121
- userName,
2122
- projectRoot
2123
- }) => {
2124
- config = (0, import_jev4.parseJevConfig)(config);
2125
- const inputHash = (0, import_jev5.jevHash)(input);
2126
- const questions = questionsForJevInput(input);
2127
- const questionsHash = (0, import_jev5.jevHash)(questions);
2128
- const base = {
2129
- questionSet: import_jev4.JEV_QUESTION_SET,
2130
- schema: "tradejs-signal-assessment/v5",
2131
- source: config.source,
2132
- mode: config.mode,
2133
- inputHash,
2134
- questionsHash,
2135
- geometryStatus: input.geometryStatus,
2136
- levelsValid: typeof input.features["signal.validLevels"] === "boolean" ? input.features["signal.validLevels"] : null
2137
- };
2138
- if (!input.questions.length)
2139
- return {
2140
- assessment: {
2141
- ...base,
2142
- status: "unavailable",
2143
- model: "",
2144
- scores: emptyScores(),
2145
- confidence: emptyScores()
2146
- }
2147
- };
2148
- if (config.source === "local") {
2149
- const contents = await import_promises.default.readFile(
2150
- import_node_path.default.resolve(projectRoot, config.modelFile),
2151
- "utf8"
2152
- );
2153
- if ((0, import_jev5.jevFileHash)(contents) !== config.modelSha256)
2154
- throw new Error("Local Jev gate checksum mismatch");
2155
- const { validateJevGateModel: validateJevGateModel2 } = await Promise.resolve().then(() => (init_jevTraining(), jevTraining_exports));
2156
- const model = validateJevGateModel2(JSON.parse(contents));
2157
- if (model.strategy !== input.strategy || model.questionsHash !== (0, import_jev5.jevHash)(JEV_QUESTIONS))
2158
- throw new Error("Local Jev gate strategy or question version mismatch");
2159
- const scores = Object.fromEntries(
2160
- import_jev4.JEV_DIMENSIONS.map((key) => [
2161
- key,
2162
- input.questions.includes(key) && model.trees[key] ? (0, import_jev4.predictJevTree)(model.trees[key], input.features) : null
2163
- ])
2164
- );
2165
- return {
2166
- assessment: {
2167
- ...base,
2168
- status: "available",
2169
- model: config.modelSha256,
2170
- scores,
2171
- confidence: emptyScores()
2097
+ // src/portfolio.ts
2098
+ var import_portfolio = require("@tradejs/core/portfolio");
2099
+ function createPortfolioCoordinator({
2100
+ policy,
2101
+ strategy,
2102
+ store,
2103
+ execution
2104
+ }) {
2105
+ const fingerprint3 = (0, import_portfolio.portfolioFingerprint)(policy, strategy);
2106
+ let cache;
2107
+ async function transact(now2, change, force = false) {
2108
+ for (let attempt = 0; attempt < 16; attempt++) {
2109
+ const previous = await store.read();
2110
+ if (previous && previous.fingerprint !== fingerprint3)
2111
+ throw new Error(
2112
+ "Portfolio policy changed under existing state; reconcile the persisted scope before changing configuration"
2113
+ );
2114
+ const base = previous ?? (0, import_portfolio.createPortfolioState)(fingerprint3);
2115
+ const bucket = Math.floor(now2 / policy.observationIntervalMs);
2116
+ const observation = !force && cache?.revision === base.revision && cache.bucket === bucket ? cache.observation : await execution.observe(now2, base.reservations);
2117
+ const reconciled = (0, import_portfolio.reconcilePortfolio)(
2118
+ policy,
2119
+ strategy,
2120
+ base,
2121
+ observation
2122
+ );
2123
+ for (const intent of base.closing) {
2124
+ if (!observation.positions.some(
2125
+ (p) => p.symbol === intent.position.symbol
2126
+ ))
2127
+ await execution.confirmClosed?.(intent, observation.asOf);
2172
2128
  }
2173
- };
2174
- }
2175
- const provider = config.provider ?? await resolveJevProvider(userName);
2176
- const id = (0, import_jev5.jevHash)({ inputHash, questionsHash, provider });
2177
- const dir = import_node_path.default.resolve(projectRoot, config.recordsDir ?? "data/ai/jev");
2178
- const file = import_node_path.default.join(dir, "records", `${id}.json`);
2179
- const getRecord = async () => {
2180
- const existing = await (0, import_jev5.readJevArtifact)(file);
2181
- if (existing) {
2182
- if (existing.schema !== "tradejs-jev-record/v4" || existing.id !== id || existing.inputHash !== inputHash || existing.questionsHash !== questionsHash || (0, import_jev5.jevHash)(existing.input) !== inputHash || (0, import_jev5.jevHash)(existing.provider) !== (0, import_jev5.jevHash)(provider))
2183
- throw new Error("Jev recording identity mismatch");
2184
- (0, import_jev5.validateJevResponse)(existing.response, input.questions);
2185
- for (const key of import_jev4.JEV_DIMENSIONS) {
2186
- const expected = input.questions.includes(key) ? existing.response.answers[key].score / (0, import_jev5.jevScoreMaximum)(key) : null;
2187
- if (existing.scores[key] !== expected)
2188
- throw new Error("Jev recording scores do not match the response");
2129
+ const changed = change(reconciled, observation);
2130
+ const next = { ...changed.state, revision: base.revision + 1 };
2131
+ if (await store.compare(previous, next)) {
2132
+ cache = { revision: next.revision, bucket, observation };
2133
+ return { state: next, observation, result: changed.result };
2189
2134
  }
2190
- return existing;
2135
+ cache = void 0;
2191
2136
  }
2192
- if (config.source === "recorded")
2193
- throw new Error(
2194
- `Missing Jev recording ${id} for ${input.strategy} ${input.symbol} at ${new Date(input.timestamp).toISOString()}. Recorded mode requires the same signal-time facts and provider; a shorter backtest can change warm-up indicators or prior trade state.`
2137
+ throw new Error("Portfolio state contention; entry was not submitted");
2138
+ }
2139
+ async function tick(now2, force = false) {
2140
+ const claimed = await transact(
2141
+ now2,
2142
+ (state, observation) => {
2143
+ const commands = (0, import_portfolio.portfolioCloseCommands)(policy, state, observation);
2144
+ return {
2145
+ state: {
2146
+ ...state,
2147
+ closing: state.closing.map(
2148
+ (c) => commands.some((x) => x.intent.positionId === c.positionId) ? { ...c, lastAttemptAt: observation.asOf } : c
2149
+ )
2150
+ },
2151
+ result: commands
2152
+ };
2153
+ },
2154
+ force
2155
+ );
2156
+ for (const command of claimed.result)
2157
+ await execution.close(
2158
+ command.position,
2159
+ command.intent,
2160
+ claimed.observation.asOf
2195
2161
  );
2196
- const settings = await (0, import_userSettings.getUserSettings)(userName);
2197
- if (!settings.JEV_API_KEY)
2198
- throw new Error("Configure the Jev API key in Account settings");
2199
- const apiKey = settings.JEV_API_KEY;
2200
- const configuredProvider = await resolveJevProvider(userName);
2201
- if (configuredProvider.endpoint !== provider.endpoint)
2202
- throw new Error(
2203
- "Jev account endpoint changed; update the frozen provider configuration"
2162
+ if (claimed.result.length) {
2163
+ cache = void 0;
2164
+ const refreshed = await transact(
2165
+ now2,
2166
+ (state) => ({ state, result: void 0 }),
2167
+ true
2168
+ );
2169
+ return (0, import_portfolio.buildPortfolioContext)(
2170
+ policy,
2171
+ strategy,
2172
+ refreshed.state,
2173
+ refreshed.observation
2204
2174
  );
2205
- const started = Date.now();
2206
- const state = stateForJevInput(input);
2207
- const responseId = (0, import_jev5.jevHash)({
2208
- state,
2209
- questionsHash,
2210
- provider,
2211
- user: (0, import_jev5.jevHash)(userName)
2212
- });
2213
- const responseFile = import_node_path.default.join(dir, "responses", `${responseId}.json`);
2214
- let responsePromise = pendingResponses.get(responseFile);
2215
- if (!responsePromise) {
2216
- responsePromise = (async () => {
2217
- const saved = await (0, import_jev5.readJevArtifact)(responseFile);
2218
- if (saved) return (0, import_jev5.validateJevResponse)(saved, input.questions);
2219
- const fresh = await (0, import_jev5.requestJev)(provider, apiKey, state, questions);
2220
- await (0, import_jev5.writeJevArtifact)(responseFile, fresh);
2221
- return fresh;
2222
- })();
2223
- pendingResponses.set(responseFile, responsePromise);
2224
- }
2225
- let response;
2226
- try {
2227
- response = await responsePromise;
2228
- } finally {
2229
- pendingResponses.delete(responseFile);
2230
2175
  }
2231
- const scores = Object.fromEntries(
2232
- import_jev4.JEV_DIMENSIONS.map((key) => [
2233
- key,
2234
- input.questions.includes(key) ? response.answers[key].score / (0, import_jev5.jevScoreMaximum)(key) : null
2235
- ])
2176
+ return (0, import_portfolio.buildPortfolioContext)(
2177
+ policy,
2178
+ strategy,
2179
+ claimed.state,
2180
+ claimed.observation
2236
2181
  );
2237
- const value = {
2238
- schema: "tradejs-jev-record/v4",
2239
- id,
2240
- inputHash,
2241
- questionsHash,
2242
- provider,
2243
- input,
2244
- response,
2245
- scores,
2246
- createdAt: (/* @__PURE__ */ new Date()).toISOString(),
2247
- elapsedMs: Date.now() - started
2248
- };
2249
- await (0, import_jev5.writeJevArtifact)(file, value);
2250
- return await (0, import_jev5.readJevArtifact)(file);
2251
- };
2252
- const pendingKey = `${file}:${(0, import_jev5.jevHash)(userName)}`;
2253
- let promise = pending.get(pendingKey);
2254
- if (!promise) {
2255
- promise = getRecord();
2256
- pending.set(pendingKey, promise);
2257
2182
  }
2258
- let record;
2259
- try {
2260
- record = await promise;
2261
- } finally {
2262
- pending.delete(pendingKey);
2183
+ async function updateReservation(now2, id, submitted, qty) {
2184
+ return transact(
2185
+ now2,
2186
+ (s, o) => {
2187
+ const exists = s.reservations.some((r) => r.id === id);
2188
+ const blocked = submitted && (!exists || s.closing.length > 0 || o.asOf <= s.blockedThrough);
2189
+ return {
2190
+ state: {
2191
+ ...s,
2192
+ reservations: s.reservations.flatMap(
2193
+ (r) => r.id !== id ? [r] : submitted && !blocked ? [{ ...r, submitted: true, qty: qty ?? r.qty }] : []
2194
+ )
2195
+ },
2196
+ result: !blocked
2197
+ };
2198
+ },
2199
+ true
2200
+ );
2263
2201
  }
2264
2202
  return {
2265
- record,
2266
- assessment: {
2267
- ...base,
2268
- status: "available",
2269
- recordId: id,
2270
- model: record.response.model,
2271
- scores: record.scores,
2272
- confidence: Object.fromEntries(
2273
- import_jev4.JEV_DIMENSIONS.map((key) => [
2274
- key,
2275
- record.response.answers[key]?.confidence ?? null
2276
- ])
2277
- )
2203
+ async context(now2) {
2204
+ const state = await store.read() ?? (0, import_portfolio.createPortfolioState)(fingerprint3);
2205
+ if (state.fingerprint !== fingerprint3)
2206
+ throw new Error("Portfolio policy changed under existing state");
2207
+ const observation = await execution.observe(now2, state.reservations);
2208
+ (0, import_portfolio.validatePortfolioObservation)(observation);
2209
+ return (0, import_portfolio.buildPortfolioContext)(policy, strategy, state, observation);
2210
+ },
2211
+ tick,
2212
+ async submit(now2, entry, send) {
2213
+ await tick(now2);
2214
+ const admitted = await transact(
2215
+ now2,
2216
+ (s, o) => {
2217
+ const r = (0, import_portfolio.reservePortfolioEntry)(policy, strategy, s, o, {
2218
+ ...entry,
2219
+ submitted: false,
2220
+ oppositeHandled: false
2221
+ });
2222
+ return { state: r.state, result: r.reason };
2223
+ },
2224
+ true
2225
+ );
2226
+ if (admitted.result) return { reason: admitted.result };
2227
+ let submitted = false;
2228
+ try {
2229
+ const value = await send(
2230
+ async (qty) => {
2231
+ if (!Number.isFinite(qty) || qty <= 0)
2232
+ throw new Error("Invalid submitted portfolio quantity");
2233
+ submitted = true;
2234
+ const update = await updateReservation(now2, entry.id, true, qty);
2235
+ if (!update.result)
2236
+ throw new Error("PORTFOLIO_SUBMISSION_NO_LONGER_ADMITTED");
2237
+ },
2238
+ async () => {
2239
+ await transact(
2240
+ now2,
2241
+ (s) => ({
2242
+ state: {
2243
+ ...s,
2244
+ reservations: s.reservations.filter((r) => r.id !== entry.id),
2245
+ completedSignals: {
2246
+ ...s.completedSignals,
2247
+ [entry.signalId]: now2
2248
+ }
2249
+ },
2250
+ result: void 0
2251
+ }),
2252
+ true
2253
+ );
2254
+ }
2255
+ );
2256
+ if (!submitted) await updateReservation(now2, entry.id, false);
2257
+ else await tick(now2, true);
2258
+ return { value };
2259
+ } catch (error) {
2260
+ if (!submitted) await updateReservation(now2, entry.id, false);
2261
+ throw error;
2262
+ }
2278
2263
  }
2279
2264
  };
2280
- };
2281
- var assessSignalWithJev = async ({
2282
- signal,
2283
- config,
2284
- userName,
2285
- projectRoot,
2286
- strict,
2287
- scope = "runtime"
2288
- }) => {
2289
- let assessment;
2290
- try {
2291
- const input = buildJevInput(signal);
2292
- ({ assessment } = await evaluateJevInput({
2293
- input,
2294
- config,
2295
- userName,
2296
- projectRoot
2297
- }));
2298
- } catch (error) {
2299
- if (strict) throw error;
2300
- assessment = {
2301
- questionSet: import_jev4.JEV_QUESTION_SET,
2302
- schema: "tradejs-signal-assessment/v5",
2303
- source: config.source,
2304
- mode: config.mode,
2305
- status: "unavailable",
2306
- inputHash: "",
2307
- questionsHash: "",
2308
- geometryStatus: "absent",
2309
- levelsValid: null,
2310
- model: config.provider?.model ?? config.modelSha256 ?? "",
2311
- scores: emptyScores(),
2312
- confidence: emptyScores()
2313
- };
2314
- }
2315
- signal.assessment = assessment;
2316
- signal.additionalIndicators = {
2317
- ...signal.additionalIndicators,
2318
- jev: (0, import_jev4.jevAssessmentFeatures)(assessment)
2265
+ }
2266
+ function createMemoryPortfolioStore() {
2267
+ let state = null;
2268
+ return {
2269
+ read: async () => state === null ? null : structuredClone(state),
2270
+ compare: async (expected, next) => {
2271
+ if ((state?.revision ?? -1) !== (expected?.revision ?? -1)) return false;
2272
+ state = structuredClone(next);
2273
+ return true;
2274
+ }
2319
2275
  };
2320
- const identity = {
2321
- signalId: signal.signalId,
2322
- strategy: signal.strategy,
2323
- symbol: signal.symbol,
2324
- timestamp: signal.timestamp,
2325
- scope,
2326
- user: (0, import_jev5.jevHash)(userName),
2327
- config,
2328
- assessment
2329
- };
2330
- await (0, import_jev5.writeJevArtifact)(
2331
- import_node_path.default.resolve(
2332
- projectRoot,
2333
- config.recordsDir ?? "data/ai/jev",
2334
- "decisions",
2335
- `${(0, import_jev5.jevHash)(identity)}.json`
2336
- ),
2337
- identity
2338
- );
2339
- return assessment;
2340
- };
2341
-
2342
- // src/strategyRuntime.ts
2343
- var import_constants7 = require("@tradejs/core/constants");
2344
- var import_strategies6 = require("@tradejs/core/strategies");
2345
- var import_logger14 = require("@tradejs/infra/logger");
2346
-
2347
- // src/strategyHelpers/runtime.ts
2348
- var import_logger12 = require("@tradejs/infra/logger");
2349
- var import_constants5 = require("@tradejs/core/constants");
2350
- var import_ml2 = require("@tradejs/infra/ml");
2276
+ }
2351
2277
 
2352
- // src/strategy/entrySubmissionValidation.ts
2278
+ // src/runtimeJournal.ts
2279
+ var import_node_crypto = require("crypto");
2280
+ var import_constants3 = require("@tradejs/core/constants");
2281
+ var import_time3 = require("@tradejs/core/time");
2282
+ var import_trade = require("@tradejs/core/trade");
2353
2283
  var import_logger5 = require("@tradejs/infra/logger");
2354
-
2355
- // src/strategy/entryExecutionValidation.ts
2356
- var EntrySubmissionValidationError = class extends Error {
2357
- constructor() {
2358
- super(...arguments);
2359
- this.name = "EntrySubmissionValidationError";
2360
- }
2361
- };
2362
- var resolveEntryExecutionLimits = (config, direction) => {
2363
- const side = config[direction];
2364
- const minimum = side && typeof side === "object" && "minRiskRatio" in side ? side.minRiskRatio : void 0;
2365
- const maximum = config.MAX_LOSS_VALUE;
2366
- if (minimum == null && maximum == null) return void 0;
2367
- const minimumNetRiskRatio = minimum == null ? void 0 : Number(minimum);
2368
- const maxLossValue = maximum == null ? void 0 : Number(maximum);
2369
- const feeRate = Number(config.RISK_FEE_RATE ?? 0);
2370
- const slippage = Number(config.RISK_SLIPPAGE_BPS ?? 0);
2371
- const impact = Number(config.RISK_MARKET_IMPACT_BPS ?? 0);
2372
- const slippageBps = slippage + impact;
2373
- if (minimumNetRiskRatio != null && (!Number.isFinite(minimumNetRiskRatio) || minimumNetRiskRatio < 0) || maxLossValue != null && (!Number.isFinite(maxLossValue) || maxLossValue <= 0) || !Number.isFinite(feeRate) || feeRate < 0 || feeRate >= 1 || !Number.isFinite(slippage) || slippage < 0 || !Number.isFinite(impact) || impact < 0 || !Number.isFinite(slippageBps) || slippageBps < 0 || slippageBps >= 1e4)
2374
- throw new Error("INVALID_ENTRY_EXECUTION_LIMITS");
2375
- return { minimumNetRiskRatio, maxLossValue, feeRate, slippageBps };
2376
- };
2377
- var validateEntryExecution = ({
2284
+ var import_redis2 = require("@tradejs/infra/redis");
2285
+ var now = () => Date.now();
2286
+ var toRandomOrderSuffix = () => (0, import_node_crypto.randomUUID)().replace(/-/g, "").slice(0, 12).toLowerCase();
2287
+ var calculateClosedPnl = ({
2378
2288
  direction,
2379
- price,
2380
- qty,
2381
- stopLossPrice,
2382
- takeProfitPrice,
2383
- limits
2289
+ entryPrice,
2290
+ exitPrice,
2291
+ qty
2384
2292
  }) => {
2385
- if (!Number.isFinite(price) || price <= 0 || !Number.isFinite(qty) || qty <= 0 || stopLossPrice == null || !Number.isFinite(stopLossPrice) || stopLossPrice <= 0 || !Number.isFinite(takeProfitPrice) || takeProfitPrice <= 0)
2386
- throw new EntrySubmissionValidationError(
2387
- "ENTRY_ECONOMICS_INPUTS_UNAVAILABLE"
2388
- );
2389
- const side = direction === "LONG" ? 1 : -1;
2390
- if (side * (price - stopLossPrice) <= 0 || side * (takeProfitPrice - price) <= 0)
2391
- throw new EntrySubmissionValidationError(
2392
- "INVALID_ENTRY_PROTECTION_AT_SUBMISSION"
2393
- );
2394
- const slippage = limits.slippageBps / 1e4;
2395
- const cost = (value, sign) => value * slippage + value * (1 + sign * slippage) * limits.feeRate;
2396
- const entryCost = cost(price, side);
2397
- const lossPerUnit = Math.abs(price - stopLossPrice) + entryCost + cost(stopLossPrice, -side);
2398
- const rewardPerUnit = Math.max(
2399
- 0,
2400
- Math.abs(takeProfitPrice - price) - entryCost - cost(takeProfitPrice, -side)
2401
- );
2402
- const netRiskRatio = rewardPerUnit / lossPerUnit;
2403
- const plannedLossValue = qty * lossPerUnit;
2404
- if (!Number.isFinite(netRiskRatio) || !Number.isFinite(plannedLossValue))
2405
- throw new EntrySubmissionValidationError(
2406
- "ENTRY_ECONOMICS_INPUTS_UNAVAILABLE"
2407
- );
2408
- if (limits.minimumNetRiskRatio != null && netRiskRatio <= limits.minimumNetRiskRatio)
2409
- throw new EntrySubmissionValidationError(
2410
- "ENTRY_RR_BELOW_MIN_AT_SUBMISSION"
2411
- );
2412
- const riskExceeded = limits.maxLossValue != null && plannedLossValue > limits.maxLossValue * (1 + 1e-9);
2413
- return { netRiskRatio, plannedLossValue, riskExceeded };
2293
+ const pnl = direction === "LONG" ? (exitPrice - entryPrice) * qty : (entryPrice - exitPrice) * qty;
2294
+ return Number.isFinite(pnl) ? pnl : null;
2414
2295
  };
2415
-
2416
- // src/strategy/entrySubmissionValidation.ts
2417
- var MAX_ENTRY_QUOTE_AGE_MS = 5e3;
2418
- var toFiniteNumberOrNull = (value) => typeof value === "number" && Number.isFinite(value) ? value : null;
2419
- var getOrderArrivalSnapshot = async ({
2420
- connector,
2421
- symbol,
2422
- requireTimestamp = false
2423
- }) => {
2424
- if (typeof connector.getTopOfBookTicker !== "function") {
2425
- return {
2426
- arrivalSnapshotTime: Date.now(),
2427
- arrivalSource: "unavailable",
2428
- bid: null,
2429
- ask: null,
2430
- arrivalMid: null,
2431
- spreadBps: null
2432
- };
2296
+ var createRuntimeOrderId = (strategy) => {
2297
+ const prefix = (0, import_trade.createRuntimeOrderLinkPrefix)(strategy);
2298
+ if (prefix === "tjs-") {
2299
+ return `tjs-${(0, import_node_crypto.randomUUID)().replace(/-/g, "").slice(0, 24).toLowerCase()}`;
2300
+ }
2301
+ return `${prefix}${toRandomOrderSuffix()}`;
2302
+ };
2303
+ var recordRuntimeTradeOpen = async (params) => {
2304
+ const { userName } = params;
2305
+ if (!userName) {
2306
+ return null;
2433
2307
  }
2308
+ const record = {
2309
+ ...params,
2310
+ entryCount: 1,
2311
+ lastEntryPrice: params.entryPrice,
2312
+ lastEntryQty: params.qty,
2313
+ lastEntryTimestamp: params.entryTimestamp,
2314
+ status: "active",
2315
+ currentPrice: params.entryPrice,
2316
+ currentPnl: 0,
2317
+ closedPnl: null,
2318
+ exitPrice: null,
2319
+ exitTimestamp: null,
2320
+ lastSyncedAt: now()
2321
+ };
2322
+ const dayKey = (0, import_time3.getRuntimeStorageDayKey)(record.entryTimestamp);
2323
+ const runtimeScopeId = record.deploymentId ?? record.accountId;
2434
2324
  try {
2435
- const ticker = await connector.getTopOfBookTicker(symbol);
2436
- const arrivalSnapshotTime = toFiniteNumberOrNull(ticker?.timestamp);
2437
- const timestampUnavailable = requireTimestamp && arrivalSnapshotTime == null;
2438
- const bid = timestampUnavailable ? null : toFiniteNumberOrNull(ticker?.bidPrice);
2439
- const ask = timestampUnavailable ? null : toFiniteNumberOrNull(ticker?.askPrice);
2440
- const arrivalMid = bid != null && ask != null ? (bid + ask) / 2 : null;
2441
- const spreadBps = bid != null && ask != null && arrivalMid != null && arrivalMid > 0 ? (ask - bid) / arrivalMid * 1e4 : null;
2442
- return {
2443
- arrivalSnapshotTime: arrivalSnapshotTime ?? Date.now(),
2444
- arrivalSource: "top_of_book",
2445
- bid,
2446
- ask,
2447
- arrivalMid,
2448
- spreadBps
2449
- };
2325
+ await Promise.all([
2326
+ (0, import_redis2.setData)(import_redis2.redisKeys.runtimeTrade(userName, record.orderId), record, {
2327
+ expire: 0
2328
+ }),
2329
+ (0, import_redis2.setHashJsonField)(
2330
+ import_redis2.redisKeys.runtimeTradeBucket(userName, dayKey),
2331
+ record.orderId,
2332
+ record,
2333
+ { expire: 0 }
2334
+ ),
2335
+ (0, import_redis2.setData)(
2336
+ import_redis2.redisKeys.runtimeActiveTrade(userName, record.symbol, runtimeScopeId),
2337
+ { orderId: record.orderId },
2338
+ { expire: 0 }
2339
+ )
2340
+ ]);
2450
2341
  } catch (error) {
2451
- import_logger5.logger.warn(
2452
- "runtime order arrival snapshot failed: %s %s",
2453
- symbol,
2342
+ import_logger5.logger.error(
2343
+ "runtime trade open journal failed: %s %s",
2344
+ record.symbol,
2454
2345
  error?.message || String(error)
2455
2346
  );
2456
- return {
2457
- arrivalSnapshotTime: Date.now(),
2458
- arrivalSource: "top_of_book_error",
2459
- bid: null,
2460
- ask: null,
2461
- arrivalMid: null,
2462
- spreadBps: null
2463
- };
2464
2347
  }
2348
+ return record;
2465
2349
  };
2466
- var validateEntryProtectionAtArrival = ({
2467
- direction,
2468
- signalPrice,
2469
- bid,
2470
- ask,
2471
- arrivalMid,
2472
- takeProfits,
2473
- stopLossPrice
2474
- }) => {
2475
- const entryReference = direction === "LONG" ? ask ?? arrivalMid ?? signalPrice : bid ?? arrivalMid ?? signalPrice;
2476
- const stopReference = direction === "LONG" ? bid ?? arrivalMid ?? signalPrice : ask ?? arrivalMid ?? signalPrice;
2477
- if (!Number.isFinite(entryReference) || entryReference <= 0 || !Number.isFinite(stopReference) || stopReference <= 0) {
2478
- throw new Error("INVALID_ENTRY_REFERENCE_AT_ARRIVAL");
2479
- }
2480
- for (const takeProfit of takeProfits) {
2481
- if (!Number.isFinite(takeProfit.price) || takeProfit.price <= 0 || (direction === "LONG" ? takeProfit.price <= entryReference : takeProfit.price >= entryReference)) {
2482
- throw new Error("TAKE_PROFIT_CROSSED_BEFORE_ENTRY");
2483
- }
2350
+ var recordRuntimeTradeIncrease = async (params) => {
2351
+ const {
2352
+ userName,
2353
+ strategy,
2354
+ symbol,
2355
+ direction,
2356
+ resultingQty,
2357
+ resultingEntryPrice,
2358
+ addedQty,
2359
+ addedEntryPrice,
2360
+ entryTimestamp,
2361
+ fee,
2362
+ accountId,
2363
+ deploymentId
2364
+ } = params;
2365
+ if (!userName) {
2366
+ return null;
2484
2367
  }
2485
- if (stopLossPrice != null && (!Number.isFinite(stopLossPrice) || stopLossPrice <= 0 || (direction === "LONG" ? stopLossPrice >= stopReference : stopLossPrice <= stopReference))) {
2486
- throw new Error("STOP_LOSS_CROSSED_BEFORE_ENTRY");
2368
+ const existing = await getActiveRuntimeTrade({
2369
+ userName,
2370
+ symbol,
2371
+ accountId,
2372
+ deploymentId
2373
+ });
2374
+ if (!existing || existing.strategy !== strategy || existing.direction !== direction) {
2375
+ return null;
2487
2376
  }
2488
- };
2489
- var createEntrySubmissionValidation = ({
2490
- connector,
2491
- symbol,
2492
- direction,
2493
- takeProfits,
2494
- stopLossPrice,
2495
- limits,
2496
- onValidated
2497
- }) => {
2498
- if (!limits) return void 0;
2499
- if (!connector.capabilities?.orderSubmissionValidation)
2500
- throw new EntrySubmissionValidationError(
2501
- "ENTRY_SUBMISSION_VALIDATION_UNSUPPORTED"
2502
- );
2503
- return async ({ qty, executionPrice }) => {
2504
- const simulated = Reflect.get(connector, "__tradejsTestConnector") === true;
2505
- const snapshot2 = simulated && executionPrice != null ? {
2506
- arrivalSnapshotTime: Date.now(),
2507
- arrivalSource: "simulation",
2508
- bid: executionPrice,
2509
- ask: executionPrice,
2510
- arrivalMid: executionPrice,
2511
- spreadBps: 0
2512
- } : await getOrderArrivalSnapshot({
2513
- connector,
2514
- symbol,
2515
- requireTimestamp: true
2516
- });
2517
- const age = Date.now() - snapshot2.arrivalSnapshotTime;
2518
- if (snapshot2.bid == null || snapshot2.ask == null || !Number.isFinite(snapshot2.bid) || !Number.isFinite(snapshot2.ask) || snapshot2.bid <= 0 || snapshot2.ask < snapshot2.bid)
2519
- throw new EntrySubmissionValidationError(
2520
- "ENTRY_QUOTE_UNAVAILABLE_AT_SUBMISSION"
2521
- );
2522
- if (!Number.isFinite(age) || age < -MAX_ENTRY_QUOTE_AGE_MS || age > MAX_ENTRY_QUOTE_AGE_MS)
2523
- throw new EntrySubmissionValidationError(
2524
- "ENTRY_QUOTE_STALE_AT_SUBMISSION"
2525
- );
2526
- const price = direction === "LONG" ? snapshot2.ask : snapshot2.bid;
2527
- validateEntryProtectionAtArrival({
2528
- direction,
2529
- signalPrice: price,
2530
- bid: snapshot2.bid,
2531
- ask: snapshot2.ask,
2532
- arrivalMid: snapshot2.arrivalMid,
2533
- takeProfits,
2534
- stopLossPrice
2535
- });
2536
- const prices = takeProfits.map((tp) => tp.price);
2537
- const economics2 = validateEntryExecution({
2538
- direction,
2539
- price,
2540
- qty,
2541
- stopLossPrice,
2542
- takeProfitPrice: direction === "LONG" ? Math.max(...prices) : Math.min(...prices),
2543
- limits
2544
- });
2545
- onValidated?.(snapshot2, economics2);
2377
+ const addedFee = typeof fee === "number" && Number.isFinite(fee) ? fee : 0;
2378
+ const openFee = (existing.openFee ?? existing.fee ?? 0) + addedFee;
2379
+ const totalFee = (existing.totalFee ?? existing.fee ?? 0) + addedFee;
2380
+ const next = {
2381
+ ...existing,
2382
+ ...params.orderLeverage ? { orderLeverage: params.orderLeverage } : {},
2383
+ qty: resultingQty,
2384
+ entryPrice: resultingEntryPrice,
2385
+ entryCount: Math.max(1, existing.entryCount ?? 1) + 1,
2386
+ lastEntryPrice: addedEntryPrice,
2387
+ lastEntryQty: addedQty,
2388
+ lastEntryTimestamp: entryTimestamp,
2389
+ currentPrice: resultingEntryPrice,
2390
+ currentPnl: 0,
2391
+ fee: openFee,
2392
+ openFee,
2393
+ totalFee,
2394
+ lastSyncedAt: now()
2546
2395
  };
2547
- };
2548
-
2549
- // src/ai.ts
2550
- var import_aiLanguages2 = require("@tradejs/core/aiLanguages");
2551
- var import_redis2 = require("@tradejs/infra/redis");
2552
-
2553
- // src/aiShared.ts
2554
- var MAX_AI_SERIES_POINTS = 5;
2555
- var COMPACT_INDICATORS_SNAPSHOT_SYMBOL = /* @__PURE__ */ Symbol.for(
2556
- "tradejs.indicators.compactSnapshot"
2557
- );
2558
- var COMPACT_INDICATORS_SNAPSHOT_KEY = "__tradejsCompactIndicatorsSnapshot";
2559
- var trimSeriesDeep = (value) => {
2560
- if (Array.isArray(value)) {
2561
- const trimmed = value.slice(-MAX_AI_SERIES_POINTS);
2562
- const isMatrix = trimmed.every((item) => Array.isArray(item));
2563
- if (isMatrix) {
2564
- return trimmed;
2565
- }
2566
- return trimmed.map(
2567
- (item) => item && typeof item === "object" ? trimSeriesDeep(item) : item
2568
- );
2569
- }
2570
- if (value && typeof value === "object") {
2571
- return Object.fromEntries(
2572
- Object.entries(value).map(([key, nested]) => [
2573
- key,
2574
- trimSeriesDeep(nested)
2575
- ])
2396
+ const dayKey = (0, import_time3.getRuntimeStorageDayKey)(existing.entryTimestamp);
2397
+ try {
2398
+ await Promise.all([
2399
+ (0, import_redis2.setData)(import_redis2.redisKeys.runtimeTrade(userName, existing.orderId), next, {
2400
+ expire: 0
2401
+ }),
2402
+ (0, import_redis2.setHashJsonField)(
2403
+ import_redis2.redisKeys.runtimeTradeBucket(userName, dayKey),
2404
+ existing.orderId,
2405
+ next,
2406
+ { expire: 0 }
2407
+ )
2408
+ ]);
2409
+ } catch (error) {
2410
+ import_logger5.logger.error(
2411
+ "runtime trade increase journal failed: %s %s",
2412
+ symbol,
2413
+ error?.message || String(error)
2576
2414
  );
2577
2415
  }
2578
- return value;
2416
+ return next;
2579
2417
  };
2580
- var buildCompactAiIndicatorsSnapshot = (value) => {
2581
- const compactSnapshot = value && typeof value === "object" ? value[COMPACT_INDICATORS_SNAPSHOT_SYMBOL] ?? value[COMPACT_INDICATORS_SNAPSHOT_KEY] : void 0;
2582
- if (typeof compactSnapshot === "function") {
2583
- return compactSnapshot({ limit: MAX_AI_SERIES_POINTS });
2418
+ var getActiveRuntimeTrade = async (params) => {
2419
+ const { userName, symbol, accountId, deploymentId } = params;
2420
+ if (!userName) {
2421
+ return null;
2584
2422
  }
2585
- return trimSeriesDeep(value);
2586
- };
2587
-
2588
- // src/aiMarketContext.ts
2589
- var toRecord = (value) => {
2590
- if (!value || typeof value !== "object" || Array.isArray(value)) {
2423
+ const activeRef = await (0, import_redis2.getData)(
2424
+ import_redis2.redisKeys.runtimeActiveTrade(userName, symbol, deploymentId ?? accountId),
2425
+ null
2426
+ );
2427
+ const orderId = String(activeRef?.orderId || "").trim();
2428
+ if (!orderId) {
2591
2429
  return null;
2592
2430
  }
2593
- return value;
2594
- };
2595
- var toFiniteNumber = (value) => {
2596
- const numeric = typeof value === "number" ? value : typeof value === "string" && value.trim() ? Number(value) : Number.NaN;
2597
- return Number.isFinite(numeric) ? numeric : null;
2598
- };
2599
- var roundTo = (value, decimals) => {
2600
- const factor = 10 ** decimals;
2601
- return Math.round(value * factor) / factor;
2602
- };
2603
- var buildMissingSpreadContext = () => ({
2604
- source: "payload.additionalIndicators.baseContext.relative.execution.venueSpread",
2605
- indicatorKey: "payload.additionalIndicators.baseContext.relative.execution.venueSpread",
2606
- available: false,
2607
- value: null,
2608
- zScore: null,
2609
- bps: null,
2610
- absBps: null,
2611
- bias: null,
2612
- severity: null
2613
- });
2614
- var buildSpreadContextFromSignal = (signal) => {
2615
- const baseContext = toRecord(signal.additionalIndicators?.baseContext);
2616
- const relative = toRecord(baseContext?.relative);
2617
- const execution = toRecord(relative?.execution);
2618
- const spread = toFiniteNumber(execution?.venueSpread);
2619
- const zScore = toFiniteNumber(execution?.venueSpreadZScore);
2620
- if (spread == null) {
2621
- return buildMissingSpreadContext();
2431
+ const existing = await (0, import_redis2.getData)(
2432
+ import_redis2.redisKeys.runtimeTrade(userName, orderId),
2433
+ null
2434
+ );
2435
+ if (!existing || existing.status !== "active") {
2436
+ await (0, import_redis2.delKey)(
2437
+ import_redis2.redisKeys.runtimeActiveTrade(userName, symbol, deploymentId ?? accountId)
2438
+ );
2439
+ return null;
2622
2440
  }
2623
- const value = roundTo(spread, 8);
2624
- const bps = roundTo(value * 1e4, 2);
2625
- const absBps = Math.abs(bps);
2626
- const bias = absBps < 1 ? "flat" : bps > 0 ? "coinbase_premium" : "binance_premium";
2627
- const severity = absBps >= 20 ? "wide" : absBps >= 5 ? "elevated" : "normal";
2628
- return {
2629
- source: "payload.additionalIndicators.baseContext.relative.execution.venueSpread",
2630
- indicatorKey: "payload.additionalIndicators.baseContext.relative.execution.venueSpread",
2631
- available: true,
2632
- value,
2633
- zScore,
2634
- bps,
2635
- absBps,
2636
- bias,
2637
- severity
2638
- };
2441
+ return existing;
2639
2442
  };
2640
- var buildTrueDeltaContextFromSignal = (signal) => {
2641
- const baseContext = toRecord(signal.additionalIndicators?.baseContext);
2642
- const participation = toRecord(baseContext?.participation);
2643
- const delta = toRecord(participation?.delta);
2644
- const source = String(delta?.source ?? "");
2645
- const isTrueDeltaSource = source === "kline_taker_volume" || source === "agg_trades" || source === "trades";
2646
- if (!delta || !isTrueDeltaSource) {
2647
- return {
2648
- source: source || null,
2649
- available: false,
2650
- buyPressurePct: null,
2651
- buyVolume: null,
2652
- sellVolume: null,
2653
- netDelta: null,
2654
- deltaPct: null,
2655
- signedVolumeZScore: null
2656
- };
2443
+ var markRuntimeTradeClosed = async (params) => {
2444
+ const {
2445
+ userName,
2446
+ expectedOrderId,
2447
+ symbol,
2448
+ strategy,
2449
+ exitPrice,
2450
+ exitTimestamp,
2451
+ closedPnl,
2452
+ exitType,
2453
+ accountId,
2454
+ deploymentId
2455
+ } = params;
2456
+ if (!userName) {
2457
+ return null;
2657
2458
  }
2658
- return {
2659
- source,
2660
- available: true,
2661
- buyPressurePct: toFiniteNumber(delta.buyPressurePct),
2662
- buyVolume: toFiniteNumber(delta.buyVolume),
2663
- sellVolume: toFiniteNumber(delta.sellVolume),
2664
- netDelta: toFiniteNumber(delta.netDelta),
2665
- deltaPct: toFiniteNumber(delta.deltaPct),
2666
- signedVolumeZScore: toFiniteNumber(delta.signedVolumeZScore)
2667
- };
2668
- };
2669
- var buildTradeFlowContextFromSignal = (signal) => {
2670
- const baseContext = toRecord(signal.additionalIndicators?.baseContext);
2671
- const participation = toRecord(baseContext?.participation);
2672
- const tradeFlow = toRecord(participation?.tradeFlow);
2673
- if (!tradeFlow) {
2674
- return {
2675
- source: null,
2676
- available: false,
2677
- interval: null,
2678
- stale: null,
2679
- trades: null,
2680
- buyPressurePct: null,
2681
- netBaseDelta: null,
2682
- netQuoteDelta: null
2683
- };
2459
+ const existing = expectedOrderId ? await (0, import_redis2.getDataStrict)(
2460
+ import_redis2.redisKeys.runtimeTrade(userName, expectedOrderId)
2461
+ ) : await getActiveRuntimeTrade({
2462
+ userName,
2463
+ symbol,
2464
+ accountId,
2465
+ deploymentId
2466
+ });
2467
+ if (!existing || expectedOrderId && existing.orderId !== expectedOrderId) {
2468
+ return null;
2684
2469
  }
2685
- return {
2686
- source: String(tradeFlow.source ?? ""),
2687
- available: true,
2688
- interval: String(tradeFlow.interval ?? ""),
2689
- stale: typeof tradeFlow.stale === "boolean" ? tradeFlow.stale : null,
2690
- trades: toFiniteNumber(tradeFlow.trades),
2691
- buyPressurePct: toFiniteNumber(tradeFlow.buyPressurePct),
2692
- netBaseDelta: toFiniteNumber(tradeFlow.netBaseDelta),
2693
- netQuoteDelta: toFiniteNumber(tradeFlow.netQuoteDelta)
2470
+ const orderId = existing.orderId;
2471
+ if (strategy && existing.strategy !== strategy) {
2472
+ return null;
2473
+ }
2474
+ const resolvedExitPrice = typeof exitPrice === "number" && Number.isFinite(exitPrice) ? exitPrice : existing.currentPrice ?? existing.entryPrice;
2475
+ const resolvedClosedPnl = typeof closedPnl === "number" && Number.isFinite(closedPnl) ? closedPnl : typeof resolvedExitPrice === "number" && Number.isFinite(resolvedExitPrice) ? calculateClosedPnl({
2476
+ direction: existing.direction,
2477
+ entryPrice: existing.entryPrice,
2478
+ exitPrice: resolvedExitPrice,
2479
+ qty: existing.qty
2480
+ }) : existing.closedPnl ?? existing.currentPnl ?? null;
2481
+ const next = expectedOrderId && existing.status === "closed" ? existing : {
2482
+ ...existing,
2483
+ status: "closed",
2484
+ currentPrice: resolvedExitPrice,
2485
+ currentPnl: resolvedClosedPnl,
2486
+ closedPnl: resolvedClosedPnl,
2487
+ exitPrice: resolvedExitPrice,
2488
+ exitTimestamp: typeof exitTimestamp === "number" && Number.isFinite(exitTimestamp) ? exitTimestamp : now(),
2489
+ exitType: exitType ?? existing.exitType ?? null,
2490
+ lastSyncedAt: now()
2694
2491
  };
2695
- };
2696
- var buildMarketBreadthContextFromSignal = (signal) => {
2697
- const baseContext = toRecord(signal.additionalIndicators?.baseContext);
2698
- const relative = toRecord(baseContext?.relative);
2699
- const breadth = toRecord(relative?.marketBreadth);
2700
- if (!breadth) {
2701
- return {
2702
- source: null,
2703
- available: false,
2704
- universe: null,
2705
- interval: null,
2706
- stale: null,
2707
- symbolsCount: null,
2708
- advanceDeclineRatio: null,
2709
- pctAboveMa20: null,
2710
- pctAboveMa50: null,
2711
- equalWeightedReturn: null,
2712
- volumeWeightedReturn: null,
2713
- dispersion: null
2714
- };
2492
+ const dayKey = (0, import_time3.getRuntimeStorageDayKey)(existing.entryTimestamp);
2493
+ const closeDayKey = (0, import_time3.getRuntimeStorageDayKey)(next.exitTimestamp);
2494
+ try {
2495
+ await Promise.all([
2496
+ (expectedOrderId ? import_redis2.setDataStrict : import_redis2.setData)(
2497
+ import_redis2.redisKeys.runtimeTrade(userName, orderId),
2498
+ next,
2499
+ {
2500
+ expire: import_constants3.TTL_1M
2501
+ }
2502
+ ),
2503
+ (expectedOrderId ? import_redis2.setHashJsonFieldStrict : import_redis2.setHashJsonField)(
2504
+ import_redis2.redisKeys.runtimeTradeBucket(userName, dayKey),
2505
+ orderId,
2506
+ next,
2507
+ { expire: import_constants3.TTL_1M }
2508
+ ),
2509
+ (expectedOrderId ? import_redis2.setHashJsonFieldStrict : import_redis2.setHashJsonField)(
2510
+ import_redis2.redisKeys.runtimeClosedTradeBucket(userName, closeDayKey),
2511
+ orderId,
2512
+ next,
2513
+ { expire: import_constants3.TTL_1M }
2514
+ ),
2515
+ expectedOrderId ? (0, import_redis2.deleteRuntimeTradeReference)(
2516
+ import_redis2.redisKeys.runtimeActiveTrade(
2517
+ userName,
2518
+ symbol,
2519
+ deploymentId ?? accountId
2520
+ ),
2521
+ expectedOrderId
2522
+ ) : (0, import_redis2.delKey)(
2523
+ import_redis2.redisKeys.runtimeActiveTrade(
2524
+ userName,
2525
+ symbol,
2526
+ deploymentId ?? accountId
2527
+ )
2528
+ )
2529
+ ]);
2530
+ } catch (error) {
2531
+ if (expectedOrderId) throw error;
2532
+ import_logger5.logger.error(
2533
+ "runtime trade close journal failed: %s %s",
2534
+ symbol,
2535
+ error?.message || String(error)
2536
+ );
2715
2537
  }
2716
- return {
2717
- source: String(breadth.source ?? ""),
2718
- available: true,
2719
- universe: String(breadth.universe ?? ""),
2720
- interval: String(breadth.interval ?? ""),
2721
- stale: typeof breadth.stale === "boolean" ? breadth.stale : null,
2722
- symbolsCount: toFiniteNumber(breadth.symbolsCount),
2723
- advanceDeclineRatio: toFiniteNumber(breadth.advanceDeclineRatio),
2724
- pctAboveMa20: toFiniteNumber(breadth.pctAboveMa20),
2725
- pctAboveMa50: toFiniteNumber(breadth.pctAboveMa50),
2726
- equalWeightedReturn: toFiniteNumber(breadth.equalWeightedReturn),
2727
- volumeWeightedReturn: toFiniteNumber(breadth.volumeWeightedReturn),
2728
- dispersion: toFiniteNumber(breadth.dispersion)
2729
- };
2538
+ return next;
2730
2539
  };
2731
- var buildMarketBreadthsContextFromSignal = (signal) => {
2732
- const baseContext = toRecord(signal.additionalIndicators?.baseContext);
2733
- const relative = toRecord(baseContext?.relative);
2734
- const breadths = toRecord(relative?.marketBreadths);
2735
- const build = (key) => {
2736
- const breadth = toRecord(breadths?.[key]);
2737
- if (!breadth) {
2540
+
2541
+ // src/strategy/portfolioRuntime.ts
2542
+ var coordinators = /* @__PURE__ */ new WeakMap();
2543
+ async function observeRuntimePortfolio(connector, binding, asOf, reservations, observedNow = Date.now) {
2544
+ if (!connector.getOpenPositionPnl || !connector.getEntryExecutions)
2545
+ throw new Error(
2546
+ "Portfolio policies require position marks and entry executions"
2547
+ );
2548
+ const orders = await Promise.all(
2549
+ reservations.map(async (r) => {
2550
+ if (r.submitted && connector.getEntryOrderObservation) {
2551
+ const fact2 = await connector.getEntryOrderObservation({
2552
+ symbol: r.symbol,
2553
+ orderId: r.id
2554
+ });
2555
+ if (fact2) return fact2;
2556
+ }
2557
+ const executions = r.submitted ? await connector.getEntryExecutions({
2558
+ symbol: r.symbol,
2559
+ startTime: r.timestamp,
2560
+ endTime: asOf
2561
+ }) : [];
2562
+ const filledQty = executions.filter(
2563
+ (e) => (e.orderLinkId === r.id || e.orderId === r.id) && e.direction === r.direction
2564
+ ).reduce((sum, e) => sum + e.qty, 0);
2738
2565
  return {
2739
- source: null,
2740
- available: false,
2741
- universe: null,
2742
- interval: null,
2743
- stale: null,
2744
- symbolsCount: null,
2745
- advanceDeclineRatio: null,
2746
- pctAboveMa20: null,
2747
- pctAboveMa50: null,
2748
- equalWeightedReturn: null,
2749
- volumeWeightedReturn: null,
2750
- dispersion: null
2566
+ id: r.id,
2567
+ status: filledQty >= r.qty ? "filled" : filledQty > 0 ? "partial" : "pending",
2568
+ filledQty,
2569
+ positionId: r.id
2570
+ };
2571
+ })
2572
+ );
2573
+ const snapshots2 = await connector.getOpenPositionPnl();
2574
+ const positions = await Promise.all(
2575
+ snapshots2.map(async (p) => {
2576
+ const reserved = reservations.find(
2577
+ (r) => r.symbol === p.symbol && r.direction === p.direction && orders.some(
2578
+ (o) => o.id === r.id && o.filledQty > 0 && p.qty <= o.filledQty * (1 + 1e-8)
2579
+ )
2580
+ );
2581
+ const trade = await getActiveRuntimeTrade({
2582
+ ...binding,
2583
+ symbol: p.symbol
2584
+ });
2585
+ 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;
2586
+ return {
2587
+ id: reserved?.id ?? owner?.orderId ?? `unknown:${p.symbol}:${p.direction}`,
2588
+ strategy: reserved?.strategy ?? owner?.strategy ?? null,
2589
+ symbol: p.symbol,
2590
+ direction: p.direction,
2591
+ qty: p.qty,
2592
+ entryPrice: p.price,
2593
+ markPrice: p.currentPrice,
2594
+ markedAt: p.observedAt ?? null,
2595
+ stopLossPrice: p.slPrice ?? p.stopLossPrice ?? null
2751
2596
  };
2597
+ })
2598
+ );
2599
+ return { asOf: Math.max(asOf, observedNow()), positions, orders };
2600
+ }
2601
+ function persistentStore(key) {
2602
+ const raw = /* @__PURE__ */ new WeakMap();
2603
+ return {
2604
+ async read() {
2605
+ const value = await import_redis3.portfolioStateStorage.read(key);
2606
+ if (value === null) return null;
2607
+ const state = JSON.parse(value);
2608
+ (0, import_portfolio2.assertPortfolioState)(state);
2609
+ raw.set(state, value);
2610
+ return state;
2611
+ },
2612
+ async compare(previous, next) {
2613
+ const expected = previous === null ? null : raw.get(previous);
2614
+ if (expected === void 0)
2615
+ throw new Error("Portfolio CAS requires a storage snapshot");
2616
+ return import_redis3.portfolioStateStorage.compare(key, expected, JSON.stringify(next));
2752
2617
  }
2753
- return {
2754
- source: String(breadth.source ?? ""),
2755
- available: true,
2756
- universe: String(breadth.universe ?? ""),
2757
- interval: String(breadth.interval ?? ""),
2758
- stale: typeof breadth.stale === "boolean" ? breadth.stale : null,
2759
- symbolsCount: toFiniteNumber(breadth.symbolsCount),
2760
- advanceDeclineRatio: toFiniteNumber(breadth.advanceDeclineRatio),
2761
- pctAboveMa20: toFiniteNumber(breadth.pctAboveMa20),
2762
- pctAboveMa50: toFiniteNumber(breadth.pctAboveMa50),
2763
- equalWeightedReturn: toFiniteNumber(breadth.equalWeightedReturn),
2764
- volumeWeightedReturn: toFiniteNumber(breadth.volumeWeightedReturn),
2765
- dispersion: toFiniteNumber(breadth.dispersion)
2766
- };
2767
2618
  };
2768
- return {
2769
- top5: build("top5"),
2770
- top10: build("top10"),
2771
- top30: build("top30"),
2772
- top50: build("top50"),
2773
- top100: build("top100")
2619
+ }
2620
+ function getPortfolioRuntime({
2621
+ value,
2622
+ connectorName,
2623
+ strategy,
2624
+ connector,
2625
+ env,
2626
+ userName,
2627
+ accountId,
2628
+ deploymentId
2629
+ }) {
2630
+ const policy = (0, import_portfolio2.parsePortfolioPolicy)(value);
2631
+ if (!policy) return void 0;
2632
+ const replay = (env === "BACKTEST" || env === "PARITY") && Reflect.get(connector, "__tradejsReplayConnector") === true;
2633
+ if ((env === "BACKTEST" || env === "PARITY") && (!replay || !connector.getPortfolioObservation))
2634
+ throw new Error(
2635
+ "Portfolio policies require shared portfolio replay; isolated symbol backtests are unsupported"
2636
+ );
2637
+ if (!replay && (!userName || !accountId || !deploymentId || !connector.getOpenPositionPnl || !connector.getEntryExecutions))
2638
+ throw new Error(
2639
+ "Portfolio runtime requires exact user/account/deployment binding, marks and executions"
2640
+ );
2641
+ const binding = {
2642
+ userName,
2643
+ accountId,
2644
+ deploymentId
2774
2645
  };
2775
- };
2776
- var buildTargetVsBtcContextFromSignal = (signal) => {
2777
- const baseContext = toRecord(signal.additionalIndicators?.baseContext);
2778
- const relative = toRecord(baseContext?.relative);
2779
- const targetVsBtc = toRecord(relative?.targetVsBtc);
2780
- if (!targetVsBtc) {
2781
- return {
2782
- source: null,
2783
- available: false,
2784
- ratioReturn1h: null,
2785
- ratioReturn4h: null,
2786
- ratioReturn24h: null,
2787
- alphaVsBtc1h: null,
2788
- alphaVsBtc4h: null,
2789
- alphaVsBtc24h: null,
2790
- betaToBtc20: null,
2791
- correlationToBtc20: null,
2792
- ratioTrend: null
2793
- };
2646
+ const scope = policy.scope === "strategy" ? strategy : policy.scope === "account" ? "account" : [...policy.scope.strategies].sort();
2647
+ const key = (0, import_node_crypto2.createHash)("sha256").update(JSON.stringify([binding, connectorName, connector.universe, scope])).digest("hex");
2648
+ const fingerprint3 = (0, import_portfolio2.portfolioFingerprint)(policy, strategy);
2649
+ const cacheKey = key;
2650
+ let registry = coordinators.get(connector);
2651
+ if (!registry) {
2652
+ registry = /* @__PURE__ */ new Map();
2653
+ coordinators.set(connector, registry);
2654
+ }
2655
+ const existing = registry.get(cacheKey);
2656
+ if (existing) {
2657
+ if (existing.fingerprint !== fingerprint3)
2658
+ throw new Error("Conflicting portfolio configuration for the same scope");
2659
+ return existing.coordinator;
2660
+ }
2661
+ const coordinator = createPortfolioCoordinator({
2662
+ policy,
2663
+ strategy,
2664
+ store: replay ? createMemoryPortfolioStore() : persistentStore(key),
2665
+ execution: {
2666
+ async confirmClosed(intent, asOf) {
2667
+ if (replay) return;
2668
+ const position = intent.position;
2669
+ await markRuntimeTradeClosed({
2670
+ ...binding,
2671
+ strategy: position.strategy,
2672
+ expectedOrderId: position.id,
2673
+ symbol: position.symbol,
2674
+ exitPrice: position.markPrice,
2675
+ exitTimestamp: asOf,
2676
+ exitType: "exit"
2677
+ });
2678
+ },
2679
+ observe: (asOf, reservations) => replay ? connector.getPortfolioObservation(asOf, reservations) : observeRuntimePortfolio(connector, binding, asOf, reservations),
2680
+ async close(position, intent, asOf) {
2681
+ if (!replay) {
2682
+ const trade = await getActiveRuntimeTrade({
2683
+ ...binding,
2684
+ symbol: position.symbol
2685
+ });
2686
+ if (!trade || trade.orderId !== position.id)
2687
+ throw new Error("Portfolio close ownership changed");
2688
+ }
2689
+ await connector.closePosition({
2690
+ symbol: position.symbol,
2691
+ direction: position.direction,
2692
+ price: position.markPrice,
2693
+ timestamp: asOf,
2694
+ signal: {
2695
+ signalId: `${position.id}:${intent.reason}`,
2696
+ strategy: position.strategy,
2697
+ symbol: position.symbol,
2698
+ direction: position.direction,
2699
+ interval: "1",
2700
+ timestamp: asOf,
2701
+ prices: {
2702
+ riskRatio: 0,
2703
+ currentPrice: position.markPrice,
2704
+ takeProfitPrice: position.markPrice,
2705
+ stopLossPrice: position.markPrice
2706
+ },
2707
+ figures: {},
2708
+ indicators: {},
2709
+ additionalIndicators: { portfolioExitReason: intent.reason }
2710
+ }
2711
+ });
2712
+ }
2713
+ }
2714
+ });
2715
+ registry.set(cacheKey, { fingerprint: fingerprint3, coordinator });
2716
+ return coordinator;
2717
+ }
2718
+
2719
+ // src/strategyHelpers/tickerProfiles.ts
2720
+ var import_tickerProfiles = require("@tradejs/infra/tickerProfiles");
2721
+ var import_tickerProfiles2 = require("@tradejs/core/tickerProfiles");
2722
+
2723
+ // src/strategy/manifests.ts
2724
+ var import_indicators = require("@tradejs/core/indicators");
2725
+ var SHARED_STRATEGY_REGISTRY_KEY = "__tradejsNodeSharedStrategyRegistryV1__";
2726
+ var sharedRegistryScope = globalThis;
2727
+ var sharedStrategyRegistry = sharedRegistryScope[SHARED_STRATEGY_REGISTRY_KEY] ?? (sharedRegistryScope[SHARED_STRATEGY_REGISTRY_KEY] = {
2728
+ registryStateByProjectRoot: /* @__PURE__ */ new Map()
2729
+ });
2730
+ var createStrategyRegistryState = () => ({
2731
+ strategyCreators: /* @__PURE__ */ new Map(),
2732
+ strategyManifestsMap: /* @__PURE__ */ new Map(),
2733
+ strategyEntriesMap: /* @__PURE__ */ new Map(),
2734
+ strategySourcesMap: /* @__PURE__ */ new Map(),
2735
+ strategyModuleResolutions: /* @__PURE__ */ new Map(),
2736
+ pluginsLoadPromise: null
2737
+ });
2738
+ var registryStateByProjectRoot2 = sharedStrategyRegistry.registryStateByProjectRoot;
2739
+ var getStrategyRegistryState = (cwd = getTradejsProjectCwd()) => {
2740
+ const projectRoot = getTradejsProjectCwd(cwd);
2741
+ let state = registryStateByProjectRoot2.get(projectRoot);
2742
+ if (!state) {
2743
+ state = createStrategyRegistryState();
2744
+ registryStateByProjectRoot2.set(projectRoot, state);
2794
2745
  }
2795
2746
  return {
2796
- source: String(targetVsBtc.source ?? ""),
2797
- available: true,
2798
- ratioReturn1h: toFiniteNumber(targetVsBtc.ratioReturn1h),
2799
- ratioReturn4h: toFiniteNumber(targetVsBtc.ratioReturn4h),
2800
- ratioReturn24h: toFiniteNumber(targetVsBtc.ratioReturn24h),
2801
- alphaVsBtc1h: toFiniteNumber(targetVsBtc.alphaVsBtc1h),
2802
- alphaVsBtc4h: toFiniteNumber(targetVsBtc.alphaVsBtc4h),
2803
- alphaVsBtc24h: toFiniteNumber(targetVsBtc.alphaVsBtc24h),
2804
- betaToBtc20: toFiniteNumber(targetVsBtc.betaToBtc20),
2805
- correlationToBtc20: toFiniteNumber(targetVsBtc.correlationToBtc20),
2806
- ratioTrend: typeof targetVsBtc.ratioTrend === "string" ? targetVsBtc.ratioTrend : null
2747
+ projectRoot,
2748
+ state
2807
2749
  };
2808
2750
  };
2809
- var buildBtcAltRegimeContextFromSignal = (signal) => {
2810
- const baseContext = toRecord(signal.additionalIndicators?.baseContext);
2811
- const relative = toRecord(baseContext?.relative);
2812
- const btcAltRegime = toRecord(relative?.btcAltRegime);
2813
- if (!btcAltRegime) {
2814
- return {
2815
- source: null,
2816
- available: false,
2817
- universe: null,
2818
- interval: null,
2819
- stale: null,
2820
- regime: null,
2821
- btcReturn24h: null,
2822
- altBasketReturn24h: null,
2823
- btcVsAltReturn24h: null,
2824
- btcTurnoverShare24h: null,
2825
- btcTurnoverShareChange24h: null,
2826
- altVolToBtcVol24h: null,
2827
- altDispersion24h: null
2828
- };
2829
- }
2751
+ var toUniqueModules2 = (modules = []) => [
2752
+ ...new Set(modules.map((moduleName) => moduleName.trim()).filter(Boolean))
2753
+ ];
2754
+ var getConfiguredPluginModuleNames = async (cwd = getTradejsProjectCwd()) => {
2755
+ const config = await loadTradejsConfig(cwd);
2830
2756
  return {
2831
- source: String(btcAltRegime.source ?? ""),
2832
- available: true,
2833
- universe: String(btcAltRegime.universe ?? ""),
2834
- interval: String(btcAltRegime.interval ?? ""),
2835
- stale: typeof btcAltRegime.stale === "boolean" ? btcAltRegime.stale : null,
2836
- regime: typeof btcAltRegime.regime === "string" ? btcAltRegime.regime : null,
2837
- btcReturn24h: toFiniteNumber(btcAltRegime.btcReturn24h),
2838
- altBasketReturn24h: toFiniteNumber(btcAltRegime.altBasketReturn24h),
2839
- btcVsAltReturn24h: toFiniteNumber(btcAltRegime.btcVsAltReturn24h),
2840
- btcTurnoverShare24h: toFiniteNumber(btcAltRegime.btcTurnoverShare24h),
2841
- btcTurnoverShareChange24h: toFiniteNumber(
2842
- btcAltRegime.btcTurnoverShareChange24h
2843
- ),
2844
- altVolToBtcVol24h: toFiniteNumber(btcAltRegime.altVolToBtcVol24h),
2845
- altDispersion24h: toFiniteNumber(btcAltRegime.altDispersion24h)
2757
+ strategyModules: toUniqueModules2(config.strategies),
2758
+ indicatorModules: toUniqueModules2(config.indicators)
2846
2759
  };
2847
2760
  };
2848
- var buildCmcGlobalContextFromSignal = (signal) => {
2849
- const baseContext = toRecord(signal.additionalIndicators?.baseContext);
2850
- const relative = toRecord(baseContext?.relative);
2851
- const cmcGlobal = toRecord(relative?.cmcGlobal);
2852
- if (!cmcGlobal) {
2853
- return {
2854
- source: null,
2855
- available: false,
2856
- interval: null,
2857
- asOfTs: null,
2858
- stale: null,
2859
- totalMarketCapUsd: null,
2860
- totalVolumeUsd: null,
2861
- totalVolumeReportedUsd: null,
2862
- altMarketCapUsd: null,
2863
- altVolumeUsd: null,
2864
- altVolumeReportedUsd: null,
2865
- btcDominancePct: null,
2866
- ethDominancePct: null,
2867
- btcDominanceChange24hPct: null,
2868
- ethDominanceChange24hPct: null,
2869
- altMarketCapChange24hPct: null,
2870
- altVolumeChange24hPct: null,
2871
- activeCryptocurrencies: null,
2872
- activeExchanges: null,
2873
- activeMarketPairs: null,
2874
- altLiquidityRegime: null
2875
- };
2761
+ var extractModuleEntries = (moduleExport, key) => {
2762
+ if (!moduleExport || typeof moduleExport !== "object") {
2763
+ return null;
2876
2764
  }
2877
- return {
2878
- source: String(cmcGlobal.source ?? ""),
2879
- available: true,
2880
- interval: typeof cmcGlobal.interval === "string" ? cmcGlobal.interval : null,
2881
- asOfTs: toFiniteNumber(cmcGlobal.asOfTs),
2882
- stale: typeof cmcGlobal.stale === "boolean" ? cmcGlobal.stale : null,
2883
- totalMarketCapUsd: toFiniteNumber(cmcGlobal.totalMarketCapUsd),
2884
- totalVolumeUsd: toFiniteNumber(cmcGlobal.totalVolumeUsd),
2885
- totalVolumeReportedUsd: toFiniteNumber(cmcGlobal.totalVolumeReportedUsd),
2886
- altMarketCapUsd: toFiniteNumber(cmcGlobal.altMarketCapUsd),
2887
- altVolumeUsd: toFiniteNumber(cmcGlobal.altVolumeUsd),
2888
- altVolumeReportedUsd: toFiniteNumber(cmcGlobal.altVolumeReportedUsd),
2889
- btcDominancePct: toFiniteNumber(cmcGlobal.btcDominancePct),
2890
- ethDominancePct: toFiniteNumber(cmcGlobal.ethDominancePct),
2891
- btcDominanceChange24hPct: toFiniteNumber(
2892
- cmcGlobal.btcDominanceChange24hPct
2893
- ),
2894
- ethDominanceChange24hPct: toFiniteNumber(
2895
- cmcGlobal.ethDominanceChange24hPct
2896
- ),
2897
- altMarketCapChange24hPct: toFiniteNumber(
2898
- cmcGlobal.altMarketCapChange24hPct
2765
+ const candidate = moduleExport;
2766
+ if (Array.isArray(candidate[key])) {
2767
+ return candidate[key];
2768
+ }
2769
+ const defaultExport = candidate.default;
2770
+ if (defaultExport && Array.isArray(defaultExport[key])) {
2771
+ return defaultExport[key];
2772
+ }
2773
+ return null;
2774
+ };
2775
+ var extractStrategyPluginDefinition = (moduleExport) => {
2776
+ const strategyEntries = extractModuleEntries(
2777
+ moduleExport,
2778
+ "strategyEntries"
2779
+ );
2780
+ return strategyEntries ? { strategyEntries } : null;
2781
+ };
2782
+ var extractIndicatorPluginDefinition = (moduleExport) => {
2783
+ const indicatorEntries = extractModuleEntries(
2784
+ moduleExport,
2785
+ "indicatorEntries"
2786
+ );
2787
+ return indicatorEntries ? { indicatorEntries } : null;
2788
+ };
2789
+ var validateStrategyEntries = (moduleName, entries, state) => {
2790
+ const issues = [];
2791
+ const names = /* @__PURE__ */ new Set();
2792
+ entries.forEach((entry, index) => {
2793
+ const entryPath = `${moduleName}.strategyEntries[${index}]`;
2794
+ const strategyName = entry?.manifest?.name;
2795
+ if (typeof strategyName !== "string" || !strategyName.trim()) {
2796
+ issues.push(`${entryPath}: manifest.name is required`);
2797
+ } else if (names.has(strategyName) || state?.strategyEntriesMap.has(strategyName)) {
2798
+ issues.push(`${entryPath}: duplicate strategy ${strategyName}`);
2799
+ } else {
2800
+ names.add(strategyName);
2801
+ }
2802
+ if (!entry || typeof entry !== "object" || !entry.defaults || typeof entry.defaults !== "object" || Array.isArray(entry.defaults)) {
2803
+ issues.push(`${entryPath}: defaults must be an object`);
2804
+ }
2805
+ if (typeof entry?.parseConfig !== "function") {
2806
+ issues.push(`${entryPath}: parseConfig is required`);
2807
+ }
2808
+ if (typeof entry?.createCore !== "function") {
2809
+ issues.push(`${entryPath}: createCore is required`);
2810
+ }
2811
+ });
2812
+ return issues;
2813
+ };
2814
+ var registerEntries2 = (entries, source, state) => {
2815
+ for (const entry of entries) {
2816
+ const strategyName = entry.manifest.name;
2817
+ state.strategyManifestsMap.set(strategyName, entry.manifest);
2818
+ state.strategyEntriesMap.set(strategyName, entry);
2819
+ state.strategySourcesMap.set(strategyName, source);
2820
+ materializeStrategyCreator(strategyName, state);
2821
+ }
2822
+ };
2823
+ var materializeStrategyCreator = (strategyName, state) => {
2824
+ if (state.strategyCreators.has(strategyName) || !sharedStrategyRegistry.strategyRuntimeFactory) {
2825
+ return;
2826
+ }
2827
+ const entry = state.strategyEntriesMap.get(strategyName);
2828
+ if (!entry) return;
2829
+ state.strategyCreators.set(
2830
+ strategyName,
2831
+ sharedStrategyRegistry.strategyRuntimeFactory({
2832
+ strategyName,
2833
+ defaults: entry.defaults,
2834
+ createCore: entry.createCore,
2835
+ manifest: entry.manifest,
2836
+ detectorKey: entry.detectorKey,
2837
+ detectorNoSignalSkipReason: entry.detectorNoSignalSkipReason,
2838
+ resolveRegisteredManifest: (name) => state.strategyManifestsMap.get(name)
2839
+ })
2840
+ );
2841
+ };
2842
+ var setStrategyRuntimeFactory = (factory) => {
2843
+ sharedStrategyRegistry.strategyRuntimeFactory = factory;
2844
+ for (const state of registryStateByProjectRoot2.values()) {
2845
+ for (const strategyName of state.strategyEntriesMap.keys()) {
2846
+ materializeStrategyCreator(strategyName, state);
2847
+ }
2848
+ }
2849
+ };
2850
+ var importStrategyPluginModule = async (moduleName, cwd = getTradejsProjectCwd()) => {
2851
+ if (typeof importTradejsModule === "function") {
2852
+ return importTradejsModule(moduleName, cwd);
2853
+ }
2854
+ return import(
2855
+ /* webpackIgnore: true */
2856
+ moduleName
2857
+ );
2858
+ };
2859
+ var resolveStrategyModuleEntry = async ({
2860
+ strategyName,
2861
+ moduleName,
2862
+ cwd
2863
+ }) => {
2864
+ const { projectRoot, state } = getStrategyRegistryState(cwd);
2865
+ const source = moduleName.trim();
2866
+ const resolvedModuleName = resolvePluginModuleSpecifier(source, projectRoot);
2867
+ const cacheKey = `${resolvedModuleName}\0${strategyName}`;
2868
+ const cached = state.strategyModuleResolutions.get(cacheKey);
2869
+ if (cached) return cached;
2870
+ const resolution = (async () => {
2871
+ const moduleExport = await importStrategyPluginModule(
2872
+ resolvedModuleName,
2873
+ projectRoot
2874
+ );
2875
+ const pluginDefinition = extractStrategyPluginDefinition(moduleExport);
2876
+ if (!pluginDefinition) {
2877
+ throw new Error(`${source}: export { strategyEntries } is missing`);
2878
+ }
2879
+ const issues = validateStrategyEntries(
2880
+ source,
2881
+ pluginDefinition.strategyEntries
2882
+ );
2883
+ if (issues.length > 0) {
2884
+ throw new Error(
2885
+ ["Invalid TradeJS strategy module:", ...issues].join("\n")
2886
+ );
2887
+ }
2888
+ const entry = pluginDefinition.strategyEntries.find(
2889
+ (candidate) => candidate.manifest.name === strategyName
2890
+ );
2891
+ if (!entry) {
2892
+ throw new Error(`${source}: strategy ${strategyName} is missing`);
2893
+ }
2894
+ const factory = sharedStrategyRegistry.strategyRuntimeFactory;
2895
+ if (!factory) {
2896
+ throw new Error("Strategy runtime factory is not configured");
2897
+ }
2898
+ const manifestsByName = new Map(
2899
+ pluginDefinition.strategyEntries.map(
2900
+ (candidate) => [candidate.manifest.name, candidate.manifest]
2901
+ )
2902
+ );
2903
+ return {
2904
+ entry,
2905
+ creator: factory({
2906
+ strategyName,
2907
+ defaults: entry.defaults,
2908
+ createCore: entry.createCore,
2909
+ manifest: entry.manifest,
2910
+ detectorKey: entry.detectorKey,
2911
+ detectorNoSignalSkipReason: entry.detectorNoSignalSkipReason,
2912
+ resolveRegisteredManifest: (name) => manifestsByName.get(name) ?? state.strategyManifestsMap.get(name)
2913
+ }),
2914
+ source
2915
+ };
2916
+ })();
2917
+ state.strategyModuleResolutions.set(cacheKey, resolution);
2918
+ try {
2919
+ return await resolution;
2920
+ } catch (error) {
2921
+ state.strategyModuleResolutions.delete(cacheKey);
2922
+ throw error;
2923
+ }
2924
+ };
2925
+ var ensureStrategyPluginsLoaded = async (cwd = getTradejsProjectCwd()) => {
2926
+ const { projectRoot, state } = getStrategyRegistryState(cwd);
2927
+ if (!state.pluginsLoadPromise) {
2928
+ (0, import_indicators.resetIndicatorRegistryCache)(projectRoot);
2929
+ state.pluginsLoadPromise = (async () => {
2930
+ const { strategyModules, indicatorModules } = await getConfiguredPluginModuleNames(projectRoot);
2931
+ const strategySet = new Set(strategyModules);
2932
+ const indicatorSet = new Set(indicatorModules);
2933
+ const pluginModuleNames = [
2934
+ .../* @__PURE__ */ new Set([...strategyModules, ...indicatorModules])
2935
+ ];
2936
+ if (!pluginModuleNames.length) {
2937
+ return;
2938
+ }
2939
+ const issues = [];
2940
+ for (const moduleName of pluginModuleNames) {
2941
+ try {
2942
+ const resolvedModuleName = resolvePluginModuleSpecifier(
2943
+ moduleName,
2944
+ projectRoot
2945
+ );
2946
+ const moduleExport = await importStrategyPluginModule(
2947
+ resolvedModuleName,
2948
+ projectRoot
2949
+ );
2950
+ if (strategySet.has(moduleName)) {
2951
+ const pluginDefinition = extractStrategyPluginDefinition(moduleExport);
2952
+ if (!pluginDefinition) {
2953
+ issues.push(
2954
+ `${moduleName}: export { strategyEntries } is missing`
2955
+ );
2956
+ } else {
2957
+ const entryIssues = validateStrategyEntries(
2958
+ moduleName,
2959
+ pluginDefinition.strategyEntries,
2960
+ state
2961
+ );
2962
+ issues.push(...entryIssues);
2963
+ if (entryIssues.length === 0) {
2964
+ registerEntries2(
2965
+ pluginDefinition.strategyEntries,
2966
+ moduleName,
2967
+ state
2968
+ );
2969
+ }
2970
+ }
2971
+ }
2972
+ if (indicatorSet.has(moduleName)) {
2973
+ const indicatorPluginDefinition = extractIndicatorPluginDefinition(moduleExport);
2974
+ if (!indicatorPluginDefinition) {
2975
+ issues.push(
2976
+ `${moduleName}: export { indicatorEntries } is missing`
2977
+ );
2978
+ } else {
2979
+ (0, import_indicators.registerIndicatorEntries)(
2980
+ indicatorPluginDefinition.indicatorEntries,
2981
+ moduleName,
2982
+ projectRoot
2983
+ );
2984
+ }
2985
+ }
2986
+ if (!strategySet.has(moduleName) && !indicatorSet.has(moduleName)) {
2987
+ issues.push(`${moduleName}: plugin is not declared in config`);
2988
+ }
2989
+ } catch (error) {
2990
+ issues.push(`${moduleName}: failed to import: ${String(error)}`);
2991
+ }
2992
+ }
2993
+ if (issues.length > 0) {
2994
+ throw new Error(
2995
+ ["Invalid TradeJS plugin catalog:", ...issues].join("\n")
2996
+ );
2997
+ }
2998
+ })();
2999
+ }
3000
+ await state.pluginsLoadPromise;
3001
+ };
3002
+ var getStrategyCreator = async (name, cwd = getTradejsProjectCwd(), moduleName) => {
3003
+ if (moduleName?.trim()) {
3004
+ await ensureStrategyPluginsLoaded(cwd);
3005
+ return (await resolveStrategyModuleEntry({ strategyName: name, moduleName, cwd })).creator;
3006
+ }
3007
+ await ensureStrategyPluginsLoaded(cwd);
3008
+ const { state } = getStrategyRegistryState(cwd);
3009
+ return state.strategyCreators.get(name);
3010
+ };
3011
+ var getStrategyEntry = async (name, cwd = getTradejsProjectCwd(), moduleName) => {
3012
+ if (moduleName?.trim()) {
3013
+ await ensureStrategyPluginsLoaded(cwd);
3014
+ return (await resolveStrategyModuleEntry({ strategyName: name, moduleName, cwd })).entry;
3015
+ }
3016
+ await ensureStrategyPluginsLoaded(cwd);
3017
+ const { state } = getStrategyRegistryState(cwd);
3018
+ return state.strategyEntriesMap.get(name);
3019
+ };
3020
+ var getStrategyPluginSource = async (name, cwd = getTradejsProjectCwd(), moduleName) => {
3021
+ if (moduleName?.trim()) {
3022
+ await ensureStrategyPluginsLoaded(cwd);
3023
+ return (await resolveStrategyModuleEntry({ strategyName: name, moduleName, cwd })).source;
3024
+ }
3025
+ await ensureStrategyPluginsLoaded(cwd);
3026
+ const { state } = getStrategyRegistryState(cwd);
3027
+ return state.strategySourcesMap.get(name);
3028
+ };
3029
+ var getStrategyManifest = (name, cwd = getTradejsProjectCwd()) => {
3030
+ if (!name) {
3031
+ return void 0;
3032
+ }
3033
+ const { state } = getStrategyRegistryState(cwd);
3034
+ return state.strategyManifestsMap.get(name);
3035
+ };
3036
+ var strategies = new Proxy(
3037
+ {},
3038
+ {
3039
+ get: (_target, property) => {
3040
+ if (typeof property !== "string") {
3041
+ return void 0;
3042
+ }
3043
+ return getStrategyRegistryState().state.strategyCreators.get(property);
3044
+ },
3045
+ ownKeys: () => {
3046
+ return [...getStrategyRegistryState().state.strategyCreators.keys()];
3047
+ },
3048
+ getOwnPropertyDescriptor: () => ({
3049
+ enumerable: true,
3050
+ configurable: true
3051
+ })
3052
+ }
3053
+ );
3054
+
3055
+ // src/strategyHelpers/tickerProfiles.ts
3056
+ var snapshots = /* @__PURE__ */ new Map();
3057
+ var resolveTickerProfileSource = (strategy) => {
3058
+ const declared = getStrategyManifest(strategy)?.contextRequirements?.tickerProfiles;
3059
+ if (declared) return declared;
3060
+ const provider = process.env.TICKER_PROFILES_PROVIDER?.trim();
3061
+ const universeId = process.env.TICKER_PROFILES_UNIVERSE?.trim();
3062
+ if (!provider && !universeId) return void 0;
3063
+ if (!provider || !universeId)
3064
+ throw new Error(
3065
+ "Set both TICKER_PROFILES_PROVIDER and TICKER_PROFILES_UNIVERSE"
3066
+ );
3067
+ return { provider, universeId };
3068
+ };
3069
+ var loadTickerProfileContext = async ({
3070
+ projectRoot,
3071
+ provider,
3072
+ universeId,
3073
+ symbol,
3074
+ timestamp
3075
+ }) => {
3076
+ const asOf = (0, import_tickerProfiles2.tickerProfileMonth)(timestamp), key = JSON.stringify([projectRoot, provider, universeId, asOf]);
3077
+ let cached = snapshots.get(key);
3078
+ if (!cached || cached.expires < Date.now()) {
3079
+ const value = (0, import_tickerProfiles.readTickerProfiles)(projectRoot, provider, universeId, asOf).then((envelope) => {
3080
+ if (!envelope) return null;
3081
+ const s = envelope.snapshot;
3082
+ if (!(0, import_tickerProfiles2.isTickerProfileSnapshot)(s) || s.asOf !== asOf || s.provider !== provider || s.universeId !== universeId)
3083
+ throw new Error("Ticker profile identity/schema mismatch");
3084
+ const entry = snapshots.get(key);
3085
+ if (entry) entry.expires = Infinity;
3086
+ return new Map(
3087
+ s.profiles.map((p) => [
3088
+ p.symbol,
3089
+ Object.freeze({
3090
+ ...p,
3091
+ definition: s.definition,
3092
+ provider,
3093
+ universeId,
3094
+ snapshotId: envelope.snapshotId
3095
+ })
3096
+ ])
3097
+ );
3098
+ }).catch((error) => {
3099
+ snapshots.delete(key);
3100
+ throw error;
3101
+ });
3102
+ cached = { expires: Date.now() + 6e4, value };
3103
+ snapshots.set(key, cached);
3104
+ if (snapshots.size > 24) {
3105
+ const oldest = snapshots.keys().next().value;
3106
+ if (oldest) snapshots.delete(oldest);
3107
+ }
3108
+ }
3109
+ return (await cached.value)?.get(symbol);
3110
+ };
3111
+ var withTickerProfile = (baseContext, profile) => Object.defineProperty(
3112
+ Object.create(
3113
+ Object.getPrototypeOf(baseContext),
3114
+ Object.getOwnPropertyDescriptors(baseContext)
3115
+ ),
3116
+ "tickerProfile",
3117
+ { enumerable: true, configurable: true, value: profile }
3118
+ );
3119
+ var enrichSignalWithTickerProfile = async (signal) => {
3120
+ const source = resolveTickerProfileSource(signal.strategy);
3121
+ if (!source || signal.universe === "tradfi" || !signal.symbol.endsWith("USDT"))
3122
+ return;
3123
+ const profile = await loadTickerProfileContext({
3124
+ ...source,
3125
+ projectRoot: getTradejsProjectCwd(),
3126
+ symbol: signal.symbol,
3127
+ timestamp: signal.timestamp
3128
+ });
3129
+ if (profile) {
3130
+ signal.additionalIndicators = {
3131
+ ...signal.additionalIndicators,
3132
+ tickerProfile: profile
3133
+ };
3134
+ }
3135
+ };
3136
+
3137
+ // src/strategyRuntime.ts
3138
+ var import_jev6 = require("@tradejs/core/jev");
3139
+
3140
+ // src/jev.ts
3141
+ var import_promises = __toESM(require("fs/promises"));
3142
+ var import_node_path = __toESM(require("path"));
3143
+ var import_jev4 = require("@tradejs/core/jev");
3144
+ var import_jev5 = require("@tradejs/infra/jev");
3145
+ var import_userSettings = require("@tradejs/infra/userSettings");
3146
+ init_jevInput();
3147
+ init_jevInput();
3148
+ init_jevTraining();
3149
+ var resolveJevProvider = async (userName) => {
3150
+ const settings = await (0, import_userSettings.getUserSettings)(userName);
3151
+ const provider = {
3152
+ endpoint: settings.JEV_API_ENDPOINT || import_jev4.JEV_PROVIDERS[0].endpoint,
3153
+ model: settings.JEV_MODEL || import_jev4.JEV_PROVIDERS[0].model
3154
+ };
3155
+ (0, import_jev4.parseJevConfig)({ source: "provider", mode: "observe", provider });
3156
+ if (!settings.JEV_API_KEY)
3157
+ throw new Error("Configure the Jev API key in Account settings");
3158
+ return provider;
3159
+ };
3160
+ var emptyScores = () => Object.fromEntries(import_jev4.JEV_DIMENSIONS.map((key) => [key, null]));
3161
+ var pending = /* @__PURE__ */ new Map();
3162
+ var pendingResponses = /* @__PURE__ */ new Map();
3163
+ var evaluateJevInput = async ({
3164
+ input,
3165
+ config,
3166
+ userName,
3167
+ projectRoot
3168
+ }) => {
3169
+ config = (0, import_jev4.parseJevConfig)(config);
3170
+ const inputHash = (0, import_jev5.jevHash)(input);
3171
+ const questions = questionsForJevInput(input);
3172
+ const questionsHash = (0, import_jev5.jevHash)(questions);
3173
+ const base = {
3174
+ questionSet: import_jev4.JEV_QUESTION_SET,
3175
+ schema: "tradejs-signal-assessment/v5",
3176
+ source: config.source,
3177
+ mode: config.mode,
3178
+ inputHash,
3179
+ questionsHash,
3180
+ geometryStatus: input.geometryStatus,
3181
+ levelsValid: typeof input.features["signal.validLevels"] === "boolean" ? input.features["signal.validLevels"] : null
3182
+ };
3183
+ if (!input.questions.length)
3184
+ return {
3185
+ assessment: {
3186
+ ...base,
3187
+ status: "unavailable",
3188
+ model: "",
3189
+ scores: emptyScores(),
3190
+ confidence: emptyScores()
3191
+ }
3192
+ };
3193
+ if (config.source === "local") {
3194
+ const contents = await import_promises.default.readFile(
3195
+ import_node_path.default.resolve(projectRoot, config.modelFile),
3196
+ "utf8"
3197
+ );
3198
+ if ((0, import_jev5.jevFileHash)(contents) !== config.modelSha256)
3199
+ throw new Error("Local Jev gate checksum mismatch");
3200
+ const { validateJevGateModel: validateJevGateModel2 } = await Promise.resolve().then(() => (init_jevTraining(), jevTraining_exports));
3201
+ const model = validateJevGateModel2(JSON.parse(contents));
3202
+ if (model.strategy !== input.strategy || model.questionsHash !== (0, import_jev5.jevHash)(JEV_QUESTIONS))
3203
+ throw new Error("Local Jev gate strategy or question version mismatch");
3204
+ const scores = Object.fromEntries(
3205
+ import_jev4.JEV_DIMENSIONS.map((key) => [
3206
+ key,
3207
+ input.questions.includes(key) && model.trees[key] ? (0, import_jev4.predictJevTree)(model.trees[key], input.features) : null
3208
+ ])
3209
+ );
3210
+ return {
3211
+ assessment: {
3212
+ ...base,
3213
+ status: "available",
3214
+ model: config.modelSha256,
3215
+ scores,
3216
+ confidence: emptyScores()
3217
+ }
3218
+ };
3219
+ }
3220
+ const provider = config.provider ?? await resolveJevProvider(userName);
3221
+ const id = (0, import_jev5.jevHash)({ inputHash, questionsHash, provider });
3222
+ const dir = import_node_path.default.resolve(projectRoot, config.recordsDir ?? "data/ai/jev");
3223
+ const file = import_node_path.default.join(dir, "records", `${id}.json`);
3224
+ const getRecord = async () => {
3225
+ const existing = await (0, import_jev5.readJevArtifact)(file);
3226
+ if (existing) {
3227
+ if (existing.schema !== "tradejs-jev-record/v4" || existing.id !== id || existing.inputHash !== inputHash || existing.questionsHash !== questionsHash || (0, import_jev5.jevHash)(existing.input) !== inputHash || (0, import_jev5.jevHash)(existing.provider) !== (0, import_jev5.jevHash)(provider))
3228
+ throw new Error("Jev recording identity mismatch");
3229
+ (0, import_jev5.validateJevResponse)(existing.response, input.questions);
3230
+ for (const key of import_jev4.JEV_DIMENSIONS) {
3231
+ const expected = input.questions.includes(key) ? existing.response.answers[key].score / (0, import_jev5.jevScoreMaximum)(key) : null;
3232
+ if (existing.scores[key] !== expected)
3233
+ throw new Error("Jev recording scores do not match the response");
3234
+ }
3235
+ return existing;
3236
+ }
3237
+ if (config.source === "recorded")
3238
+ throw new Error(
3239
+ `Missing Jev recording ${id} for ${input.strategy} ${input.symbol} at ${new Date(input.timestamp).toISOString()}. Recorded mode requires the same signal-time facts and provider; a shorter backtest can change warm-up indicators or prior trade state.`
3240
+ );
3241
+ const settings = await (0, import_userSettings.getUserSettings)(userName);
3242
+ if (!settings.JEV_API_KEY)
3243
+ throw new Error("Configure the Jev API key in Account settings");
3244
+ const apiKey = settings.JEV_API_KEY;
3245
+ const configuredProvider = await resolveJevProvider(userName);
3246
+ if (configuredProvider.endpoint !== provider.endpoint)
3247
+ throw new Error(
3248
+ "Jev account endpoint changed; update the frozen provider configuration"
3249
+ );
3250
+ const started = Date.now();
3251
+ const state = stateForJevInput(input);
3252
+ const responseId = (0, import_jev5.jevHash)({
3253
+ state,
3254
+ questionsHash,
3255
+ provider,
3256
+ user: (0, import_jev5.jevHash)(userName)
3257
+ });
3258
+ const responseFile = import_node_path.default.join(dir, "responses", `${responseId}.json`);
3259
+ let responsePromise = pendingResponses.get(responseFile);
3260
+ if (!responsePromise) {
3261
+ responsePromise = (async () => {
3262
+ const saved = await (0, import_jev5.readJevArtifact)(responseFile);
3263
+ if (saved) return (0, import_jev5.validateJevResponse)(saved, input.questions);
3264
+ const fresh = await (0, import_jev5.requestJev)(provider, apiKey, state, questions);
3265
+ await (0, import_jev5.writeJevArtifact)(responseFile, fresh);
3266
+ return fresh;
3267
+ })();
3268
+ pendingResponses.set(responseFile, responsePromise);
3269
+ }
3270
+ let response;
3271
+ try {
3272
+ response = await responsePromise;
3273
+ } finally {
3274
+ pendingResponses.delete(responseFile);
3275
+ }
3276
+ const scores = Object.fromEntries(
3277
+ import_jev4.JEV_DIMENSIONS.map((key) => [
3278
+ key,
3279
+ input.questions.includes(key) ? response.answers[key].score / (0, import_jev5.jevScoreMaximum)(key) : null
3280
+ ])
3281
+ );
3282
+ const value = {
3283
+ schema: "tradejs-jev-record/v4",
3284
+ id,
3285
+ inputHash,
3286
+ questionsHash,
3287
+ provider,
3288
+ input,
3289
+ response,
3290
+ scores,
3291
+ createdAt: (/* @__PURE__ */ new Date()).toISOString(),
3292
+ elapsedMs: Date.now() - started
3293
+ };
3294
+ await (0, import_jev5.writeJevArtifact)(file, value);
3295
+ return await (0, import_jev5.readJevArtifact)(file);
3296
+ };
3297
+ const pendingKey = `${file}:${(0, import_jev5.jevHash)(userName)}`;
3298
+ let promise = pending.get(pendingKey);
3299
+ if (!promise) {
3300
+ promise = getRecord();
3301
+ pending.set(pendingKey, promise);
3302
+ }
3303
+ let record;
3304
+ try {
3305
+ record = await promise;
3306
+ } finally {
3307
+ pending.delete(pendingKey);
3308
+ }
3309
+ return {
3310
+ record,
3311
+ assessment: {
3312
+ ...base,
3313
+ status: "available",
3314
+ recordId: id,
3315
+ model: record.response.model,
3316
+ scores: record.scores,
3317
+ confidence: Object.fromEntries(
3318
+ import_jev4.JEV_DIMENSIONS.map((key) => [
3319
+ key,
3320
+ record.response.answers[key]?.confidence ?? null
3321
+ ])
3322
+ )
3323
+ }
3324
+ };
3325
+ };
3326
+ var assessSignalWithJev = async ({
3327
+ signal,
3328
+ config,
3329
+ userName,
3330
+ projectRoot,
3331
+ strict,
3332
+ scope = "runtime"
3333
+ }) => {
3334
+ let assessment;
3335
+ try {
3336
+ const input = buildJevInput(signal);
3337
+ ({ assessment } = await evaluateJevInput({
3338
+ input,
3339
+ config,
3340
+ userName,
3341
+ projectRoot
3342
+ }));
3343
+ } catch (error) {
3344
+ if (strict) throw error;
3345
+ assessment = {
3346
+ questionSet: import_jev4.JEV_QUESTION_SET,
3347
+ schema: "tradejs-signal-assessment/v5",
3348
+ source: config.source,
3349
+ mode: config.mode,
3350
+ status: "unavailable",
3351
+ inputHash: "",
3352
+ questionsHash: "",
3353
+ geometryStatus: "absent",
3354
+ levelsValid: null,
3355
+ model: config.provider?.model ?? config.modelSha256 ?? "",
3356
+ scores: emptyScores(),
3357
+ confidence: emptyScores()
3358
+ };
3359
+ }
3360
+ signal.assessment = assessment;
3361
+ signal.additionalIndicators = {
3362
+ ...signal.additionalIndicators,
3363
+ jev: (0, import_jev4.jevAssessmentFeatures)(assessment)
3364
+ };
3365
+ const identity = {
3366
+ signalId: signal.signalId,
3367
+ strategy: signal.strategy,
3368
+ symbol: signal.symbol,
3369
+ timestamp: signal.timestamp,
3370
+ scope,
3371
+ user: (0, import_jev5.jevHash)(userName),
3372
+ config,
3373
+ assessment
3374
+ };
3375
+ await (0, import_jev5.writeJevArtifact)(
3376
+ import_node_path.default.resolve(
3377
+ projectRoot,
3378
+ config.recordsDir ?? "data/ai/jev",
3379
+ "decisions",
3380
+ `${(0, import_jev5.jevHash)(identity)}.json`
2899
3381
  ),
2900
- altVolumeChange24hPct: toFiniteNumber(cmcGlobal.altVolumeChange24hPct),
2901
- activeCryptocurrencies: toFiniteNumber(cmcGlobal.activeCryptocurrencies),
2902
- activeExchanges: toFiniteNumber(cmcGlobal.activeExchanges),
2903
- activeMarketPairs: toFiniteNumber(cmcGlobal.activeMarketPairs),
2904
- altLiquidityRegime: typeof cmcGlobal.altLiquidityRegime === "string" ? cmcGlobal.altLiquidityRegime : null
3382
+ identity
3383
+ );
3384
+ return assessment;
3385
+ };
3386
+
3387
+ // src/strategyRuntime.ts
3388
+ var import_constants7 = require("@tradejs/core/constants");
3389
+ var import_strategies6 = require("@tradejs/core/strategies");
3390
+ var import_logger14 = require("@tradejs/infra/logger");
3391
+
3392
+ // src/strategyHelpers/runtime.ts
3393
+ var import_logger12 = require("@tradejs/infra/logger");
3394
+ var import_constants5 = require("@tradejs/core/constants");
3395
+ var import_ml2 = require("@tradejs/infra/ml");
3396
+
3397
+ // src/strategy/entrySubmissionValidation.ts
3398
+ var import_logger6 = require("@tradejs/infra/logger");
3399
+
3400
+ // src/strategy/entryExecutionValidation.ts
3401
+ var EntrySubmissionValidationError = class extends Error {
3402
+ constructor() {
3403
+ super(...arguments);
3404
+ this.name = "EntrySubmissionValidationError";
3405
+ }
3406
+ };
3407
+ var resolveEntryExecutionLimits = (config, direction) => {
3408
+ const side = config[direction];
3409
+ const minimum = side && typeof side === "object" && "minRiskRatio" in side ? side.minRiskRatio : void 0;
3410
+ const maximum = config.MAX_LOSS_VALUE;
3411
+ if (minimum == null && maximum == null) return void 0;
3412
+ const minimumNetRiskRatio = minimum == null ? void 0 : Number(minimum);
3413
+ const maxLossValue = maximum == null ? void 0 : Number(maximum);
3414
+ const feeRate = Number(config.RISK_FEE_RATE ?? 0);
3415
+ const slippage = Number(config.RISK_SLIPPAGE_BPS ?? 0);
3416
+ const impact = Number(config.RISK_MARKET_IMPACT_BPS ?? 0);
3417
+ const slippageBps = slippage + impact;
3418
+ if (minimumNetRiskRatio != null && (!Number.isFinite(minimumNetRiskRatio) || minimumNetRiskRatio < 0) || maxLossValue != null && (!Number.isFinite(maxLossValue) || maxLossValue <= 0) || !Number.isFinite(feeRate) || feeRate < 0 || feeRate >= 1 || !Number.isFinite(slippage) || slippage < 0 || !Number.isFinite(impact) || impact < 0 || !Number.isFinite(slippageBps) || slippageBps < 0 || slippageBps >= 1e4)
3419
+ throw new Error("INVALID_ENTRY_EXECUTION_LIMITS");
3420
+ return { minimumNetRiskRatio, maxLossValue, feeRate, slippageBps };
3421
+ };
3422
+ var validateEntryExecution = ({
3423
+ direction,
3424
+ price,
3425
+ qty,
3426
+ stopLossPrice,
3427
+ takeProfitPrice,
3428
+ limits
3429
+ }) => {
3430
+ if (!Number.isFinite(price) || price <= 0 || !Number.isFinite(qty) || qty <= 0 || stopLossPrice == null || !Number.isFinite(stopLossPrice) || stopLossPrice <= 0 || !Number.isFinite(takeProfitPrice) || takeProfitPrice <= 0)
3431
+ throw new EntrySubmissionValidationError(
3432
+ "ENTRY_ECONOMICS_INPUTS_UNAVAILABLE"
3433
+ );
3434
+ const side = direction === "LONG" ? 1 : -1;
3435
+ if (side * (price - stopLossPrice) <= 0 || side * (takeProfitPrice - price) <= 0)
3436
+ throw new EntrySubmissionValidationError(
3437
+ "INVALID_ENTRY_PROTECTION_AT_SUBMISSION"
3438
+ );
3439
+ const slippage = limits.slippageBps / 1e4;
3440
+ const cost = (value, sign) => value * slippage + value * (1 + sign * slippage) * limits.feeRate;
3441
+ const entryCost = cost(price, side);
3442
+ const lossPerUnit = Math.abs(price - stopLossPrice) + entryCost + cost(stopLossPrice, -side);
3443
+ const rewardPerUnit = Math.max(
3444
+ 0,
3445
+ Math.abs(takeProfitPrice - price) - entryCost - cost(takeProfitPrice, -side)
3446
+ );
3447
+ const netRiskRatio = rewardPerUnit / lossPerUnit;
3448
+ const plannedLossValue = qty * lossPerUnit;
3449
+ if (!Number.isFinite(netRiskRatio) || !Number.isFinite(plannedLossValue))
3450
+ throw new EntrySubmissionValidationError(
3451
+ "ENTRY_ECONOMICS_INPUTS_UNAVAILABLE"
3452
+ );
3453
+ const belowMinimumNetRiskRatio = limits.minimumNetRiskRatio != null && netRiskRatio <= limits.minimumNetRiskRatio;
3454
+ const riskExceeded = limits.maxLossValue != null && plannedLossValue > limits.maxLossValue * (1 + 1e-9);
3455
+ return {
3456
+ netRiskRatio,
3457
+ plannedLossValue,
3458
+ riskExceeded,
3459
+ belowMinimumNetRiskRatio
2905
3460
  };
2906
3461
  };
2907
- var buildCmcReferenceAssetsContextFromSignal = (signal) => {
2908
- const baseContext = toRecord(signal.additionalIndicators?.baseContext);
2909
- const relative = toRecord(baseContext?.relative);
2910
- const cmcReferenceAssets = toRecord(relative?.cmcReferenceAssets);
2911
- if (!cmcReferenceAssets) {
3462
+
3463
+ // src/strategy/entrySubmissionValidation.ts
3464
+ var MAX_ENTRY_QUOTE_AGE_MS = 5e3;
3465
+ var toFiniteNumberOrNull = (value) => typeof value === "number" && Number.isFinite(value) ? value : null;
3466
+ var getOrderArrivalSnapshot = async ({
3467
+ connector,
3468
+ symbol,
3469
+ requireTimestamp = false
3470
+ }) => {
3471
+ if (typeof connector.getTopOfBookTicker !== "function") {
3472
+ return {
3473
+ arrivalSnapshotTime: Date.now(),
3474
+ arrivalSource: "unavailable",
3475
+ bid: null,
3476
+ ask: null,
3477
+ arrivalMid: null,
3478
+ spreadBps: null
3479
+ };
3480
+ }
3481
+ try {
3482
+ const ticker = await connector.getTopOfBookTicker(symbol);
3483
+ const arrivalSnapshotTime = toFiniteNumberOrNull(ticker?.timestamp);
3484
+ const timestampUnavailable = requireTimestamp && arrivalSnapshotTime == null;
3485
+ const bid = timestampUnavailable ? null : toFiniteNumberOrNull(ticker?.bidPrice);
3486
+ const ask = timestampUnavailable ? null : toFiniteNumberOrNull(ticker?.askPrice);
3487
+ const arrivalMid = bid != null && ask != null ? (bid + ask) / 2 : null;
3488
+ const spreadBps = bid != null && ask != null && arrivalMid != null && arrivalMid > 0 ? (ask - bid) / arrivalMid * 1e4 : null;
3489
+ return {
3490
+ arrivalSnapshotTime: arrivalSnapshotTime ?? Date.now(),
3491
+ arrivalSource: "top_of_book",
3492
+ bid,
3493
+ ask,
3494
+ arrivalMid,
3495
+ spreadBps
3496
+ };
3497
+ } catch (error) {
3498
+ import_logger6.logger.warn(
3499
+ "runtime order arrival snapshot failed: %s %s",
3500
+ symbol,
3501
+ error?.message || String(error)
3502
+ );
2912
3503
  return {
2913
- source: null,
2914
- available: false,
2915
- interval: null,
2916
- asOfTs: null,
2917
- stale: null,
2918
- btcMarketCapUsd: null,
2919
- ethMarketCapUsd: null,
2920
- btcVolumeUsd: null,
2921
- ethVolumeUsd: null,
2922
- btcVolumeToMarketCap: null,
2923
- ethVolumeToMarketCap: null,
2924
- ethBtcMarketCapRatio: null,
2925
- ethBtcMarketCapRatioChange24hPct: null,
2926
- ethVsBtcVolumeRatio: null,
2927
- referenceLiquidityRegime: null
3504
+ arrivalSnapshotTime: Date.now(),
3505
+ arrivalSource: "top_of_book_error",
3506
+ bid: null,
3507
+ ask: null,
3508
+ arrivalMid: null,
3509
+ spreadBps: null
2928
3510
  };
2929
3511
  }
2930
- return {
2931
- source: String(cmcReferenceAssets.source ?? ""),
2932
- available: true,
2933
- interval: typeof cmcReferenceAssets.interval === "string" ? cmcReferenceAssets.interval : null,
2934
- asOfTs: toFiniteNumber(cmcReferenceAssets.asOfTs),
2935
- stale: typeof cmcReferenceAssets.stale === "boolean" ? cmcReferenceAssets.stale : null,
2936
- btcMarketCapUsd: toFiniteNumber(cmcReferenceAssets.btcMarketCapUsd),
2937
- ethMarketCapUsd: toFiniteNumber(cmcReferenceAssets.ethMarketCapUsd),
2938
- btcVolumeUsd: toFiniteNumber(cmcReferenceAssets.btcVolumeUsd),
2939
- ethVolumeUsd: toFiniteNumber(cmcReferenceAssets.ethVolumeUsd),
2940
- btcVolumeToMarketCap: toFiniteNumber(
2941
- cmcReferenceAssets.btcVolumeToMarketCap
2942
- ),
2943
- ethVolumeToMarketCap: toFiniteNumber(
2944
- cmcReferenceAssets.ethVolumeToMarketCap
2945
- ),
2946
- ethBtcMarketCapRatio: toFiniteNumber(
2947
- cmcReferenceAssets.ethBtcMarketCapRatio
2948
- ),
2949
- ethBtcMarketCapRatioChange24hPct: toFiniteNumber(
2950
- cmcReferenceAssets.ethBtcMarketCapRatioChange24hPct
2951
- ),
2952
- ethVsBtcVolumeRatio: toFiniteNumber(cmcReferenceAssets.ethVsBtcVolumeRatio),
2953
- referenceLiquidityRegime: typeof cmcReferenceAssets.referenceLiquidityRegime === "string" ? cmcReferenceAssets.referenceLiquidityRegime : null
3512
+ };
3513
+ var validateEntryProtectionAtArrival = ({
3514
+ direction,
3515
+ signalPrice,
3516
+ bid,
3517
+ ask,
3518
+ arrivalMid,
3519
+ takeProfits,
3520
+ stopLossPrice
3521
+ }) => {
3522
+ const entryReference = direction === "LONG" ? ask ?? arrivalMid ?? signalPrice : bid ?? arrivalMid ?? signalPrice;
3523
+ const stopReference = direction === "LONG" ? bid ?? arrivalMid ?? signalPrice : ask ?? arrivalMid ?? signalPrice;
3524
+ if (!Number.isFinite(entryReference) || entryReference <= 0 || !Number.isFinite(stopReference) || stopReference <= 0) {
3525
+ throw new Error("INVALID_ENTRY_REFERENCE_AT_ARRIVAL");
3526
+ }
3527
+ for (const takeProfit of takeProfits) {
3528
+ if (!Number.isFinite(takeProfit.price) || takeProfit.price <= 0 || (direction === "LONG" ? takeProfit.price <= entryReference : takeProfit.price >= entryReference)) {
3529
+ throw new Error("TAKE_PROFIT_CROSSED_BEFORE_ENTRY");
3530
+ }
3531
+ }
3532
+ if (stopLossPrice != null && (!Number.isFinite(stopLossPrice) || stopLossPrice <= 0 || (direction === "LONG" ? stopLossPrice >= stopReference : stopLossPrice <= stopReference))) {
3533
+ throw new Error("STOP_LOSS_CROSSED_BEFORE_ENTRY");
3534
+ }
3535
+ };
3536
+ var createEntrySubmissionValidation = ({
3537
+ connector,
3538
+ symbol,
3539
+ direction,
3540
+ takeProfits,
3541
+ stopLossPrice,
3542
+ limits,
3543
+ onValidated
3544
+ }) => {
3545
+ if (!limits) return void 0;
3546
+ if (!connector.capabilities?.orderSubmissionValidation)
3547
+ throw new EntrySubmissionValidationError(
3548
+ "ENTRY_SUBMISSION_VALIDATION_UNSUPPORTED"
3549
+ );
3550
+ return async ({ qty, executionPrice }) => {
3551
+ const simulated = Reflect.get(connector, "__tradejsTestConnector") === true;
3552
+ const snapshot2 = simulated && executionPrice != null ? {
3553
+ arrivalSnapshotTime: Date.now(),
3554
+ arrivalSource: "simulation",
3555
+ bid: executionPrice,
3556
+ ask: executionPrice,
3557
+ arrivalMid: executionPrice,
3558
+ spreadBps: 0
3559
+ } : await getOrderArrivalSnapshot({
3560
+ connector,
3561
+ symbol,
3562
+ requireTimestamp: true
3563
+ });
3564
+ const age = Date.now() - snapshot2.arrivalSnapshotTime;
3565
+ if (snapshot2.bid == null || snapshot2.ask == null || !Number.isFinite(snapshot2.bid) || !Number.isFinite(snapshot2.ask) || snapshot2.bid <= 0 || snapshot2.ask < snapshot2.bid)
3566
+ throw new EntrySubmissionValidationError(
3567
+ "ENTRY_QUOTE_UNAVAILABLE_AT_SUBMISSION"
3568
+ );
3569
+ if (!Number.isFinite(age) || age < -MAX_ENTRY_QUOTE_AGE_MS || age > MAX_ENTRY_QUOTE_AGE_MS)
3570
+ throw new EntrySubmissionValidationError(
3571
+ "ENTRY_QUOTE_STALE_AT_SUBMISSION"
3572
+ );
3573
+ const price = direction === "LONG" ? snapshot2.ask : snapshot2.bid;
3574
+ validateEntryProtectionAtArrival({
3575
+ direction,
3576
+ signalPrice: price,
3577
+ bid: snapshot2.bid,
3578
+ ask: snapshot2.ask,
3579
+ arrivalMid: snapshot2.arrivalMid,
3580
+ takeProfits,
3581
+ stopLossPrice
3582
+ });
3583
+ const prices = takeProfits.map((tp) => tp.price);
3584
+ const economics2 = validateEntryExecution({
3585
+ direction,
3586
+ price,
3587
+ qty,
3588
+ stopLossPrice,
3589
+ takeProfitPrice: direction === "LONG" ? Math.max(...prices) : Math.min(...prices),
3590
+ limits
3591
+ });
3592
+ if (economics2.belowMinimumNetRiskRatio) {
3593
+ import_logger6.logger.warn(
3594
+ "entry submission warning: %s %s ENTRY_RR_BELOW_MIN_AT_SUBMISSION (RR %s, threshold %s)",
3595
+ symbol,
3596
+ direction,
3597
+ economics2.netRiskRatio,
3598
+ limits.minimumNetRiskRatio
3599
+ );
3600
+ }
3601
+ onValidated?.(snapshot2, economics2);
2954
3602
  };
2955
3603
  };
2956
- var buildCmcExchangeLiquidityContextFromSignal = (signal) => {
3604
+
3605
+ // src/ai.ts
3606
+ var import_aiLanguages2 = require("@tradejs/core/aiLanguages");
3607
+ var import_redis4 = require("@tradejs/infra/redis");
3608
+
3609
+ // src/strategyAdapters/ai.ts
3610
+ var import_indicators2 = require("@tradejs/core/indicators");
3611
+ var import_aiFeatures = require("@tradejs/core/aiFeatures");
3612
+
3613
+ // src/aiShared.ts
3614
+ var MAX_AI_SERIES_POINTS = 5;
3615
+ var COMPACT_INDICATORS_SNAPSHOT_SYMBOL = /* @__PURE__ */ Symbol.for(
3616
+ "tradejs.indicators.compactSnapshot"
3617
+ );
3618
+ var COMPACT_INDICATORS_SNAPSHOT_KEY = "__tradejsCompactIndicatorsSnapshot";
3619
+ var trimSeriesDeep = (value) => {
3620
+ if (Array.isArray(value)) {
3621
+ const trimmed = value.slice(-MAX_AI_SERIES_POINTS);
3622
+ const isMatrix = trimmed.every((item) => Array.isArray(item));
3623
+ if (isMatrix) {
3624
+ return trimmed;
3625
+ }
3626
+ return trimmed.map(
3627
+ (item) => item && typeof item === "object" ? trimSeriesDeep(item) : item
3628
+ );
3629
+ }
3630
+ if (value && typeof value === "object") {
3631
+ return Object.fromEntries(
3632
+ Object.entries(value).map(([key, nested]) => [
3633
+ key,
3634
+ trimSeriesDeep(nested)
3635
+ ])
3636
+ );
3637
+ }
3638
+ return value;
3639
+ };
3640
+ var buildCompactAiIndicatorsSnapshot = (value) => {
3641
+ const compactSnapshot = value && typeof value === "object" ? value[COMPACT_INDICATORS_SNAPSHOT_SYMBOL] ?? value[COMPACT_INDICATORS_SNAPSHOT_KEY] : void 0;
3642
+ if (typeof compactSnapshot === "function") {
3643
+ return compactSnapshot({ limit: MAX_AI_SERIES_POINTS });
3644
+ }
3645
+ return trimSeriesDeep(value);
3646
+ };
3647
+
3648
+ // src/aiMarketContext.ts
3649
+ var toRecord = (value) => {
3650
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
3651
+ return null;
3652
+ }
3653
+ return value;
3654
+ };
3655
+ var toFiniteNumber = (value) => {
3656
+ const numeric = typeof value === "number" ? value : typeof value === "string" && value.trim() ? Number(value) : Number.NaN;
3657
+ return Number.isFinite(numeric) ? numeric : null;
3658
+ };
3659
+ var roundTo = (value, decimals) => {
3660
+ const factor = 10 ** decimals;
3661
+ return Math.round(value * factor) / factor;
3662
+ };
3663
+ var buildMissingSpreadContext = () => ({
3664
+ source: "payload.additionalIndicators.baseContext.relative.execution.venueSpread",
3665
+ indicatorKey: "payload.additionalIndicators.baseContext.relative.execution.venueSpread",
3666
+ available: false,
3667
+ value: null,
3668
+ zScore: null,
3669
+ bps: null,
3670
+ absBps: null,
3671
+ bias: null,
3672
+ severity: null
3673
+ });
3674
+ var buildSpreadContextFromSignal = (signal) => {
2957
3675
  const baseContext = toRecord(signal.additionalIndicators?.baseContext);
2958
3676
  const relative = toRecord(baseContext?.relative);
2959
- const cmcExchangeLiquidity = toRecord(relative?.cmcExchangeLiquidity);
2960
- if (!cmcExchangeLiquidity) {
2961
- return {
2962
- source: null,
2963
- available: false,
2964
- interval: null,
2965
- asOfTs: null,
2966
- stale: null,
2967
- exchangesCount: null,
2968
- totalVolumeUsd: null,
2969
- totalVolumeChange24hPct: null,
2970
- binanceVolumeUsd: null,
2971
- binanceVolumeShare: null,
2972
- topExchangeVolumeShare: null,
2973
- liquidityRegime: null
2974
- };
3677
+ const execution = toRecord(relative?.execution);
3678
+ const spread = toFiniteNumber(execution?.venueSpread);
3679
+ const zScore = toFiniteNumber(execution?.venueSpreadZScore);
3680
+ if (spread == null) {
3681
+ return buildMissingSpreadContext();
2975
3682
  }
3683
+ const value = roundTo(spread, 8);
3684
+ const bps = roundTo(value * 1e4, 2);
3685
+ const absBps = Math.abs(bps);
3686
+ const bias = absBps < 1 ? "flat" : bps > 0 ? "coinbase_premium" : "binance_premium";
3687
+ const severity = absBps >= 20 ? "wide" : absBps >= 5 ? "elevated" : "normal";
2976
3688
  return {
2977
- source: String(cmcExchangeLiquidity.source ?? ""),
3689
+ source: "payload.additionalIndicators.baseContext.relative.execution.venueSpread",
3690
+ indicatorKey: "payload.additionalIndicators.baseContext.relative.execution.venueSpread",
2978
3691
  available: true,
2979
- interval: typeof cmcExchangeLiquidity.interval === "string" ? cmcExchangeLiquidity.interval : null,
2980
- asOfTs: toFiniteNumber(cmcExchangeLiquidity.asOfTs),
2981
- stale: typeof cmcExchangeLiquidity.stale === "boolean" ? cmcExchangeLiquidity.stale : null,
2982
- exchangesCount: toFiniteNumber(cmcExchangeLiquidity.exchangesCount),
2983
- totalVolumeUsd: toFiniteNumber(cmcExchangeLiquidity.totalVolumeUsd),
2984
- totalVolumeChange24hPct: toFiniteNumber(
2985
- cmcExchangeLiquidity.totalVolumeChange24hPct
2986
- ),
2987
- binanceVolumeUsd: toFiniteNumber(cmcExchangeLiquidity.binanceVolumeUsd),
2988
- binanceVolumeShare: toFiniteNumber(cmcExchangeLiquidity.binanceVolumeShare),
2989
- topExchangeVolumeShare: toFiniteNumber(
2990
- cmcExchangeLiquidity.topExchangeVolumeShare
2991
- ),
2992
- liquidityRegime: typeof cmcExchangeLiquidity.liquidityRegime === "string" ? cmcExchangeLiquidity.liquidityRegime : null
3692
+ value,
3693
+ zScore,
3694
+ bps,
3695
+ absBps,
3696
+ bias,
3697
+ severity
2993
3698
  };
2994
3699
  };
2995
- var buildCmcFearGreedContextFromSignal = (signal) => {
3700
+ var buildTrueDeltaContextFromSignal = (signal) => {
2996
3701
  const baseContext = toRecord(signal.additionalIndicators?.baseContext);
2997
- const relative = toRecord(baseContext?.relative);
2998
- const cmcFearGreed = toRecord(relative?.cmcFearGreed);
2999
- if (!cmcFearGreed) {
3702
+ const participation = toRecord(baseContext?.participation);
3703
+ const delta = toRecord(participation?.delta);
3704
+ const source = String(delta?.source ?? "");
3705
+ const isTrueDeltaSource = source === "kline_taker_volume" || source === "agg_trades" || source === "trades";
3706
+ if (!delta || !isTrueDeltaSource) {
3000
3707
  return {
3001
- source: null,
3708
+ source: source || null,
3002
3709
  available: false,
3003
- interval: null,
3004
- asOfTs: null,
3005
- stale: null,
3006
- value: null,
3007
- valueChange24h: null,
3008
- valueChange7d: null,
3009
- classification: null,
3010
- sentimentRegime: null
3710
+ buyPressurePct: null,
3711
+ buyVolume: null,
3712
+ sellVolume: null,
3713
+ netDelta: null,
3714
+ deltaPct: null,
3715
+ signedVolumeZScore: null
3011
3716
  };
3012
3717
  }
3013
3718
  return {
3014
- source: String(cmcFearGreed.source ?? ""),
3719
+ source,
3015
3720
  available: true,
3016
- interval: typeof cmcFearGreed.interval === "string" ? cmcFearGreed.interval : null,
3017
- asOfTs: toFiniteNumber(cmcFearGreed.asOfTs),
3018
- stale: typeof cmcFearGreed.stale === "boolean" ? cmcFearGreed.stale : null,
3019
- value: toFiniteNumber(cmcFearGreed.value),
3020
- valueChange24h: toFiniteNumber(cmcFearGreed.valueChange24h),
3021
- valueChange7d: toFiniteNumber(cmcFearGreed.valueChange7d),
3022
- classification: typeof cmcFearGreed.classification === "string" ? cmcFearGreed.classification : null,
3023
- sentimentRegime: typeof cmcFearGreed.sentimentRegime === "string" ? cmcFearGreed.sentimentRegime : null
3721
+ buyPressurePct: toFiniteNumber(delta.buyPressurePct),
3722
+ buyVolume: toFiniteNumber(delta.buyVolume),
3723
+ sellVolume: toFiniteNumber(delta.sellVolume),
3724
+ netDelta: toFiniteNumber(delta.netDelta),
3725
+ deltaPct: toFiniteNumber(delta.deltaPct),
3726
+ signedVolumeZScore: toFiniteNumber(delta.signedVolumeZScore)
3024
3727
  };
3025
3728
  };
3026
- var buildCmcIndexesContextFromSignal = (signal) => {
3729
+ var buildTradeFlowContextFromSignal = (signal) => {
3027
3730
  const baseContext = toRecord(signal.additionalIndicators?.baseContext);
3028
- const relative = toRecord(baseContext?.relative);
3029
- const cmcIndexes = toRecord(relative?.cmcIndexes);
3030
- if (!cmcIndexes) {
3731
+ const participation = toRecord(baseContext?.participation);
3732
+ const tradeFlow = toRecord(participation?.tradeFlow);
3733
+ if (!tradeFlow) {
3031
3734
  return {
3032
3735
  source: null,
3033
3736
  available: false,
3034
3737
  interval: null,
3035
- asOfTs: null,
3036
3738
  stale: null,
3037
- cmc100Value: null,
3038
- cmc100Change24hPct: null,
3039
- cmc100TopConstituentSymbol: null,
3040
- cmc100TopConstituentWeightPct: null,
3041
- cmc20Value: null,
3042
- cmc20Change24hPct: null,
3043
- cmc20TopConstituentSymbol: null,
3044
- cmc20TopConstituentWeightPct: null,
3045
- cmc20ToCmc100Ratio: null,
3046
- cmc20ToCmc100RatioChange24hPct: null,
3047
- indexRegime: null
3739
+ trades: null,
3740
+ buyPressurePct: null,
3741
+ netBaseDelta: null,
3742
+ netQuoteDelta: null
3048
3743
  };
3049
3744
  }
3050
3745
  return {
3051
- source: String(cmcIndexes.source ?? ""),
3746
+ source: String(tradeFlow.source ?? ""),
3052
3747
  available: true,
3053
- interval: typeof cmcIndexes.interval === "string" ? cmcIndexes.interval : null,
3054
- asOfTs: toFiniteNumber(cmcIndexes.asOfTs),
3055
- stale: typeof cmcIndexes.stale === "boolean" ? cmcIndexes.stale : null,
3056
- cmc100Value: toFiniteNumber(cmcIndexes.cmc100Value),
3057
- cmc100Change24hPct: toFiniteNumber(cmcIndexes.cmc100Change24hPct),
3058
- cmc100TopConstituentSymbol: typeof cmcIndexes.cmc100TopConstituentSymbol === "string" ? cmcIndexes.cmc100TopConstituentSymbol : null,
3059
- cmc100TopConstituentWeightPct: toFiniteNumber(
3060
- cmcIndexes.cmc100TopConstituentWeightPct
3061
- ),
3062
- cmc20Value: toFiniteNumber(cmcIndexes.cmc20Value),
3063
- cmc20Change24hPct: toFiniteNumber(cmcIndexes.cmc20Change24hPct),
3064
- cmc20TopConstituentSymbol: typeof cmcIndexes.cmc20TopConstituentSymbol === "string" ? cmcIndexes.cmc20TopConstituentSymbol : null,
3065
- cmc20TopConstituentWeightPct: toFiniteNumber(
3066
- cmcIndexes.cmc20TopConstituentWeightPct
3067
- ),
3068
- cmc20ToCmc100Ratio: toFiniteNumber(cmcIndexes.cmc20ToCmc100Ratio),
3069
- cmc20ToCmc100RatioChange24hPct: toFiniteNumber(
3070
- cmcIndexes.cmc20ToCmc100RatioChange24hPct
3071
- ),
3072
- indexRegime: typeof cmcIndexes.indexRegime === "string" ? cmcIndexes.indexRegime : null
3748
+ interval: String(tradeFlow.interval ?? ""),
3749
+ stale: typeof tradeFlow.stale === "boolean" ? tradeFlow.stale : null,
3750
+ trades: toFiniteNumber(tradeFlow.trades),
3751
+ buyPressurePct: toFiniteNumber(tradeFlow.buyPressurePct),
3752
+ netBaseDelta: toFiniteNumber(tradeFlow.netBaseDelta),
3753
+ netQuoteDelta: toFiniteNumber(tradeFlow.netQuoteDelta)
3073
3754
  };
3074
3755
  };
3075
- var buildReferenceTradeFlowContextFromSignal = (signal) => {
3756
+ var buildMarketBreadthContextFromSignal = (signal) => {
3076
3757
  const baseContext = toRecord(signal.additionalIndicators?.baseContext);
3077
3758
  const relative = toRecord(baseContext?.relative);
3078
- const refs = toRecord(relative?.referenceTradeFlow);
3079
- const primaryReferenceSymbol = typeof refs?.primaryReferenceSymbol === "string" ? refs.primaryReferenceSymbol : null;
3080
- const tradeFlowBySymbol = toRecord(refs?.tradeFlowBySymbol);
3081
- const primaryTradeFlow = primaryReferenceSymbol != null ? toRecord(tradeFlowBySymbol?.[primaryReferenceSymbol]) : null;
3082
- if (!refs) {
3759
+ const breadth = toRecord(relative?.marketBreadth);
3760
+ if (!breadth) {
3083
3761
  return {
3084
3762
  source: null,
3085
3763
  available: false,
3086
- primaryReferenceSymbol: null,
3087
- referenceSymbols: [],
3088
- primaryTradeFlowBuyPressurePct: null,
3089
- primaryTradeFlowStale: null
3764
+ universe: null,
3765
+ interval: null,
3766
+ stale: null,
3767
+ symbolsCount: null,
3768
+ advanceDeclineRatio: null,
3769
+ pctAboveMa20: null,
3770
+ pctAboveMa50: null,
3771
+ equalWeightedReturn: null,
3772
+ volumeWeightedReturn: null,
3773
+ dispersion: null
3090
3774
  };
3091
3775
  }
3092
3776
  return {
3093
- source: String(refs.source ?? ""),
3777
+ source: String(breadth.source ?? ""),
3094
3778
  available: true,
3095
- primaryReferenceSymbol,
3096
- referenceSymbols: Array.isArray(refs.referenceSymbols) ? refs.referenceSymbols.map(String) : [],
3097
- primaryTradeFlowBuyPressurePct: toFiniteNumber(
3098
- primaryTradeFlow?.buyPressurePct
3099
- ),
3100
- primaryTradeFlowStale: typeof primaryTradeFlow?.stale === "boolean" ? primaryTradeFlow.stale : null
3101
- };
3102
- };
3103
- var buildAiMarketContext = (signal) => ({
3104
- execution: {
3105
- binanceCoinbaseSpread: buildSpreadContextFromSignal(signal)
3106
- },
3107
- participation: {
3108
- trueDelta: buildTrueDeltaContextFromSignal(signal),
3109
- tradeFlow: buildTradeFlowContextFromSignal(signal)
3110
- },
3111
- relative: {
3112
- marketBreadth: buildMarketBreadthContextFromSignal(signal),
3113
- marketBreadths: buildMarketBreadthsContextFromSignal(signal),
3114
- targetVsBtc: buildTargetVsBtcContextFromSignal(signal),
3115
- btcAltRegime: buildBtcAltRegimeContextFromSignal(signal),
3116
- cmcGlobal: buildCmcGlobalContextFromSignal(signal),
3117
- cmcReferenceAssets: buildCmcReferenceAssetsContextFromSignal(signal),
3118
- cmcExchangeLiquidity: buildCmcExchangeLiquidityContextFromSignal(signal),
3119
- cmcFearGreed: buildCmcFearGreedContextFromSignal(signal),
3120
- cmcIndexes: buildCmcIndexesContextFromSignal(signal),
3121
- referenceTradeFlow: buildReferenceTradeFlowContextFromSignal(signal)
3122
- }
3123
- });
3124
-
3125
- // src/strategy/manifests.ts
3126
- var import_indicators = require("@tradejs/core/indicators");
3127
- var SHARED_STRATEGY_REGISTRY_KEY = "__tradejsNodeSharedStrategyRegistryV1__";
3128
- var sharedRegistryScope = globalThis;
3129
- var sharedStrategyRegistry = sharedRegistryScope[SHARED_STRATEGY_REGISTRY_KEY] ?? (sharedRegistryScope[SHARED_STRATEGY_REGISTRY_KEY] = {
3130
- registryStateByProjectRoot: /* @__PURE__ */ new Map()
3131
- });
3132
- var createStrategyRegistryState = () => ({
3133
- strategyCreators: /* @__PURE__ */ new Map(),
3134
- strategyManifestsMap: /* @__PURE__ */ new Map(),
3135
- strategyEntriesMap: /* @__PURE__ */ new Map(),
3136
- strategySourcesMap: /* @__PURE__ */ new Map(),
3137
- strategyModuleResolutions: /* @__PURE__ */ new Map(),
3138
- pluginsLoadPromise: null
3139
- });
3140
- var registryStateByProjectRoot2 = sharedStrategyRegistry.registryStateByProjectRoot;
3141
- var getStrategyRegistryState = (cwd = getTradejsProjectCwd()) => {
3142
- const projectRoot = getTradejsProjectCwd(cwd);
3143
- let state = registryStateByProjectRoot2.get(projectRoot);
3144
- if (!state) {
3145
- state = createStrategyRegistryState();
3146
- registryStateByProjectRoot2.set(projectRoot, state);
3147
- }
3148
- return {
3149
- projectRoot,
3150
- state
3151
- };
3152
- };
3153
- var toUniqueModules2 = (modules = []) => [
3154
- ...new Set(modules.map((moduleName) => moduleName.trim()).filter(Boolean))
3155
- ];
3156
- var getConfiguredPluginModuleNames = async (cwd = getTradejsProjectCwd()) => {
3157
- const config = await loadTradejsConfig(cwd);
3158
- return {
3159
- strategyModules: toUniqueModules2(config.strategies),
3160
- indicatorModules: toUniqueModules2(config.indicators)
3779
+ universe: String(breadth.universe ?? ""),
3780
+ interval: String(breadth.interval ?? ""),
3781
+ stale: typeof breadth.stale === "boolean" ? breadth.stale : null,
3782
+ symbolsCount: toFiniteNumber(breadth.symbolsCount),
3783
+ advanceDeclineRatio: toFiniteNumber(breadth.advanceDeclineRatio),
3784
+ pctAboveMa20: toFiniteNumber(breadth.pctAboveMa20),
3785
+ pctAboveMa50: toFiniteNumber(breadth.pctAboveMa50),
3786
+ equalWeightedReturn: toFiniteNumber(breadth.equalWeightedReturn),
3787
+ volumeWeightedReturn: toFiniteNumber(breadth.volumeWeightedReturn),
3788
+ dispersion: toFiniteNumber(breadth.dispersion)
3161
3789
  };
3162
3790
  };
3163
- var extractModuleEntries = (moduleExport, key) => {
3164
- if (!moduleExport || typeof moduleExport !== "object") {
3165
- return null;
3166
- }
3167
- const candidate = moduleExport;
3168
- if (Array.isArray(candidate[key])) {
3169
- return candidate[key];
3170
- }
3171
- const defaultExport = candidate.default;
3172
- if (defaultExport && Array.isArray(defaultExport[key])) {
3173
- return defaultExport[key];
3174
- }
3175
- return null;
3176
- };
3177
- var extractStrategyPluginDefinition = (moduleExport) => {
3178
- const strategyEntries = extractModuleEntries(
3179
- moduleExport,
3180
- "strategyEntries"
3181
- );
3182
- return strategyEntries ? { strategyEntries } : null;
3183
- };
3184
- var extractIndicatorPluginDefinition = (moduleExport) => {
3185
- const indicatorEntries = extractModuleEntries(
3186
- moduleExport,
3187
- "indicatorEntries"
3188
- );
3189
- return indicatorEntries ? { indicatorEntries } : null;
3190
- };
3191
- var validateStrategyEntries = (moduleName, entries, state) => {
3192
- const issues = [];
3193
- const names = /* @__PURE__ */ new Set();
3194
- entries.forEach((entry, index) => {
3195
- const entryPath = `${moduleName}.strategyEntries[${index}]`;
3196
- const strategyName = entry?.manifest?.name;
3197
- if (typeof strategyName !== "string" || !strategyName.trim()) {
3198
- issues.push(`${entryPath}: manifest.name is required`);
3199
- } else if (names.has(strategyName) || state?.strategyEntriesMap.has(strategyName)) {
3200
- issues.push(`${entryPath}: duplicate strategy ${strategyName}`);
3201
- } else {
3202
- names.add(strategyName);
3203
- }
3204
- if (!entry || typeof entry !== "object" || !entry.defaults || typeof entry.defaults !== "object" || Array.isArray(entry.defaults)) {
3205
- issues.push(`${entryPath}: defaults must be an object`);
3206
- }
3207
- if (typeof entry?.parseConfig !== "function") {
3208
- issues.push(`${entryPath}: parseConfig is required`);
3209
- }
3210
- if (typeof entry?.createCore !== "function") {
3211
- issues.push(`${entryPath}: createCore is required`);
3791
+ var buildMarketBreadthsContextFromSignal = (signal) => {
3792
+ const baseContext = toRecord(signal.additionalIndicators?.baseContext);
3793
+ const relative = toRecord(baseContext?.relative);
3794
+ const breadths = toRecord(relative?.marketBreadths);
3795
+ const build = (key) => {
3796
+ const breadth = toRecord(breadths?.[key]);
3797
+ if (!breadth) {
3798
+ return {
3799
+ source: null,
3800
+ available: false,
3801
+ universe: null,
3802
+ interval: null,
3803
+ stale: null,
3804
+ symbolsCount: null,
3805
+ advanceDeclineRatio: null,
3806
+ pctAboveMa20: null,
3807
+ pctAboveMa50: null,
3808
+ equalWeightedReturn: null,
3809
+ volumeWeightedReturn: null,
3810
+ dispersion: null
3811
+ };
3212
3812
  }
3213
- });
3214
- return issues;
3215
- };
3216
- var registerEntries2 = (entries, source, state) => {
3217
- for (const entry of entries) {
3218
- const strategyName = entry.manifest.name;
3219
- state.strategyManifestsMap.set(strategyName, entry.manifest);
3220
- state.strategyEntriesMap.set(strategyName, entry);
3221
- state.strategySourcesMap.set(strategyName, source);
3222
- materializeStrategyCreator(strategyName, state);
3223
- }
3813
+ return {
3814
+ source: String(breadth.source ?? ""),
3815
+ available: true,
3816
+ universe: String(breadth.universe ?? ""),
3817
+ interval: String(breadth.interval ?? ""),
3818
+ stale: typeof breadth.stale === "boolean" ? breadth.stale : null,
3819
+ symbolsCount: toFiniteNumber(breadth.symbolsCount),
3820
+ advanceDeclineRatio: toFiniteNumber(breadth.advanceDeclineRatio),
3821
+ pctAboveMa20: toFiniteNumber(breadth.pctAboveMa20),
3822
+ pctAboveMa50: toFiniteNumber(breadth.pctAboveMa50),
3823
+ equalWeightedReturn: toFiniteNumber(breadth.equalWeightedReturn),
3824
+ volumeWeightedReturn: toFiniteNumber(breadth.volumeWeightedReturn),
3825
+ dispersion: toFiniteNumber(breadth.dispersion)
3826
+ };
3827
+ };
3828
+ return {
3829
+ top5: build("top5"),
3830
+ top10: build("top10"),
3831
+ top30: build("top30"),
3832
+ top50: build("top50"),
3833
+ top100: build("top100")
3834
+ };
3224
3835
  };
3225
- var materializeStrategyCreator = (strategyName, state) => {
3226
- if (state.strategyCreators.has(strategyName) || !sharedStrategyRegistry.strategyRuntimeFactory) {
3227
- return;
3228
- }
3229
- const entry = state.strategyEntriesMap.get(strategyName);
3230
- if (!entry) return;
3231
- state.strategyCreators.set(
3232
- strategyName,
3233
- sharedStrategyRegistry.strategyRuntimeFactory({
3234
- strategyName,
3235
- defaults: entry.defaults,
3236
- createCore: entry.createCore,
3237
- manifest: entry.manifest,
3238
- detectorKey: entry.detectorKey,
3239
- detectorNoSignalSkipReason: entry.detectorNoSignalSkipReason,
3240
- resolveRegisteredManifest: (name) => state.strategyManifestsMap.get(name)
3241
- })
3242
- );
3836
+ var buildTargetVsReference = (signal, asset) => {
3837
+ const base = toRecord(signal.additionalIndicators?.baseContext);
3838
+ const relative = toRecord(base?.relative);
3839
+ const target = toRecord(relative?.[`targetVs${asset}`]);
3840
+ return {
3841
+ source: target ? String(target.source ?? "") : null,
3842
+ available: Boolean(target),
3843
+ ratioReturn1h: toFiniteNumber(target?.ratioReturn1h),
3844
+ ratioReturn4h: toFiniteNumber(target?.ratioReturn4h),
3845
+ ratioReturn24h: toFiniteNumber(target?.ratioReturn24h),
3846
+ alpha1h: toFiniteNumber(target?.[`alphaVs${asset}1h`]),
3847
+ alpha4h: toFiniteNumber(target?.[`alphaVs${asset}4h`]),
3848
+ alpha24h: toFiniteNumber(target?.[`alphaVs${asset}24h`]),
3849
+ beta: toFiniteNumber(target?.[`betaTo${asset}20`]),
3850
+ correlation: toFiniteNumber(target?.[`correlationTo${asset}20`]),
3851
+ ratioTrend: typeof target?.ratioTrend === "string" ? target.ratioTrend : null
3852
+ };
3243
3853
  };
3244
- var setStrategyRuntimeFactory = (factory) => {
3245
- sharedStrategyRegistry.strategyRuntimeFactory = factory;
3246
- for (const state of registryStateByProjectRoot2.values()) {
3247
- for (const strategyName of state.strategyEntriesMap.keys()) {
3248
- materializeStrategyCreator(strategyName, state);
3249
- }
3250
- }
3854
+ var buildTargetVsBtcContextFromSignal = (signal) => {
3855
+ const { alpha1h, alpha4h, alpha24h, beta, correlation, ...shared } = buildTargetVsReference(signal, "Btc");
3856
+ return {
3857
+ ...shared,
3858
+ alphaVsBtc1h: alpha1h,
3859
+ alphaVsBtc4h: alpha4h,
3860
+ alphaVsBtc24h: alpha24h,
3861
+ betaToBtc20: beta,
3862
+ correlationToBtc20: correlation
3863
+ };
3251
3864
  };
3252
- var importStrategyPluginModule = async (moduleName, cwd = getTradejsProjectCwd()) => {
3253
- if (typeof importTradejsModule === "function") {
3254
- return importTradejsModule(moduleName, cwd);
3255
- }
3256
- return import(
3257
- /* webpackIgnore: true */
3258
- moduleName
3259
- );
3865
+ var buildTargetVsEthContextFromSignal = (signal) => {
3866
+ const { alpha1h, alpha4h, alpha24h, beta, correlation, ...shared } = buildTargetVsReference(signal, "Eth");
3867
+ return {
3868
+ ...shared,
3869
+ alphaVsEth1h: alpha1h,
3870
+ alphaVsEth4h: alpha4h,
3871
+ alphaVsEth24h: alpha24h,
3872
+ betaToEth20: beta,
3873
+ correlationToEth20: correlation
3874
+ };
3260
3875
  };
3261
- var resolveStrategyModuleEntry = async ({
3262
- strategyName,
3263
- moduleName,
3264
- cwd
3265
- }) => {
3266
- const { projectRoot, state } = getStrategyRegistryState(cwd);
3267
- const source = moduleName.trim();
3268
- const resolvedModuleName = resolvePluginModuleSpecifier(source, projectRoot);
3269
- const cacheKey = `${resolvedModuleName}\0${strategyName}`;
3270
- const cached = state.strategyModuleResolutions.get(cacheKey);
3271
- if (cached) return cached;
3272
- const resolution = (async () => {
3273
- const moduleExport = await importStrategyPluginModule(
3274
- resolvedModuleName,
3275
- projectRoot
3276
- );
3277
- const pluginDefinition = extractStrategyPluginDefinition(moduleExport);
3278
- if (!pluginDefinition) {
3279
- throw new Error(`${source}: export { strategyEntries } is missing`);
3280
- }
3281
- const issues = validateStrategyEntries(
3282
- source,
3283
- pluginDefinition.strategyEntries
3284
- );
3285
- if (issues.length > 0) {
3286
- throw new Error(
3287
- ["Invalid TradeJS strategy module:", ...issues].join("\n")
3288
- );
3289
- }
3290
- const entry = pluginDefinition.strategyEntries.find(
3291
- (candidate) => candidate.manifest.name === strategyName
3292
- );
3293
- if (!entry) {
3294
- throw new Error(`${source}: strategy ${strategyName} is missing`);
3295
- }
3296
- const factory = sharedStrategyRegistry.strategyRuntimeFactory;
3297
- if (!factory) {
3298
- throw new Error("Strategy runtime factory is not configured");
3299
- }
3300
- const manifestsByName = new Map(
3301
- pluginDefinition.strategyEntries.map(
3302
- (candidate) => [candidate.manifest.name, candidate.manifest]
3303
- )
3304
- );
3876
+ var buildBtcAltRegimeContextFromSignal = (signal) => {
3877
+ const baseContext = toRecord(signal.additionalIndicators?.baseContext);
3878
+ const relative = toRecord(baseContext?.relative);
3879
+ const btcAltRegime = toRecord(relative?.btcAltRegime);
3880
+ if (!btcAltRegime) {
3305
3881
  return {
3306
- entry,
3307
- creator: factory({
3308
- strategyName,
3309
- defaults: entry.defaults,
3310
- createCore: entry.createCore,
3311
- manifest: entry.manifest,
3312
- detectorKey: entry.detectorKey,
3313
- detectorNoSignalSkipReason: entry.detectorNoSignalSkipReason,
3314
- resolveRegisteredManifest: (name) => manifestsByName.get(name) ?? state.strategyManifestsMap.get(name)
3315
- }),
3316
- source
3882
+ source: null,
3883
+ available: false,
3884
+ universe: null,
3885
+ interval: null,
3886
+ stale: null,
3887
+ regime: null,
3888
+ btcReturn24h: null,
3889
+ altBasketReturn24h: null,
3890
+ btcVsAltReturn24h: null,
3891
+ btcTurnoverShare24h: null,
3892
+ btcTurnoverShareChange24h: null,
3893
+ altVolToBtcVol24h: null,
3894
+ altDispersion24h: null
3317
3895
  };
3318
- })();
3319
- state.strategyModuleResolutions.set(cacheKey, resolution);
3320
- try {
3321
- return await resolution;
3322
- } catch (error) {
3323
- state.strategyModuleResolutions.delete(cacheKey);
3324
- throw error;
3325
3896
  }
3897
+ return {
3898
+ source: String(btcAltRegime.source ?? ""),
3899
+ available: true,
3900
+ universe: String(btcAltRegime.universe ?? ""),
3901
+ interval: String(btcAltRegime.interval ?? ""),
3902
+ stale: typeof btcAltRegime.stale === "boolean" ? btcAltRegime.stale : null,
3903
+ regime: typeof btcAltRegime.regime === "string" ? btcAltRegime.regime : null,
3904
+ btcReturn24h: toFiniteNumber(btcAltRegime.btcReturn24h),
3905
+ altBasketReturn24h: toFiniteNumber(btcAltRegime.altBasketReturn24h),
3906
+ btcVsAltReturn24h: toFiniteNumber(btcAltRegime.btcVsAltReturn24h),
3907
+ btcTurnoverShare24h: toFiniteNumber(btcAltRegime.btcTurnoverShare24h),
3908
+ btcTurnoverShareChange24h: toFiniteNumber(
3909
+ btcAltRegime.btcTurnoverShareChange24h
3910
+ ),
3911
+ altVolToBtcVol24h: toFiniteNumber(btcAltRegime.altVolToBtcVol24h),
3912
+ altDispersion24h: toFiniteNumber(btcAltRegime.altDispersion24h)
3913
+ };
3326
3914
  };
3327
- var ensureStrategyPluginsLoaded = async (cwd = getTradejsProjectCwd()) => {
3328
- const { projectRoot, state } = getStrategyRegistryState(cwd);
3329
- if (!state.pluginsLoadPromise) {
3330
- (0, import_indicators.resetIndicatorRegistryCache)(projectRoot);
3331
- state.pluginsLoadPromise = (async () => {
3332
- const { strategyModules, indicatorModules } = await getConfiguredPluginModuleNames(projectRoot);
3333
- const strategySet = new Set(strategyModules);
3334
- const indicatorSet = new Set(indicatorModules);
3335
- const pluginModuleNames = [
3336
- .../* @__PURE__ */ new Set([...strategyModules, ...indicatorModules])
3337
- ];
3338
- if (!pluginModuleNames.length) {
3339
- return;
3340
- }
3341
- const issues = [];
3342
- for (const moduleName of pluginModuleNames) {
3343
- try {
3344
- const resolvedModuleName = resolvePluginModuleSpecifier(
3345
- moduleName,
3346
- projectRoot
3347
- );
3348
- const moduleExport = await importStrategyPluginModule(
3349
- resolvedModuleName,
3350
- projectRoot
3351
- );
3352
- if (strategySet.has(moduleName)) {
3353
- const pluginDefinition = extractStrategyPluginDefinition(moduleExport);
3354
- if (!pluginDefinition) {
3355
- issues.push(
3356
- `${moduleName}: export { strategyEntries } is missing`
3357
- );
3358
- } else {
3359
- const entryIssues = validateStrategyEntries(
3360
- moduleName,
3361
- pluginDefinition.strategyEntries,
3362
- state
3363
- );
3364
- issues.push(...entryIssues);
3365
- if (entryIssues.length === 0) {
3366
- registerEntries2(
3367
- pluginDefinition.strategyEntries,
3368
- moduleName,
3369
- state
3370
- );
3371
- }
3372
- }
3373
- }
3374
- if (indicatorSet.has(moduleName)) {
3375
- const indicatorPluginDefinition = extractIndicatorPluginDefinition(moduleExport);
3376
- if (!indicatorPluginDefinition) {
3377
- issues.push(
3378
- `${moduleName}: export { indicatorEntries } is missing`
3379
- );
3380
- } else {
3381
- (0, import_indicators.registerIndicatorEntries)(
3382
- indicatorPluginDefinition.indicatorEntries,
3383
- moduleName,
3384
- projectRoot
3385
- );
3386
- }
3387
- }
3388
- if (!strategySet.has(moduleName) && !indicatorSet.has(moduleName)) {
3389
- issues.push(`${moduleName}: plugin is not declared in config`);
3390
- }
3391
- } catch (error) {
3392
- issues.push(`${moduleName}: failed to import: ${String(error)}`);
3393
- }
3394
- }
3395
- if (issues.length > 0) {
3396
- throw new Error(
3397
- ["Invalid TradeJS plugin catalog:", ...issues].join("\n")
3398
- );
3399
- }
3400
- })();
3915
+ var buildCmcGlobalContextFromSignal = (signal) => {
3916
+ const baseContext = toRecord(signal.additionalIndicators?.baseContext);
3917
+ const relative = toRecord(baseContext?.relative);
3918
+ const cmcGlobal = toRecord(relative?.cmcGlobal);
3919
+ if (!cmcGlobal) {
3920
+ return {
3921
+ source: null,
3922
+ available: false,
3923
+ interval: null,
3924
+ asOfTs: null,
3925
+ stale: null,
3926
+ totalMarketCapUsd: null,
3927
+ totalVolumeUsd: null,
3928
+ totalVolumeReportedUsd: null,
3929
+ altMarketCapUsd: null,
3930
+ altVolumeUsd: null,
3931
+ altVolumeReportedUsd: null,
3932
+ btcDominancePct: null,
3933
+ ethDominancePct: null,
3934
+ btcDominanceChange24hPct: null,
3935
+ ethDominanceChange24hPct: null,
3936
+ altMarketCapChange24hPct: null,
3937
+ altVolumeChange24hPct: null,
3938
+ activeCryptocurrencies: null,
3939
+ activeExchanges: null,
3940
+ activeMarketPairs: null,
3941
+ altLiquidityRegime: null
3942
+ };
3401
3943
  }
3402
- await state.pluginsLoadPromise;
3944
+ return {
3945
+ source: String(cmcGlobal.source ?? ""),
3946
+ available: true,
3947
+ interval: typeof cmcGlobal.interval === "string" ? cmcGlobal.interval : null,
3948
+ asOfTs: toFiniteNumber(cmcGlobal.asOfTs),
3949
+ stale: typeof cmcGlobal.stale === "boolean" ? cmcGlobal.stale : null,
3950
+ totalMarketCapUsd: toFiniteNumber(cmcGlobal.totalMarketCapUsd),
3951
+ totalVolumeUsd: toFiniteNumber(cmcGlobal.totalVolumeUsd),
3952
+ totalVolumeReportedUsd: toFiniteNumber(cmcGlobal.totalVolumeReportedUsd),
3953
+ altMarketCapUsd: toFiniteNumber(cmcGlobal.altMarketCapUsd),
3954
+ altVolumeUsd: toFiniteNumber(cmcGlobal.altVolumeUsd),
3955
+ altVolumeReportedUsd: toFiniteNumber(cmcGlobal.altVolumeReportedUsd),
3956
+ btcDominancePct: toFiniteNumber(cmcGlobal.btcDominancePct),
3957
+ ethDominancePct: toFiniteNumber(cmcGlobal.ethDominancePct),
3958
+ btcDominanceChange24hPct: toFiniteNumber(
3959
+ cmcGlobal.btcDominanceChange24hPct
3960
+ ),
3961
+ ethDominanceChange24hPct: toFiniteNumber(
3962
+ cmcGlobal.ethDominanceChange24hPct
3963
+ ),
3964
+ altMarketCapChange24hPct: toFiniteNumber(
3965
+ cmcGlobal.altMarketCapChange24hPct
3966
+ ),
3967
+ altVolumeChange24hPct: toFiniteNumber(cmcGlobal.altVolumeChange24hPct),
3968
+ activeCryptocurrencies: toFiniteNumber(cmcGlobal.activeCryptocurrencies),
3969
+ activeExchanges: toFiniteNumber(cmcGlobal.activeExchanges),
3970
+ activeMarketPairs: toFiniteNumber(cmcGlobal.activeMarketPairs),
3971
+ altLiquidityRegime: typeof cmcGlobal.altLiquidityRegime === "string" ? cmcGlobal.altLiquidityRegime : null
3972
+ };
3403
3973
  };
3404
- var getStrategyCreator = async (name, cwd = getTradejsProjectCwd(), moduleName) => {
3405
- if (moduleName?.trim()) {
3406
- await ensureStrategyPluginsLoaded(cwd);
3407
- return (await resolveStrategyModuleEntry({ strategyName: name, moduleName, cwd })).creator;
3974
+ var buildCmcReferenceAssetsContextFromSignal = (signal) => {
3975
+ const baseContext = toRecord(signal.additionalIndicators?.baseContext);
3976
+ const relative = toRecord(baseContext?.relative);
3977
+ const cmcReferenceAssets = toRecord(relative?.cmcReferenceAssets);
3978
+ if (!cmcReferenceAssets) {
3979
+ return {
3980
+ source: null,
3981
+ available: false,
3982
+ interval: null,
3983
+ asOfTs: null,
3984
+ stale: null,
3985
+ btcMarketCapUsd: null,
3986
+ ethMarketCapUsd: null,
3987
+ btcVolumeUsd: null,
3988
+ ethVolumeUsd: null,
3989
+ btcVolumeToMarketCap: null,
3990
+ ethVolumeToMarketCap: null,
3991
+ ethBtcMarketCapRatio: null,
3992
+ ethBtcMarketCapRatioChange24hPct: null,
3993
+ ethVsBtcVolumeRatio: null,
3994
+ referenceLiquidityRegime: null
3995
+ };
3408
3996
  }
3409
- await ensureStrategyPluginsLoaded(cwd);
3410
- const { state } = getStrategyRegistryState(cwd);
3411
- return state.strategyCreators.get(name);
3997
+ return {
3998
+ source: String(cmcReferenceAssets.source ?? ""),
3999
+ available: true,
4000
+ interval: typeof cmcReferenceAssets.interval === "string" ? cmcReferenceAssets.interval : null,
4001
+ asOfTs: toFiniteNumber(cmcReferenceAssets.asOfTs),
4002
+ stale: typeof cmcReferenceAssets.stale === "boolean" ? cmcReferenceAssets.stale : null,
4003
+ btcMarketCapUsd: toFiniteNumber(cmcReferenceAssets.btcMarketCapUsd),
4004
+ ethMarketCapUsd: toFiniteNumber(cmcReferenceAssets.ethMarketCapUsd),
4005
+ btcVolumeUsd: toFiniteNumber(cmcReferenceAssets.btcVolumeUsd),
4006
+ ethVolumeUsd: toFiniteNumber(cmcReferenceAssets.ethVolumeUsd),
4007
+ btcVolumeToMarketCap: toFiniteNumber(
4008
+ cmcReferenceAssets.btcVolumeToMarketCap
4009
+ ),
4010
+ ethVolumeToMarketCap: toFiniteNumber(
4011
+ cmcReferenceAssets.ethVolumeToMarketCap
4012
+ ),
4013
+ ethBtcMarketCapRatio: toFiniteNumber(
4014
+ cmcReferenceAssets.ethBtcMarketCapRatio
4015
+ ),
4016
+ ethBtcMarketCapRatioChange24hPct: toFiniteNumber(
4017
+ cmcReferenceAssets.ethBtcMarketCapRatioChange24hPct
4018
+ ),
4019
+ ethVsBtcVolumeRatio: toFiniteNumber(cmcReferenceAssets.ethVsBtcVolumeRatio),
4020
+ referenceLiquidityRegime: typeof cmcReferenceAssets.referenceLiquidityRegime === "string" ? cmcReferenceAssets.referenceLiquidityRegime : null
4021
+ };
3412
4022
  };
3413
- var getStrategyEntry = async (name, cwd = getTradejsProjectCwd(), moduleName) => {
3414
- if (moduleName?.trim()) {
3415
- await ensureStrategyPluginsLoaded(cwd);
3416
- return (await resolveStrategyModuleEntry({ strategyName: name, moduleName, cwd })).entry;
4023
+ var buildCmcExchangeLiquidityContextFromSignal = (signal) => {
4024
+ const baseContext = toRecord(signal.additionalIndicators?.baseContext);
4025
+ const relative = toRecord(baseContext?.relative);
4026
+ const cmcExchangeLiquidity = toRecord(relative?.cmcExchangeLiquidity);
4027
+ if (!cmcExchangeLiquidity) {
4028
+ return {
4029
+ source: null,
4030
+ available: false,
4031
+ interval: null,
4032
+ asOfTs: null,
4033
+ stale: null,
4034
+ exchangesCount: null,
4035
+ totalVolumeUsd: null,
4036
+ totalVolumeChange24hPct: null,
4037
+ binanceVolumeUsd: null,
4038
+ binanceVolumeShare: null,
4039
+ topExchangeVolumeShare: null,
4040
+ liquidityRegime: null
4041
+ };
3417
4042
  }
3418
- await ensureStrategyPluginsLoaded(cwd);
3419
- const { state } = getStrategyRegistryState(cwd);
3420
- return state.strategyEntriesMap.get(name);
4043
+ return {
4044
+ source: String(cmcExchangeLiquidity.source ?? ""),
4045
+ available: true,
4046
+ interval: typeof cmcExchangeLiquidity.interval === "string" ? cmcExchangeLiquidity.interval : null,
4047
+ asOfTs: toFiniteNumber(cmcExchangeLiquidity.asOfTs),
4048
+ stale: typeof cmcExchangeLiquidity.stale === "boolean" ? cmcExchangeLiquidity.stale : null,
4049
+ exchangesCount: toFiniteNumber(cmcExchangeLiquidity.exchangesCount),
4050
+ totalVolumeUsd: toFiniteNumber(cmcExchangeLiquidity.totalVolumeUsd),
4051
+ totalVolumeChange24hPct: toFiniteNumber(
4052
+ cmcExchangeLiquidity.totalVolumeChange24hPct
4053
+ ),
4054
+ binanceVolumeUsd: toFiniteNumber(cmcExchangeLiquidity.binanceVolumeUsd),
4055
+ binanceVolumeShare: toFiniteNumber(cmcExchangeLiquidity.binanceVolumeShare),
4056
+ topExchangeVolumeShare: toFiniteNumber(
4057
+ cmcExchangeLiquidity.topExchangeVolumeShare
4058
+ ),
4059
+ liquidityRegime: typeof cmcExchangeLiquidity.liquidityRegime === "string" ? cmcExchangeLiquidity.liquidityRegime : null
4060
+ };
3421
4061
  };
3422
- var getStrategyPluginSource = async (name, cwd = getTradejsProjectCwd(), moduleName) => {
3423
- if (moduleName?.trim()) {
3424
- await ensureStrategyPluginsLoaded(cwd);
3425
- return (await resolveStrategyModuleEntry({ strategyName: name, moduleName, cwd })).source;
4062
+ var buildCmcFearGreedContextFromSignal = (signal) => {
4063
+ const baseContext = toRecord(signal.additionalIndicators?.baseContext);
4064
+ const relative = toRecord(baseContext?.relative);
4065
+ const cmcFearGreed = toRecord(relative?.cmcFearGreed);
4066
+ if (!cmcFearGreed) {
4067
+ return {
4068
+ source: null,
4069
+ available: false,
4070
+ interval: null,
4071
+ asOfTs: null,
4072
+ stale: null,
4073
+ value: null,
4074
+ valueChange24h: null,
4075
+ valueChange7d: null,
4076
+ classification: null,
4077
+ sentimentRegime: null
4078
+ };
3426
4079
  }
3427
- await ensureStrategyPluginsLoaded(cwd);
3428
- const { state } = getStrategyRegistryState(cwd);
3429
- return state.strategySourcesMap.get(name);
4080
+ return {
4081
+ source: String(cmcFearGreed.source ?? ""),
4082
+ available: true,
4083
+ interval: typeof cmcFearGreed.interval === "string" ? cmcFearGreed.interval : null,
4084
+ asOfTs: toFiniteNumber(cmcFearGreed.asOfTs),
4085
+ stale: typeof cmcFearGreed.stale === "boolean" ? cmcFearGreed.stale : null,
4086
+ value: toFiniteNumber(cmcFearGreed.value),
4087
+ valueChange24h: toFiniteNumber(cmcFearGreed.valueChange24h),
4088
+ valueChange7d: toFiniteNumber(cmcFearGreed.valueChange7d),
4089
+ classification: typeof cmcFearGreed.classification === "string" ? cmcFearGreed.classification : null,
4090
+ sentimentRegime: typeof cmcFearGreed.sentimentRegime === "string" ? cmcFearGreed.sentimentRegime : null
4091
+ };
3430
4092
  };
3431
- var getStrategyManifest = (name, cwd = getTradejsProjectCwd()) => {
3432
- if (!name) {
3433
- return void 0;
4093
+ var buildCmcIndexesContextFromSignal = (signal) => {
4094
+ const baseContext = toRecord(signal.additionalIndicators?.baseContext);
4095
+ const relative = toRecord(baseContext?.relative);
4096
+ const cmcIndexes = toRecord(relative?.cmcIndexes);
4097
+ if (!cmcIndexes) {
4098
+ return {
4099
+ source: null,
4100
+ available: false,
4101
+ interval: null,
4102
+ asOfTs: null,
4103
+ stale: null,
4104
+ cmc100Value: null,
4105
+ cmc100Change24hPct: null,
4106
+ cmc100TopConstituentSymbol: null,
4107
+ cmc100TopConstituentWeightPct: null,
4108
+ cmc20Value: null,
4109
+ cmc20Change24hPct: null,
4110
+ cmc20TopConstituentSymbol: null,
4111
+ cmc20TopConstituentWeightPct: null,
4112
+ cmc20ToCmc100Ratio: null,
4113
+ cmc20ToCmc100RatioChange24hPct: null,
4114
+ indexRegime: null
4115
+ };
3434
4116
  }
3435
- const { state } = getStrategyRegistryState(cwd);
3436
- return state.strategyManifestsMap.get(name);
4117
+ return {
4118
+ source: String(cmcIndexes.source ?? ""),
4119
+ available: true,
4120
+ interval: typeof cmcIndexes.interval === "string" ? cmcIndexes.interval : null,
4121
+ asOfTs: toFiniteNumber(cmcIndexes.asOfTs),
4122
+ stale: typeof cmcIndexes.stale === "boolean" ? cmcIndexes.stale : null,
4123
+ cmc100Value: toFiniteNumber(cmcIndexes.cmc100Value),
4124
+ cmc100Change24hPct: toFiniteNumber(cmcIndexes.cmc100Change24hPct),
4125
+ cmc100TopConstituentSymbol: typeof cmcIndexes.cmc100TopConstituentSymbol === "string" ? cmcIndexes.cmc100TopConstituentSymbol : null,
4126
+ cmc100TopConstituentWeightPct: toFiniteNumber(
4127
+ cmcIndexes.cmc100TopConstituentWeightPct
4128
+ ),
4129
+ cmc20Value: toFiniteNumber(cmcIndexes.cmc20Value),
4130
+ cmc20Change24hPct: toFiniteNumber(cmcIndexes.cmc20Change24hPct),
4131
+ cmc20TopConstituentSymbol: typeof cmcIndexes.cmc20TopConstituentSymbol === "string" ? cmcIndexes.cmc20TopConstituentSymbol : null,
4132
+ cmc20TopConstituentWeightPct: toFiniteNumber(
4133
+ cmcIndexes.cmc20TopConstituentWeightPct
4134
+ ),
4135
+ cmc20ToCmc100Ratio: toFiniteNumber(cmcIndexes.cmc20ToCmc100Ratio),
4136
+ cmc20ToCmc100RatioChange24hPct: toFiniteNumber(
4137
+ cmcIndexes.cmc20ToCmc100RatioChange24hPct
4138
+ ),
4139
+ indexRegime: typeof cmcIndexes.indexRegime === "string" ? cmcIndexes.indexRegime : null
4140
+ };
4141
+ };
4142
+ var buildReferenceTradeFlowContextFromSignal = (signal) => {
4143
+ const baseContext = toRecord(signal.additionalIndicators?.baseContext);
4144
+ const relative = toRecord(baseContext?.relative);
4145
+ const refs = toRecord(relative?.referenceTradeFlow);
4146
+ const primaryReferenceSymbol = typeof refs?.primaryReferenceSymbol === "string" ? refs.primaryReferenceSymbol : null;
4147
+ const tradeFlowBySymbol = toRecord(refs?.tradeFlowBySymbol);
4148
+ const primaryTradeFlow = primaryReferenceSymbol != null ? toRecord(tradeFlowBySymbol?.[primaryReferenceSymbol]) : null;
4149
+ if (!refs) {
4150
+ return {
4151
+ source: null,
4152
+ available: false,
4153
+ primaryReferenceSymbol: null,
4154
+ referenceSymbols: [],
4155
+ primaryTradeFlowBuyPressurePct: null,
4156
+ primaryTradeFlowStale: null
4157
+ };
4158
+ }
4159
+ return {
4160
+ source: String(refs.source ?? ""),
4161
+ available: true,
4162
+ primaryReferenceSymbol,
4163
+ referenceSymbols: Array.isArray(refs.referenceSymbols) ? refs.referenceSymbols.map(String) : [],
4164
+ primaryTradeFlowBuyPressurePct: toFiniteNumber(
4165
+ primaryTradeFlow?.buyPressurePct
4166
+ ),
4167
+ primaryTradeFlowStale: typeof primaryTradeFlow?.stale === "boolean" ? primaryTradeFlow.stale : null
4168
+ };
3437
4169
  };
3438
- var strategies = new Proxy(
3439
- {},
3440
- {
3441
- get: (_target, property) => {
3442
- if (typeof property !== "string") {
3443
- return void 0;
3444
- }
3445
- return getStrategyRegistryState().state.strategyCreators.get(property);
3446
- },
3447
- ownKeys: () => {
3448
- return [...getStrategyRegistryState().state.strategyCreators.keys()];
3449
- },
3450
- getOwnPropertyDescriptor: () => ({
3451
- enumerable: true,
3452
- configurable: true
3453
- })
4170
+ var buildAiMarketContext = (signal) => ({
4171
+ execution: {
4172
+ binanceCoinbaseSpread: buildSpreadContextFromSignal(signal)
4173
+ },
4174
+ participation: {
4175
+ trueDelta: buildTrueDeltaContextFromSignal(signal),
4176
+ tradeFlow: buildTradeFlowContextFromSignal(signal)
4177
+ },
4178
+ relative: {
4179
+ marketBreadth: buildMarketBreadthContextFromSignal(signal),
4180
+ marketBreadths: buildMarketBreadthsContextFromSignal(signal),
4181
+ targetVsBtc: buildTargetVsBtcContextFromSignal(signal),
4182
+ targetVsEth: buildTargetVsEthContextFromSignal(signal),
4183
+ btcAltRegime: buildBtcAltRegimeContextFromSignal(signal),
4184
+ cmcGlobal: buildCmcGlobalContextFromSignal(signal),
4185
+ cmcReferenceAssets: buildCmcReferenceAssetsContextFromSignal(signal),
4186
+ cmcExchangeLiquidity: buildCmcExchangeLiquidityContextFromSignal(signal),
4187
+ cmcFearGreed: buildCmcFearGreedContextFromSignal(signal),
4188
+ cmcIndexes: buildCmcIndexesContextFromSignal(signal),
4189
+ referenceTradeFlow: buildReferenceTradeFlowContextFromSignal(signal)
3454
4190
  }
3455
- );
4191
+ });
3456
4192
 
3457
4193
  // src/strategy/policyProfiles.ts
3458
4194
  var profileMatches = (profile, universe, assetClass) => {
@@ -3554,12 +4290,61 @@ var buildBaseAiPayload = (signal) => {
3554
4290
  };
3555
4291
  };
3556
4292
  var defaultAiAdapter = {};
4293
+ var tickerProfileFeatureIds = new Set(
4294
+ import_aiFeatures.TICKER_PROFILE_AI_FEATURES.map(({ id }) => id)
4295
+ );
3557
4296
  var getStrategyAiAdapter = (strategy, profileId) => getStrategyProfileAiAdapter(getStrategyManifest(strategy), profileId) ?? defaultAiAdapter;
3558
4297
  var getSignalAiAdapter = (signal) => getStrategyAiAdapter(signal.strategy, signal.policyProfileId);
3559
4298
  var buildAiPayloadByStrategy = (signal) => {
3560
4299
  const basePayload = buildBaseAiPayload(signal);
3561
4300
  const adapter = getSignalAiAdapter(signal);
3562
- return adapter.buildPayload?.({ signal, basePayload }) ?? basePayload;
4301
+ const payload = adapter.buildPayload?.({ signal, basePayload }) ?? basePayload;
4302
+ const custom = getStrategyManifest(signal.strategy)?.aiFeatures ?? [];
4303
+ const frozen = signal.aiFeatureReplay?.snapshot;
4304
+ if (frozen) (0, import_aiFeatures.validateAiFeatureSnapshot)(frozen);
4305
+ const tickerProfile = signal.additionalIndicators?.tickerProfile ?? signal.additionalIndicators?.baseContext?.tickerProfile;
4306
+ const hasProfile = Boolean(
4307
+ tickerProfile || frozen?.definitions.some(({ id }) => tickerProfileFeatureIds.has(id))
4308
+ );
4309
+ const indicatorFeatures = (0, import_indicators2.getRegisteredIndicatorEntries)(
4310
+ getTradejsProjectCwd()
4311
+ ).flatMap((entry) => entry.indicator.enabled ? entry.aiFeatures ?? [] : []);
4312
+ const hasPortfolio = Boolean(
4313
+ signal.additionalIndicators?.portfolioContext || frozen?.definitions.some(
4314
+ ({ id }) => import_aiFeatures.PORTFOLIO_AI_FEATURES.some((d) => d.id === id)
4315
+ )
4316
+ );
4317
+ const definitions = [
4318
+ ...hasPortfolio ? import_aiFeatures.PORTFOLIO_AI_FEATURES : [],
4319
+ ...hasProfile ? import_aiFeatures.TICKER_PROFILE_AI_FEATURES : [],
4320
+ ...indicatorFeatures,
4321
+ ...custom
4322
+ ];
4323
+ if (!definitions.length && !signal.aiFeatureReplay?.snapshot) return payload;
4324
+ (0, import_aiFeatures.validateAiFeatureDefinitions)(definitions);
4325
+ if (signal.aiFeatureReplay && !frozen) {
4326
+ throw new Error(
4327
+ "Dataset has no frozen AI features; regenerate the decision-time export"
4328
+ );
4329
+ }
4330
+ if (frozen && JSON.stringify(frozen.definitions) !== JSON.stringify(definitions)) {
4331
+ throw new Error(
4332
+ "AI feature definition changed; regenerate the decision-time export"
4333
+ );
4334
+ }
4335
+ return {
4336
+ ...payload,
4337
+ featureSnapshot: frozen ?? (0, import_aiFeatures.projectAiFeatures)(definitions, {
4338
+ signal: tickerProfile ? {
4339
+ ...signal,
4340
+ additionalIndicators: {
4341
+ ...signal.additionalIndicators,
4342
+ tickerProfile
4343
+ }
4344
+ } : signal,
4345
+ payload
4346
+ })
4347
+ };
3563
4348
  };
3564
4349
  var buildAiSystemPromptAddonByStrategy = (signal) => getSignalAiAdapter(signal).buildSystemPromptAddon?.({ signal }) ?? "";
3565
4350
  var buildAiHumanPromptAddonByStrategy = (signal, payload) => getSignalAiAdapter(signal).buildHumanPromptAddon?.({
@@ -3967,358 +4752,110 @@ var runAiPrompt = async ({ systemPrompt, humanPrompt }, options = {}) => {
3967
4752
  );
3968
4753
  const response = await invokeAiPromptChat({
3969
4754
  userName: options.userName,
3970
- model: options.model,
3971
- messages: [
3972
- { role: "system", content: systemPrompt },
3973
- {
3974
- role: "system",
3975
- content: `Write all user-visible text fields in ${responseLanguage}. Keep field names and JSON syntax unchanged.`
3976
- },
3977
- { role: "user", content: humanPrompt }
3978
- ]
3979
- });
3980
- const parsed = parseAIResponse(response.content);
3981
- const normalized = normalizeAnalysis(parsed);
3982
- if (!options.signal) {
3983
- return normalized;
3984
- }
3985
- return postProcessAiAnalysisByStrategy(
3986
- options.signal,
3987
- normalized,
3988
- options.payload
3989
- );
3990
- };
3991
- var runAiPromptLocal = async (signal, options = {}) => {
3992
- const payload = options.payload ?? buildAiPayload(signal);
3993
- const gateContext = getDeterministicAiGateContext(payload);
3994
- const signalDirection = getSignalDirection(signal);
3995
- const deterministicQuality = getDeterministicQuality(gateContext);
3996
- const approvalAllowedNow = typeof gateContext?.approvalAllowedNow === "boolean" ? gateContext.approvalAllowedNow : deterministicQuality >= 4;
3997
- return postProcessLocalAiAnalysisByStrategy(
3998
- signal,
3999
- {
4000
- direction: approvalAllowedNow ? signalDirection : null,
4001
- quality: deterministicQuality,
4002
- needRetest: !approvalAllowedNow,
4003
- retestPrice: null,
4004
- takeProfitPrice: approvalAllowedNow ? signal.prices?.takeProfitPrice ?? null : null,
4005
- stopLossPrice: approvalAllowedNow ? signal.prices?.stopLossPrice ?? null : null
4006
- },
4007
- payload
4008
- );
4009
- };
4010
- var askAI = async (signal, options = {}) => {
4011
- const { symbol } = signal;
4012
- const payload = buildAiPayload(signal);
4013
- const content = await runAiPrompt(
4014
- {
4015
- systemPrompt: buildAiSystemPrompt(signal),
4016
- humanPrompt: buildAiHumanPrompt(signal, payload)
4017
- },
4018
- {
4019
- ...options,
4020
- signal,
4021
- payload
4022
- }
4023
- );
4024
- await (0, import_redis2.setData)(import_redis2.redisKeys.analysis(symbol, signal.signalId), content);
4025
- return content;
4026
- };
4027
-
4028
- // src/strategyAdapters/ml.ts
4029
- var defaultMlAdapter = {
4030
- normalizeStrategyConfig: (strategyConfig) => strategyConfig
4031
- };
4032
- var getStrategyMlAdapter = (strategy, profileId) => {
4033
- const strategyAdapter = getStrategyProfileMlAdapter(
4034
- getStrategyManifest(strategy),
4035
- profileId
4036
- );
4037
- if (!strategyAdapter) return defaultMlAdapter;
4038
- return {
4039
- ...defaultMlAdapter,
4040
- ...strategyAdapter
4041
- };
4042
- };
4043
-
4044
- // src/mlPayload.ts
4045
- var normalizeStrategyConfig = (strategyConfig, strategyName, profileId) => {
4046
- return getStrategyMlAdapter(
4047
- strategyName,
4048
- profileId
4049
- ).normalizeStrategyConfig?.(strategyConfig);
4050
- };
4051
- var buildMlPayload = (payload) => {
4052
- const strategyName = payload.signal?.strategy ?? payload.context?.strategyName;
4053
- const profileId = payload.signal?.policyProfileId;
4054
- const mlAdapter = getStrategyMlAdapter(strategyName, profileId);
4055
- const normalizedSignal = mlAdapter.normalizeSignal?.(payload.signal) ?? payload.signal;
4056
- const nextSignal = {
4057
- ...normalizedSignal,
4058
- indicators: {
4059
- ...normalizedSignal?.indicators ?? {}
4060
- }
4061
- };
4062
- const nextContext = payload.context ? {
4063
- ...payload.context,
4064
- strategyConfig: normalizeStrategyConfig(
4065
- payload.context.strategyConfig,
4066
- strategyName,
4067
- profileId
4068
- )
4069
- } : void 0;
4070
- return {
4071
- signal: nextSignal,
4072
- context: nextContext
4073
- };
4074
- };
4075
-
4076
- // src/runtimeJournal.ts
4077
- var import_node_crypto = require("crypto");
4078
- var import_constants3 = require("@tradejs/core/constants");
4079
- var import_time3 = require("@tradejs/core/time");
4080
- var import_trade = require("@tradejs/core/trade");
4081
- var import_logger6 = require("@tradejs/infra/logger");
4082
- var import_redis3 = require("@tradejs/infra/redis");
4083
- var now = () => Date.now();
4084
- var toRandomOrderSuffix = () => (0, import_node_crypto.randomUUID)().replace(/-/g, "").slice(0, 12).toLowerCase();
4085
- var calculateClosedPnl = ({
4086
- direction,
4087
- entryPrice,
4088
- exitPrice,
4089
- qty
4090
- }) => {
4091
- const pnl = direction === "LONG" ? (exitPrice - entryPrice) * qty : (entryPrice - exitPrice) * qty;
4092
- return Number.isFinite(pnl) ? pnl : null;
4093
- };
4094
- var createRuntimeOrderId = (strategy) => {
4095
- const prefix = (0, import_trade.createRuntimeOrderLinkPrefix)(strategy);
4096
- if (prefix === "tjs-") {
4097
- return `tjs-${(0, import_node_crypto.randomUUID)().replace(/-/g, "").slice(0, 24).toLowerCase()}`;
4098
- }
4099
- return `${prefix}${toRandomOrderSuffix()}`;
4100
- };
4101
- var recordRuntimeTradeOpen = async (params) => {
4102
- const { userName } = params;
4103
- if (!userName) {
4104
- return null;
4105
- }
4106
- const record = {
4107
- ...params,
4108
- entryCount: 1,
4109
- lastEntryPrice: params.entryPrice,
4110
- lastEntryQty: params.qty,
4111
- lastEntryTimestamp: params.entryTimestamp,
4112
- status: "active",
4113
- currentPrice: params.entryPrice,
4114
- currentPnl: 0,
4115
- closedPnl: null,
4116
- exitPrice: null,
4117
- exitTimestamp: null,
4118
- lastSyncedAt: now()
4119
- };
4120
- const dayKey = (0, import_time3.getRuntimeStorageDayKey)(record.entryTimestamp);
4121
- const runtimeScopeId = record.deploymentId ?? record.accountId;
4122
- try {
4123
- await Promise.all([
4124
- (0, import_redis3.setData)(import_redis3.redisKeys.runtimeTrade(userName, record.orderId), record, {
4125
- expire: 0
4126
- }),
4127
- (0, import_redis3.setHashJsonField)(
4128
- import_redis3.redisKeys.runtimeTradeBucket(userName, dayKey),
4129
- record.orderId,
4130
- record,
4131
- { expire: 0 }
4132
- ),
4133
- (0, import_redis3.setData)(
4134
- import_redis3.redisKeys.runtimeActiveTrade(userName, record.symbol, runtimeScopeId),
4135
- { orderId: record.orderId },
4136
- { expire: 0 }
4137
- )
4138
- ]);
4139
- } catch (error) {
4140
- import_logger6.logger.error(
4141
- "runtime trade open journal failed: %s %s",
4142
- record.symbol,
4143
- error?.message || String(error)
4144
- );
4145
- }
4146
- return record;
4147
- };
4148
- var recordRuntimeTradeIncrease = async (params) => {
4149
- const {
4150
- userName,
4151
- strategy,
4152
- symbol,
4153
- direction,
4154
- resultingQty,
4155
- resultingEntryPrice,
4156
- addedQty,
4157
- addedEntryPrice,
4158
- entryTimestamp,
4159
- fee,
4160
- accountId,
4161
- deploymentId
4162
- } = params;
4163
- if (!userName) {
4164
- return null;
4165
- }
4166
- const existing = await getActiveRuntimeTrade({
4167
- userName,
4168
- symbol,
4169
- accountId,
4170
- deploymentId
4755
+ model: options.model,
4756
+ messages: [
4757
+ { role: "system", content: systemPrompt },
4758
+ {
4759
+ role: "system",
4760
+ content: `Write all user-visible text fields in ${responseLanguage}. Keep field names and JSON syntax unchanged.`
4761
+ },
4762
+ { role: "user", content: humanPrompt }
4763
+ ]
4171
4764
  });
4172
- if (!existing || existing.strategy !== strategy || existing.direction !== direction) {
4173
- return null;
4174
- }
4175
- const addedFee = typeof fee === "number" && Number.isFinite(fee) ? fee : 0;
4176
- const openFee = (existing.openFee ?? existing.fee ?? 0) + addedFee;
4177
- const totalFee = (existing.totalFee ?? existing.fee ?? 0) + addedFee;
4178
- const next = {
4179
- ...existing,
4180
- ...params.orderLeverage ? { orderLeverage: params.orderLeverage } : {},
4181
- qty: resultingQty,
4182
- entryPrice: resultingEntryPrice,
4183
- entryCount: Math.max(1, existing.entryCount ?? 1) + 1,
4184
- lastEntryPrice: addedEntryPrice,
4185
- lastEntryQty: addedQty,
4186
- lastEntryTimestamp: entryTimestamp,
4187
- currentPrice: resultingEntryPrice,
4188
- currentPnl: 0,
4189
- fee: openFee,
4190
- openFee,
4191
- totalFee,
4192
- lastSyncedAt: now()
4193
- };
4194
- const dayKey = (0, import_time3.getRuntimeStorageDayKey)(existing.entryTimestamp);
4195
- try {
4196
- await Promise.all([
4197
- (0, import_redis3.setData)(import_redis3.redisKeys.runtimeTrade(userName, existing.orderId), next, {
4198
- expire: 0
4199
- }),
4200
- (0, import_redis3.setHashJsonField)(
4201
- import_redis3.redisKeys.runtimeTradeBucket(userName, dayKey),
4202
- existing.orderId,
4203
- next,
4204
- { expire: 0 }
4205
- )
4206
- ]);
4207
- } catch (error) {
4208
- import_logger6.logger.error(
4209
- "runtime trade increase journal failed: %s %s",
4210
- symbol,
4211
- error?.message || String(error)
4212
- );
4765
+ const parsed = parseAIResponse(response.content);
4766
+ const normalized = normalizeAnalysis(parsed);
4767
+ if (!options.signal) {
4768
+ return normalized;
4213
4769
  }
4214
- return next;
4770
+ return postProcessAiAnalysisByStrategy(
4771
+ options.signal,
4772
+ normalized,
4773
+ options.payload
4774
+ );
4215
4775
  };
4216
- var getActiveRuntimeTrade = async (params) => {
4217
- const { userName, symbol, accountId, deploymentId } = params;
4218
- if (!userName) {
4219
- return null;
4220
- }
4221
- const activeRef = await (0, import_redis3.getData)(
4222
- import_redis3.redisKeys.runtimeActiveTrade(userName, symbol, deploymentId ?? accountId),
4223
- null
4776
+ var runAiPromptLocal = async (signal, options = {}) => {
4777
+ const payload = options.payload ?? buildAiPayload(signal);
4778
+ const gateContext = getDeterministicAiGateContext(payload);
4779
+ const signalDirection = getSignalDirection(signal);
4780
+ const deterministicQuality = getDeterministicQuality(gateContext);
4781
+ const approvalAllowedNow = typeof gateContext?.approvalAllowedNow === "boolean" ? gateContext.approvalAllowedNow : deterministicQuality >= 4;
4782
+ return postProcessLocalAiAnalysisByStrategy(
4783
+ signal,
4784
+ {
4785
+ direction: approvalAllowedNow ? signalDirection : null,
4786
+ quality: deterministicQuality,
4787
+ needRetest: !approvalAllowedNow,
4788
+ retestPrice: null,
4789
+ takeProfitPrice: approvalAllowedNow ? signal.prices?.takeProfitPrice ?? null : null,
4790
+ stopLossPrice: approvalAllowedNow ? signal.prices?.stopLossPrice ?? null : null
4791
+ },
4792
+ payload
4224
4793
  );
4225
- const orderId = String(activeRef?.orderId || "").trim();
4226
- if (!orderId) {
4227
- return null;
4228
- }
4229
- const existing = await (0, import_redis3.getData)(
4230
- import_redis3.redisKeys.runtimeTrade(userName, orderId),
4231
- null
4794
+ };
4795
+ var askAI = async (signal, options = {}) => {
4796
+ const { symbol } = signal;
4797
+ const payload = buildAiPayload(signal);
4798
+ const content = await runAiPrompt(
4799
+ {
4800
+ systemPrompt: buildAiSystemPrompt(signal),
4801
+ humanPrompt: buildAiHumanPrompt(signal, payload)
4802
+ },
4803
+ {
4804
+ ...options,
4805
+ signal,
4806
+ payload
4807
+ }
4232
4808
  );
4233
- if (!existing || existing.status !== "active") {
4234
- await (0, import_redis3.delKey)(
4235
- import_redis3.redisKeys.runtimeActiveTrade(userName, symbol, deploymentId ?? accountId)
4236
- );
4237
- return null;
4238
- }
4239
- return existing;
4809
+ await (0, import_redis4.setData)(import_redis4.redisKeys.analysis(symbol, signal.signalId), content);
4810
+ return content;
4240
4811
  };
4241
- var markRuntimeTradeClosed = async (params) => {
4242
- const {
4243
- userName,
4244
- symbol,
4245
- strategy,
4246
- exitPrice,
4247
- exitTimestamp,
4248
- closedPnl,
4249
- exitType,
4250
- accountId,
4251
- deploymentId
4252
- } = params;
4253
- if (!userName) {
4254
- return null;
4255
- }
4256
- const existing = await getActiveRuntimeTrade({
4257
- userName,
4258
- symbol,
4259
- accountId,
4260
- deploymentId
4261
- });
4262
- if (!existing) {
4263
- return null;
4264
- }
4265
- const orderId = existing.orderId;
4266
- if (strategy && existing.strategy !== strategy) {
4267
- return null;
4268
- }
4269
- const resolvedExitPrice = typeof exitPrice === "number" && Number.isFinite(exitPrice) ? exitPrice : existing.currentPrice ?? existing.entryPrice;
4270
- const resolvedClosedPnl = typeof closedPnl === "number" && Number.isFinite(closedPnl) ? closedPnl : typeof resolvedExitPrice === "number" && Number.isFinite(resolvedExitPrice) ? calculateClosedPnl({
4271
- direction: existing.direction,
4272
- entryPrice: existing.entryPrice,
4273
- exitPrice: resolvedExitPrice,
4274
- qty: existing.qty
4275
- }) : existing.closedPnl ?? existing.currentPnl ?? null;
4276
- const next = {
4277
- ...existing,
4278
- status: "closed",
4279
- currentPrice: resolvedExitPrice,
4280
- currentPnl: resolvedClosedPnl,
4281
- closedPnl: resolvedClosedPnl,
4282
- exitPrice: resolvedExitPrice,
4283
- exitTimestamp: typeof exitTimestamp === "number" && Number.isFinite(exitTimestamp) ? exitTimestamp : now(),
4284
- exitType: exitType ?? existing.exitType ?? null,
4285
- lastSyncedAt: now()
4812
+
4813
+ // src/strategyAdapters/ml.ts
4814
+ var defaultMlAdapter = {
4815
+ normalizeStrategyConfig: (strategyConfig) => strategyConfig
4816
+ };
4817
+ var getStrategyMlAdapter = (strategy, profileId) => {
4818
+ const strategyAdapter = getStrategyProfileMlAdapter(
4819
+ getStrategyManifest(strategy),
4820
+ profileId
4821
+ );
4822
+ if (!strategyAdapter) return defaultMlAdapter;
4823
+ return {
4824
+ ...defaultMlAdapter,
4825
+ ...strategyAdapter
4826
+ };
4827
+ };
4828
+
4829
+ // src/mlPayload.ts
4830
+ var normalizeStrategyConfig = (strategyConfig, strategyName, profileId) => {
4831
+ return getStrategyMlAdapter(
4832
+ strategyName,
4833
+ profileId
4834
+ ).normalizeStrategyConfig?.(strategyConfig);
4835
+ };
4836
+ var buildMlPayload = (payload) => {
4837
+ const strategyName = payload.signal?.strategy ?? payload.context?.strategyName;
4838
+ const profileId = payload.signal?.policyProfileId;
4839
+ const mlAdapter = getStrategyMlAdapter(strategyName, profileId);
4840
+ const normalizedSignal = mlAdapter.normalizeSignal?.(payload.signal) ?? payload.signal;
4841
+ const nextSignal = {
4842
+ ...normalizedSignal,
4843
+ indicators: {
4844
+ ...normalizedSignal?.indicators ?? {}
4845
+ }
4846
+ };
4847
+ const nextContext = payload.context ? {
4848
+ ...payload.context,
4849
+ strategyConfig: normalizeStrategyConfig(
4850
+ payload.context.strategyConfig,
4851
+ strategyName,
4852
+ profileId
4853
+ )
4854
+ } : void 0;
4855
+ return {
4856
+ signal: nextSignal,
4857
+ context: nextContext
4286
4858
  };
4287
- const dayKey = (0, import_time3.getRuntimeStorageDayKey)(existing.entryTimestamp);
4288
- const closeDayKey = (0, import_time3.getRuntimeStorageDayKey)(next.exitTimestamp);
4289
- try {
4290
- await Promise.all([
4291
- (0, import_redis3.setData)(import_redis3.redisKeys.runtimeTrade(userName, orderId), next, {
4292
- expire: import_constants3.TTL_1M
4293
- }),
4294
- (0, import_redis3.setHashJsonField)(
4295
- import_redis3.redisKeys.runtimeTradeBucket(userName, dayKey),
4296
- orderId,
4297
- next,
4298
- { expire: import_constants3.TTL_1M }
4299
- ),
4300
- (0, import_redis3.setHashJsonField)(
4301
- import_redis3.redisKeys.runtimeClosedTradeBucket(userName, closeDayKey),
4302
- orderId,
4303
- next,
4304
- { expire: import_constants3.TTL_1M }
4305
- ),
4306
- (0, import_redis3.delKey)(
4307
- import_redis3.redisKeys.runtimeActiveTrade(
4308
- userName,
4309
- symbol,
4310
- deploymentId ?? accountId
4311
- )
4312
- )
4313
- ]);
4314
- } catch (error) {
4315
- import_logger6.logger.error(
4316
- "runtime trade close journal failed: %s %s",
4317
- symbol,
4318
- error?.message || String(error)
4319
- );
4320
- }
4321
- return next;
4322
4859
  };
4323
4860
 
4324
4861
  // src/strategyHelpers/marketContextStages.ts
@@ -4330,7 +4867,7 @@ var import_logger7 = require("@tradejs/infra/logger");
4330
4867
  var import_strategies = require("@tradejs/core/strategies");
4331
4868
 
4332
4869
  // src/binanceBreadthUniverses.ts
4333
- var import_node_crypto2 = require("crypto");
4870
+ var import_node_crypto3 = require("crypto");
4334
4871
 
4335
4872
  // src/config/binanceBreadthUniverses.json
4336
4873
  var binanceBreadthUniverses_default = {
@@ -4569,7 +5106,7 @@ var BINANCE_BREADTH_UNIVERSE_KEYS = [
4569
5106
  "top50",
4570
5107
  "top100"
4571
5108
  ];
4572
- var fingerprint = (value, length) => (0, import_node_crypto2.createHash)("sha256").update(JSON.stringify(value)).digest("hex").slice(0, length);
5109
+ var fingerprint = (value, length) => (0, import_node_crypto3.createHash)("sha256").update(JSON.stringify(value)).digest("hex").slice(0, length);
4573
5110
  var normalizeSymbols = (symbols) => symbols.map((symbol) => symbol.trim().toUpperCase()).filter(Boolean);
4574
5111
  var buildBinanceBreadthUniverseSnapshot = ({
4575
5112
  rankedSymbols,
@@ -5593,7 +6130,7 @@ var enrichSignalWithCoinMarketCapContext = async (params) => {
5593
6130
  };
5594
6131
 
5595
6132
  // src/strategyHelpers/derivativesContext.ts
5596
- var import_indicators2 = require("@tradejs/core/indicators");
6133
+ var import_indicators3 = require("@tradejs/core/indicators");
5597
6134
  var import_data3 = require("@tradejs/core/data");
5598
6135
  var import_strategies3 = require("@tradejs/core/strategies");
5599
6136
  var import_constants4 = require("@tradejs/core/constants");
@@ -5627,7 +6164,7 @@ var parseLookbackMs = () => {
5627
6164
  };
5628
6165
  var withHourlyFallbackRows = (rowsByInterval) => ({
5629
6166
  "15m": rowsByInterval["15m"] ?? [],
5630
- "1h": (0, import_indicators2.buildCoinalyzeHourlyRowsWithFallback)({
6167
+ "1h": (0, import_indicators3.buildCoinalyzeHourlyRowsWithFallback)({
5631
6168
  rows15m: rowsByInterval["15m"],
5632
6169
  fallbackRows1h: rowsByInterval["1h"]
5633
6170
  })
@@ -5755,7 +6292,7 @@ var enrichSignalWithDerivativesContext = async (params) => {
5755
6292
  const targetSymbol = normalizeSymbol(signal.symbol);
5756
6293
  const lookbackMs = parseLookbackMs();
5757
6294
  const decisionTimeMs = signal.timestamp + (0, import_data3.intervalToMs)(signal.interval);
5758
- const derivativesEndMs = (0, import_indicators2.getLastClosedDerivativesBarStartMs)(
6295
+ const derivativesEndMs = (0, import_indicators3.getLastClosedDerivativesBarStartMs)(
5759
6296
  decisionTimeMs,
5760
6297
  "15m"
5761
6298
  );
@@ -5770,7 +6307,7 @@ var enrichSignalWithDerivativesContext = async (params) => {
5770
6307
  });
5771
6308
  return [
5772
6309
  symbol,
5773
- (0, import_indicators2.buildDerivativesContext)({
6310
+ (0, import_indicators3.buildDerivativesContext)({
5774
6311
  symbol,
5775
6312
  direction: signal.direction,
5776
6313
  timestamp: derivativesEndMs,
@@ -5797,7 +6334,7 @@ var enrichSignalWithDerivativesContext = async (params) => {
5797
6334
  lookbackMs,
5798
6335
  ...params.abortSignal ? { signal: params.abortSignal } : {}
5799
6336
  });
5800
- const context = (0, import_indicators2.buildDerivativesContext)({
6337
+ const context = (0, import_indicators3.buildDerivativesContext)({
5801
6338
  symbol: targetSymbol,
5802
6339
  direction: signal.direction,
5803
6340
  timestamp: derivativesEndMs,
@@ -5845,7 +6382,7 @@ var import_hyperliquidWhales2 = require("@tradejs/infra/timescale/hyperliquidWha
5845
6382
  var import_logger10 = require("@tradejs/infra/logger");
5846
6383
 
5847
6384
  // src/hyperliquidWhaleUniverse.ts
5848
- var import_node_crypto3 = require("crypto");
6385
+ var import_node_crypto4 = require("crypto");
5849
6386
 
5850
6387
  // src/config/hyperliquidPerpUniverse.json
5851
6388
  var hyperliquidPerpUniverse_default = {
@@ -6017,7 +6554,7 @@ var hyperliquidWhales_default = {
6017
6554
  };
6018
6555
 
6019
6556
  // src/hyperliquidWhaleUniverse.ts
6020
- var fingerprint2 = (value) => (0, import_node_crypto3.createHash)("sha256").update(JSON.stringify(value)).digest("hex").slice(0, 16);
6557
+ var fingerprint2 = (value) => (0, import_node_crypto4.createHash)("sha256").update(JSON.stringify(value)).digest("hex").slice(0, 16);
6021
6558
  var validateUnique = (values, label) => {
6022
6559
  if (new Set(values).size !== values.length) {
6023
6560
  throw new Error(`Hyperliquid ${label} snapshot contains duplicates`);
@@ -6504,6 +7041,7 @@ var enrichSignalWithMarketContextStages = async ({
6504
7041
  })
6505
7042
  });
6506
7043
  }
7044
+ await enrichSignalWithTickerProfile(signal);
6507
7045
  const results = [];
6508
7046
  for (const stage of stages) {
6509
7047
  if (abortSignal?.aborted) break;
@@ -6551,12 +7089,12 @@ var findReplayAiAnalysis = ({
6551
7089
  direction,
6552
7090
  ai
6553
7091
  }) => {
6554
- const snapshots = ai?.replayAnalyses;
6555
- if (!Array.isArray(snapshots) || !snapshots.length) {
7092
+ const snapshots2 = ai?.replayAnalyses;
7093
+ if (!Array.isArray(snapshots2) || !snapshots2.length) {
6556
7094
  return void 0;
6557
7095
  }
6558
7096
  let best = null;
6559
- for (const snapshot2 of snapshots) {
7097
+ for (const snapshot2 of snapshots2) {
6560
7098
  if (snapshot2.symbol !== signal.symbol || snapshot2.direction !== direction || snapshot2.strategy && snapshot2.strategy !== signal.strategy) {
6561
7099
  continue;
6562
7100
  }
@@ -6716,7 +7254,9 @@ var executeEntryOrder = async ({
6716
7254
  beforePlaceOrder,
6717
7255
  recordRuntimeTrade = true,
6718
7256
  leverage,
6719
- executionLimits
7257
+ executionLimits,
7258
+ markPortfolioSubmitted,
7259
+ rejectPortfolioSubmission
6720
7260
  }) => {
6721
7261
  await beforePlaceOrder?.();
6722
7262
  const previousPosition = positionIntent === "increase" ? await connector.getPosition(symbol) : null;
@@ -6789,7 +7329,13 @@ var executeEntryOrder = async ({
6789
7329
  ...typeof leverage === "number" && Number.isFinite(leverage) ? { leverage } : {},
6790
7330
  orderId,
6791
7331
  signal,
6792
- ...beforeSubmit ? { beforeSubmit } : {}
7332
+ ...rejectPortfolioSubmission ? { onRejected: rejectPortfolioSubmission } : {},
7333
+ ...beforeSubmit || markPortfolioSubmitted ? {
7334
+ beforeSubmit: async (context) => {
7335
+ await beforeSubmit?.(context);
7336
+ await markPortfolioSubmitted?.(context.qty);
7337
+ }
7338
+ } : {}
6793
7339
  });
6794
7340
  const orderAckTime = Date.now();
6795
7341
  const placedQty = typeof signal.orderQty === "number" && Number.isFinite(signal.orderQty) && signal.orderQty > 0 ? signal.orderQty : qty;
@@ -7777,7 +8323,7 @@ var handleProtectDecision = async ({
7777
8323
  }
7778
8324
  return decision.code;
7779
8325
  };
7780
- var executeEntryDecision = async ({
8326
+ var executeAdmittedEntryDecision = async ({
7781
8327
  connector,
7782
8328
  symbol,
7783
8329
  decision,
@@ -7791,7 +8337,9 @@ var executeEntryDecision = async ({
7791
8337
  ai,
7792
8338
  recordRuntimeJournal,
7793
8339
  invokeStageHooks,
7794
- notifyRuntimeError
8340
+ notifyRuntimeError,
8341
+ markPortfolioSubmitted,
8342
+ rejectPortfolioSubmission
7795
8343
  }) => {
7796
8344
  const signal = decision.signal;
7797
8345
  const beforePlaceOrder = async () => {
@@ -7838,6 +8386,8 @@ var executeEntryDecision = async ({
7838
8386
  ...Number.isFinite(Number(hookCtx.strategyConfig.LEVERAGE)) ? { leverage: Number(hookCtx.strategyConfig.LEVERAGE) } : {},
7839
8387
  signal,
7840
8388
  beforePlaceOrder,
8389
+ markPortfolioSubmitted,
8390
+ rejectPortfolioSubmission,
7841
8391
  recordRuntimeTrade: recordRuntimeJournal,
7842
8392
  executionLimits: resolveEntryExecutionLimits(
7843
8393
  hookCtx.strategyConfig,
@@ -7978,6 +8528,37 @@ var executeEntryDecision = async ({
7978
8528
  }
7979
8529
  return signal ?? decision.code;
7980
8530
  };
8531
+ async function executeEntryDecision(params) {
8532
+ if (!params.portfolio) return executeAdmittedEntryDecision(params);
8533
+ const { decision, portfolio } = params;
8534
+ const signal = decision.signal;
8535
+ if (!signal || decision.orderPlan.positionIntent === "increase")
8536
+ return "PORTFOLIO_REQUIRES_NEW_SIGNAL_POSITION";
8537
+ signal.orderId ??= createRuntimeOrderId(signal.strategy);
8538
+ const result = await portfolio.submit(
8539
+ params.portfolioTime ?? decision.entryContext.timestamp,
8540
+ {
8541
+ id: signal.orderId,
8542
+ signalId: signal.signalId,
8543
+ strategy: signal.strategy,
8544
+ symbol: params.symbol,
8545
+ direction: decision.entryContext.direction,
8546
+ qty: decision.orderPlan.qty,
8547
+ timestamp: decision.entryContext.timestamp
8548
+ },
8549
+ (markPortfolioSubmitted, rejectPortfolioSubmission) => executeAdmittedEntryDecision({
8550
+ ...params,
8551
+ markPortfolioSubmitted,
8552
+ rejectPortfolioSubmission
8553
+ })
8554
+ );
8555
+ if (result.reason) {
8556
+ signal.orderStatus = "failed";
8557
+ signal.orderFailureReason = result.reason;
8558
+ return signal;
8559
+ }
8560
+ return result.value;
8561
+ }
7981
8562
 
7982
8563
  // src/strategyRuntime.ts
7983
8564
  var cloneWithPropertyDescriptors = (value) => Object.create(
@@ -8005,6 +8586,7 @@ var createStrategyRuntime = ({
8005
8586
  };
8006
8587
  const creator = async ({
8007
8588
  userName,
8589
+ connectorName,
8008
8590
  config: baseConfig,
8009
8591
  symbol,
8010
8592
  universe: requestedUniverse,
@@ -8043,6 +8625,17 @@ var createStrategyRuntime = ({
8043
8625
  const projectConfig = await loadTradejsConfig(projectRoot);
8044
8626
  const projectHooks = projectConfig.hooks;
8045
8627
  const env = String(config.ENV ?? "BACKTEST");
8628
+ const portfolio = getPortfolioRuntime({
8629
+ connectorName,
8630
+ value: baseConfig?.PORTFOLIO_POLICY ?? config.PORTFOLIO_POLICY,
8631
+ strategy: strategyName,
8632
+ connector,
8633
+ env,
8634
+ userName,
8635
+ accountId,
8636
+ deploymentId
8637
+ });
8638
+ const portfolioContext = portfolio ? (now2) => isRuntimeOrderExecutionEnabled({ config, env, connector }) ? portfolio.tick(now2) : portfolio.context(now2) : void 0;
8046
8639
  const researchPolicyValue = baseConfig?.RESEARCH_ENTRY_POLICY ?? config.RESEARCH_ENTRY_POLICY;
8047
8640
  const researchEntryPolicy = researchPolicyValue == null ? void 0 : parseResearchEntryPolicy(researchPolicyValue);
8048
8641
  if (researchEntryPolicy && (env !== "BACKTEST" || Reflect.get(connector, "__tradejsTestConnector") !== true))
@@ -8341,6 +8934,20 @@ var createStrategyRuntime = ({
8341
8934
  interval: decisionInterval
8342
8935
  }) => {
8343
8936
  if (!baseContext) return void 0;
8937
+ const profileSource = strategyManifest?.contextRequirements?.tickerProfiles;
8938
+ if (profileSource) {
8939
+ const profile = await loadTickerProfileContext({
8940
+ ...profileSource,
8941
+ projectRoot,
8942
+ symbol: decisionSymbol,
8943
+ timestamp: candle.timestamp
8944
+ });
8945
+ if (!profile)
8946
+ throw new Error(
8947
+ "Required ticker profile is unavailable at decision time"
8948
+ );
8949
+ baseContext = withTickerProfile(baseContext, profile);
8950
+ }
8344
8951
  if (!coreContextRequirements.has("hyperliquidWhales")) {
8345
8952
  return baseContext;
8346
8953
  }
@@ -8508,6 +9115,8 @@ var createStrategyRuntime = ({
8508
9115
  runtime: pending2.runtime
8509
9116
  });
8510
9117
  return executeEntryDecision({
9118
+ portfolio,
9119
+ portfolioTime: env === "BACKTEST" || env === "PARITY" ? candle.timestamp : Date.now(),
8511
9120
  connector,
8512
9121
  symbol,
8513
9122
  decision: pending2.decision,
@@ -8525,6 +9134,10 @@ var createStrategyRuntime = ({
8525
9134
  });
8526
9135
  };
8527
9136
  const runWithDecisionOverride = async (candle, btcCandle, options = {}) => {
9137
+ if (portfolioContext)
9138
+ await portfolioContext(
9139
+ env === "BACKTEST" || env === "PARITY" ? candle.timestamp : Date.now()
9140
+ );
8528
9141
  appendCurrentMarketData(candle, btcCandle, options.ethCandle);
8529
9142
  const ethCandle = resolveEthCandle(candle, options.ethCandle);
8530
9143
  indicatorsState.setCurrentBar(candle, btcCandle, ethCandle);
@@ -8695,6 +9308,13 @@ var createStrategyRuntime = ({
8695
9308
  });
8696
9309
  const signal = decision.signal;
8697
9310
  if (signal) {
9311
+ if (portfolio)
9312
+ signal.additionalIndicators = {
9313
+ ...signal.additionalIndicators,
9314
+ portfolioContext: await portfolioContext(
9315
+ env === "BACKTEST" || env === "PARITY" ? decision.entryContext.timestamp : Date.now()
9316
+ )
9317
+ };
8698
9318
  if (runtimeLineage) signal.runtimeLineage = runtimeLineage;
8699
9319
  if (universe) signal.universe = universe;
8700
9320
  if (assetClass) signal.assetClass = assetClass;
@@ -8934,6 +9554,8 @@ var createStrategyRuntime = ({
8934
9554
  });
8935
9555
  }
8936
9556
  return executeEntryDecision({
9557
+ portfolio,
9558
+ portfolioTime: env === "BACKTEST" || env === "PARITY" ? candle.timestamp : Date.now(),
8937
9559
  connector,
8938
9560
  symbol,
8939
9561
  decision,
@@ -9045,7 +9667,7 @@ var normalizeForCanonicalJson = (value) => {
9045
9667
  }
9046
9668
  return value;
9047
9669
  };
9048
- var revision = (prefix, value) => `${prefix}:${(0, import_node_crypto4.createHash)("sha256").update(JSON.stringify(normalizeForCanonicalJson(value))).digest("hex").slice(0, 16)}`;
9670
+ var revision = (prefix, value) => `${prefix}:${(0, import_node_crypto5.createHash)("sha256").update(JSON.stringify(normalizeForCanonicalJson(value))).digest("hex").slice(0, 16)}`;
9049
9671
  var computeStrategyRevision = ({
9050
9672
  strategyName,
9051
9673
  strategyPackage,
@@ -9630,18 +10252,18 @@ var loadRuntimeTrades = async (userName, {
9630
10252
  const [bucketTradeGroups, legacyKeys] = await Promise.all([
9631
10253
  Promise.all(
9632
10254
  dayKeys.map(
9633
- (dayKey) => (0, import_redis4.getHashJsonValues)(
9634
- import_redis4.redisKeys.runtimeTradeBucket(userName, dayKey)
10255
+ (dayKey) => (0, import_redis5.getHashJsonValues)(
10256
+ import_redis5.redisKeys.runtimeTradeBucket(userName, dayKey)
9635
10257
  )
9636
10258
  )
9637
10259
  ),
9638
- (0, import_redis4.getKeys)(import_redis4.redisKeys.runtimeTrades(userName))
10260
+ (0, import_redis5.getKeys)(import_redis5.redisKeys.runtimeTrades(userName))
9639
10261
  ]);
9640
10262
  const legacyTradeKeys = legacyKeys.filter(
9641
- (key) => !key.startsWith(import_redis4.redisKeys.runtimeTradeBuckets(userName))
10263
+ (key) => !key.startsWith(import_redis5.redisKeys.runtimeTradeBuckets(userName))
9642
10264
  );
9643
10265
  const legacyTrades = await Promise.all(
9644
- legacyTradeKeys.map((key) => (0, import_redis4.getData)(key, null))
10266
+ legacyTradeKeys.map((key) => (0, import_redis5.getData)(key, null))
9645
10267
  );
9646
10268
  const dedupedTrades = /* @__PURE__ */ new Map();
9647
10269
  for (const trade of [...legacyTrades, ...bucketTradeGroups.flat()]) {
@@ -9728,8 +10350,8 @@ var loadExchangeRange = async ({
9728
10350
  }
9729
10351
  };
9730
10352
  var loadActiveRuntimeOrderIds = async (userName) => {
9731
- const keys = await (0, import_redis4.getKeys)(import_redis4.redisKeys.runtimeActiveTrades(userName));
9732
- const refs = await Promise.all(keys.map((key) => (0, import_redis4.getData)(key, null)));
10353
+ const keys = await (0, import_redis5.getKeys)(import_redis5.redisKeys.runtimeActiveTrades(userName));
10354
+ const refs = await Promise.all(keys.map((key) => (0, import_redis5.getData)(key, null)));
9733
10355
  return new Set(
9734
10356
  refs.map(
9735
10357
  (ref) => typeof ref?.orderId === "string" && ref.orderId.trim() ? ref.orderId.trim() : null