@tradejs/node 3.1.29-beta.289 → 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,1379 +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
- if (limits.maxLossValue != null && plannedLossValue > limits.maxLossValue * (1 + 1e-9))
2413
- throw new EntrySubmissionValidationError(
2414
- "ENTRY_RISK_EXCEEDS_MAX_LOSS_AT_SUBMISSION"
2415
- );
2416
- return { netRiskRatio, plannedLossValue };
2293
+ const pnl = direction === "LONG" ? (exitPrice - entryPrice) * qty : (entryPrice - exitPrice) * qty;
2294
+ return Number.isFinite(pnl) ? pnl : null;
2417
2295
  };
2418
-
2419
- // src/strategy/entrySubmissionValidation.ts
2420
- var MAX_ENTRY_QUOTE_AGE_MS = 5e3;
2421
- var toFiniteNumberOrNull = (value) => typeof value === "number" && Number.isFinite(value) ? value : null;
2422
- var getOrderArrivalSnapshot = async ({
2423
- connector,
2424
- symbol,
2425
- requireTimestamp = false
2426
- }) => {
2427
- if (typeof connector.getTopOfBookTicker !== "function") {
2428
- return {
2429
- arrivalSnapshotTime: Date.now(),
2430
- arrivalSource: "unavailable",
2431
- bid: null,
2432
- ask: null,
2433
- arrivalMid: null,
2434
- spreadBps: null
2435
- };
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;
2436
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;
2437
2324
  try {
2438
- const ticker = await connector.getTopOfBookTicker(symbol);
2439
- const arrivalSnapshotTime = toFiniteNumberOrNull(ticker?.timestamp);
2440
- const timestampUnavailable = requireTimestamp && arrivalSnapshotTime == null;
2441
- const bid = timestampUnavailable ? null : toFiniteNumberOrNull(ticker?.bidPrice);
2442
- const ask = timestampUnavailable ? null : toFiniteNumberOrNull(ticker?.askPrice);
2443
- const arrivalMid = bid != null && ask != null ? (bid + ask) / 2 : null;
2444
- const spreadBps = bid != null && ask != null && arrivalMid != null && arrivalMid > 0 ? (ask - bid) / arrivalMid * 1e4 : null;
2445
- return {
2446
- arrivalSnapshotTime: arrivalSnapshotTime ?? Date.now(),
2447
- arrivalSource: "top_of_book",
2448
- bid,
2449
- ask,
2450
- arrivalMid,
2451
- spreadBps
2452
- };
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
+ ]);
2453
2341
  } catch (error) {
2454
- import_logger5.logger.warn(
2455
- "runtime order arrival snapshot failed: %s %s",
2456
- symbol,
2342
+ import_logger5.logger.error(
2343
+ "runtime trade open journal failed: %s %s",
2344
+ record.symbol,
2457
2345
  error?.message || String(error)
2458
2346
  );
2459
- return {
2460
- arrivalSnapshotTime: Date.now(),
2461
- arrivalSource: "top_of_book_error",
2462
- bid: null,
2463
- ask: null,
2464
- arrivalMid: null,
2465
- spreadBps: null
2466
- };
2467
2347
  }
2348
+ return record;
2468
2349
  };
2469
- var validateEntryProtectionAtArrival = ({
2470
- direction,
2471
- signalPrice,
2472
- bid,
2473
- ask,
2474
- arrivalMid,
2475
- takeProfits,
2476
- stopLossPrice
2477
- }) => {
2478
- const entryReference = direction === "LONG" ? ask ?? arrivalMid ?? signalPrice : bid ?? arrivalMid ?? signalPrice;
2479
- const stopReference = direction === "LONG" ? bid ?? arrivalMid ?? signalPrice : ask ?? arrivalMid ?? signalPrice;
2480
- if (!Number.isFinite(entryReference) || entryReference <= 0 || !Number.isFinite(stopReference) || stopReference <= 0) {
2481
- throw new Error("INVALID_ENTRY_REFERENCE_AT_ARRIVAL");
2482
- }
2483
- for (const takeProfit of takeProfits) {
2484
- if (!Number.isFinite(takeProfit.price) || takeProfit.price <= 0 || (direction === "LONG" ? takeProfit.price <= entryReference : takeProfit.price >= entryReference)) {
2485
- throw new Error("TAKE_PROFIT_CROSSED_BEFORE_ENTRY");
2486
- }
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;
2487
2367
  }
2488
- if (stopLossPrice != null && (!Number.isFinite(stopLossPrice) || stopLossPrice <= 0 || (direction === "LONG" ? stopLossPrice >= stopReference : stopLossPrice <= stopReference))) {
2489
- 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;
2490
2376
  }
2491
- };
2492
- var createEntrySubmissionValidation = ({
2493
- connector,
2494
- symbol,
2495
- direction,
2496
- takeProfits,
2497
- stopLossPrice,
2498
- limits,
2499
- onValidated
2500
- }) => {
2501
- if (!limits) return void 0;
2502
- if (!connector.capabilities?.orderSubmissionValidation)
2503
- throw new EntrySubmissionValidationError(
2504
- "ENTRY_SUBMISSION_VALIDATION_UNSUPPORTED"
2505
- );
2506
- return async ({ qty, executionPrice }) => {
2507
- const simulated = Reflect.get(connector, "__tradejsTestConnector") === true;
2508
- const snapshot2 = simulated && executionPrice != null ? {
2509
- arrivalSnapshotTime: Date.now(),
2510
- arrivalSource: "simulation",
2511
- bid: executionPrice,
2512
- ask: executionPrice,
2513
- arrivalMid: executionPrice,
2514
- spreadBps: 0
2515
- } : await getOrderArrivalSnapshot({
2516
- connector,
2517
- symbol,
2518
- requireTimestamp: true
2519
- });
2520
- const age = Date.now() - snapshot2.arrivalSnapshotTime;
2521
- if (snapshot2.bid == null || snapshot2.ask == null || !Number.isFinite(snapshot2.bid) || !Number.isFinite(snapshot2.ask) || snapshot2.bid <= 0 || snapshot2.ask < snapshot2.bid)
2522
- throw new EntrySubmissionValidationError(
2523
- "ENTRY_QUOTE_UNAVAILABLE_AT_SUBMISSION"
2524
- );
2525
- if (!Number.isFinite(age) || age < -MAX_ENTRY_QUOTE_AGE_MS || age > MAX_ENTRY_QUOTE_AGE_MS)
2526
- throw new EntrySubmissionValidationError(
2527
- "ENTRY_QUOTE_STALE_AT_SUBMISSION"
2528
- );
2529
- const price = direction === "LONG" ? snapshot2.ask : snapshot2.bid;
2530
- validateEntryProtectionAtArrival({
2531
- direction,
2532
- signalPrice: price,
2533
- bid: snapshot2.bid,
2534
- ask: snapshot2.ask,
2535
- arrivalMid: snapshot2.arrivalMid,
2536
- takeProfits,
2537
- stopLossPrice
2538
- });
2539
- const prices = takeProfits.map((tp) => tp.price);
2540
- const economics2 = validateEntryExecution({
2541
- direction,
2542
- price,
2543
- qty,
2544
- stopLossPrice,
2545
- takeProfitPrice: direction === "LONG" ? Math.max(...prices) : Math.min(...prices),
2546
- limits
2547
- });
2548
- 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()
2549
2395
  };
2550
- };
2551
-
2552
- // src/ai.ts
2553
- var import_aiLanguages2 = require("@tradejs/core/aiLanguages");
2554
- var import_redis2 = require("@tradejs/infra/redis");
2555
-
2556
- // src/aiShared.ts
2557
- var MAX_AI_SERIES_POINTS = 5;
2558
- var COMPACT_INDICATORS_SNAPSHOT_SYMBOL = /* @__PURE__ */ Symbol.for(
2559
- "tradejs.indicators.compactSnapshot"
2560
- );
2561
- var COMPACT_INDICATORS_SNAPSHOT_KEY = "__tradejsCompactIndicatorsSnapshot";
2562
- var trimSeriesDeep = (value) => {
2563
- if (Array.isArray(value)) {
2564
- const trimmed = value.slice(-MAX_AI_SERIES_POINTS);
2565
- const isMatrix = trimmed.every((item) => Array.isArray(item));
2566
- if (isMatrix) {
2567
- return trimmed;
2568
- }
2569
- return trimmed.map(
2570
- (item) => item && typeof item === "object" ? trimSeriesDeep(item) : item
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)
2571
2414
  );
2572
2415
  }
2573
- if (value && typeof value === "object") {
2574
- return Object.fromEntries(
2575
- Object.entries(value).map(([key, nested]) => [
2576
- key,
2577
- trimSeriesDeep(nested)
2578
- ])
2416
+ return next;
2417
+ };
2418
+ var getActiveRuntimeTrade = async (params) => {
2419
+ const { userName, symbol, accountId, deploymentId } = params;
2420
+ if (!userName) {
2421
+ return null;
2422
+ }
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) {
2429
+ return null;
2430
+ }
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)
2579
2438
  );
2439
+ return null;
2580
2440
  }
2581
- return value;
2441
+ return existing;
2582
2442
  };
2583
- var buildCompactAiIndicatorsSnapshot = (value) => {
2584
- const compactSnapshot = value && typeof value === "object" ? value[COMPACT_INDICATORS_SNAPSHOT_SYMBOL] ?? value[COMPACT_INDICATORS_SNAPSHOT_KEY] : void 0;
2585
- if (typeof compactSnapshot === "function") {
2586
- return compactSnapshot({ limit: MAX_AI_SERIES_POINTS });
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;
2587
2458
  }
2588
- return trimSeriesDeep(value);
2589
- };
2590
-
2591
- // src/aiMarketContext.ts
2592
- var toRecord = (value) => {
2593
- if (!value || typeof value !== "object" || Array.isArray(value)) {
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) {
2594
2468
  return null;
2595
2469
  }
2596
- return value;
2597
- };
2598
- var toFiniteNumber = (value) => {
2599
- const numeric = typeof value === "number" ? value : typeof value === "string" && value.trim() ? Number(value) : Number.NaN;
2600
- return Number.isFinite(numeric) ? numeric : null;
2601
- };
2602
- var roundTo = (value, decimals) => {
2603
- const factor = 10 ** decimals;
2604
- return Math.round(value * factor) / factor;
2605
- };
2606
- var buildMissingSpreadContext = () => ({
2607
- source: "payload.additionalIndicators.baseContext.relative.execution.venueSpread",
2608
- indicatorKey: "payload.additionalIndicators.baseContext.relative.execution.venueSpread",
2609
- available: false,
2610
- value: null,
2611
- zScore: null,
2612
- bps: null,
2613
- absBps: null,
2614
- bias: null,
2615
- severity: null
2616
- });
2617
- var buildSpreadContextFromSignal = (signal) => {
2618
- const baseContext = toRecord(signal.additionalIndicators?.baseContext);
2619
- const relative = toRecord(baseContext?.relative);
2620
- const execution = toRecord(relative?.execution);
2621
- const spread = toFiniteNumber(execution?.venueSpread);
2622
- const zScore = toFiniteNumber(execution?.venueSpreadZScore);
2623
- if (spread == null) {
2624
- return buildMissingSpreadContext();
2470
+ const orderId = existing.orderId;
2471
+ if (strategy && existing.strategy !== strategy) {
2472
+ return null;
2625
2473
  }
2626
- const value = roundTo(spread, 8);
2627
- const bps = roundTo(value * 1e4, 2);
2628
- const absBps = Math.abs(bps);
2629
- const bias = absBps < 1 ? "flat" : bps > 0 ? "coinbase_premium" : "binance_premium";
2630
- const severity = absBps >= 20 ? "wide" : absBps >= 5 ? "elevated" : "normal";
2631
- return {
2632
- source: "payload.additionalIndicators.baseContext.relative.execution.venueSpread",
2633
- indicatorKey: "payload.additionalIndicators.baseContext.relative.execution.venueSpread",
2634
- available: true,
2635
- value,
2636
- zScore,
2637
- bps,
2638
- absBps,
2639
- bias,
2640
- severity
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()
2641
2491
  };
2642
- };
2643
- var buildTrueDeltaContextFromSignal = (signal) => {
2644
- const baseContext = toRecord(signal.additionalIndicators?.baseContext);
2645
- const participation = toRecord(baseContext?.participation);
2646
- const delta = toRecord(participation?.delta);
2647
- const source = String(delta?.source ?? "");
2648
- const isTrueDeltaSource = source === "kline_taker_volume" || source === "agg_trades" || source === "trades";
2649
- if (!delta || !isTrueDeltaSource) {
2650
- return {
2651
- source: source || null,
2652
- available: false,
2653
- buyPressurePct: null,
2654
- buyVolume: null,
2655
- sellVolume: null,
2656
- netDelta: null,
2657
- deltaPct: null,
2658
- signedVolumeZScore: null
2659
- };
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
+ );
2660
2537
  }
2661
- return {
2662
- source,
2663
- available: true,
2664
- buyPressurePct: toFiniteNumber(delta.buyPressurePct),
2665
- buyVolume: toFiniteNumber(delta.buyVolume),
2666
- sellVolume: toFiniteNumber(delta.sellVolume),
2667
- netDelta: toFiniteNumber(delta.netDelta),
2668
- deltaPct: toFiniteNumber(delta.deltaPct),
2669
- signedVolumeZScore: toFiniteNumber(delta.signedVolumeZScore)
2670
- };
2671
- };
2672
- var buildTradeFlowContextFromSignal = (signal) => {
2673
- const baseContext = toRecord(signal.additionalIndicators?.baseContext);
2674
- const participation = toRecord(baseContext?.participation);
2675
- const tradeFlow = toRecord(participation?.tradeFlow);
2676
- if (!tradeFlow) {
2677
- return {
2678
- source: null,
2679
- available: false,
2680
- interval: null,
2681
- stale: null,
2682
- trades: null,
2683
- buyPressurePct: null,
2684
- netBaseDelta: null,
2685
- netQuoteDelta: null
2686
- };
2687
- }
2688
- return {
2689
- source: String(tradeFlow.source ?? ""),
2690
- available: true,
2691
- interval: String(tradeFlow.interval ?? ""),
2692
- stale: typeof tradeFlow.stale === "boolean" ? tradeFlow.stale : null,
2693
- trades: toFiniteNumber(tradeFlow.trades),
2694
- buyPressurePct: toFiniteNumber(tradeFlow.buyPressurePct),
2695
- netBaseDelta: toFiniteNumber(tradeFlow.netBaseDelta),
2696
- netQuoteDelta: toFiniteNumber(tradeFlow.netQuoteDelta)
2697
- };
2698
- };
2699
- var buildMarketBreadthContextFromSignal = (signal) => {
2700
- const baseContext = toRecord(signal.additionalIndicators?.baseContext);
2701
- const relative = toRecord(baseContext?.relative);
2702
- const breadth = toRecord(relative?.marketBreadth);
2703
- if (!breadth) {
2704
- return {
2705
- source: null,
2706
- available: false,
2707
- universe: null,
2708
- interval: null,
2709
- stale: null,
2710
- symbolsCount: null,
2711
- advanceDeclineRatio: null,
2712
- pctAboveMa20: null,
2713
- pctAboveMa50: null,
2714
- equalWeightedReturn: null,
2715
- volumeWeightedReturn: null,
2716
- dispersion: null
2717
- };
2718
- }
2719
- return {
2720
- source: String(breadth.source ?? ""),
2721
- available: true,
2722
- universe: String(breadth.universe ?? ""),
2723
- interval: String(breadth.interval ?? ""),
2724
- stale: typeof breadth.stale === "boolean" ? breadth.stale : null,
2725
- symbolsCount: toFiniteNumber(breadth.symbolsCount),
2726
- advanceDeclineRatio: toFiniteNumber(breadth.advanceDeclineRatio),
2727
- pctAboveMa20: toFiniteNumber(breadth.pctAboveMa20),
2728
- pctAboveMa50: toFiniteNumber(breadth.pctAboveMa50),
2729
- equalWeightedReturn: toFiniteNumber(breadth.equalWeightedReturn),
2730
- volumeWeightedReturn: toFiniteNumber(breadth.volumeWeightedReturn),
2731
- dispersion: toFiniteNumber(breadth.dispersion)
2732
- };
2538
+ return next;
2733
2539
  };
2734
- var buildMarketBreadthsContextFromSignal = (signal) => {
2735
- const baseContext = toRecord(signal.additionalIndicators?.baseContext);
2736
- const relative = toRecord(baseContext?.relative);
2737
- const breadths = toRecord(relative?.marketBreadths);
2738
- const build = (key) => {
2739
- const breadth = toRecord(breadths?.[key]);
2740
- 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);
2741
2565
  return {
2742
- source: null,
2743
- available: false,
2744
- universe: null,
2745
- interval: null,
2746
- stale: null,
2747
- symbolsCount: null,
2748
- advanceDeclineRatio: null,
2749
- pctAboveMa20: null,
2750
- pctAboveMa50: null,
2751
- equalWeightedReturn: null,
2752
- volumeWeightedReturn: null,
2753
- 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
2754
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));
2755
2617
  }
2756
- return {
2757
- source: String(breadth.source ?? ""),
2758
- available: true,
2759
- universe: String(breadth.universe ?? ""),
2760
- interval: String(breadth.interval ?? ""),
2761
- stale: typeof breadth.stale === "boolean" ? breadth.stale : null,
2762
- symbolsCount: toFiniteNumber(breadth.symbolsCount),
2763
- advanceDeclineRatio: toFiniteNumber(breadth.advanceDeclineRatio),
2764
- pctAboveMa20: toFiniteNumber(breadth.pctAboveMa20),
2765
- pctAboveMa50: toFiniteNumber(breadth.pctAboveMa50),
2766
- equalWeightedReturn: toFiniteNumber(breadth.equalWeightedReturn),
2767
- volumeWeightedReturn: toFiniteNumber(breadth.volumeWeightedReturn),
2768
- dispersion: toFiniteNumber(breadth.dispersion)
2769
- };
2770
2618
  };
2771
- return {
2772
- top5: build("top5"),
2773
- top10: build("top10"),
2774
- top30: build("top30"),
2775
- top50: build("top50"),
2776
- 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
2777
2645
  };
2778
- };
2779
- var buildTargetVsBtcContextFromSignal = (signal) => {
2780
- const baseContext = toRecord(signal.additionalIndicators?.baseContext);
2781
- const relative = toRecord(baseContext?.relative);
2782
- const targetVsBtc = toRecord(relative?.targetVsBtc);
2783
- if (!targetVsBtc) {
2784
- return {
2785
- source: null,
2786
- available: false,
2787
- ratioReturn1h: null,
2788
- ratioReturn4h: null,
2789
- ratioReturn24h: null,
2790
- alphaVsBtc1h: null,
2791
- alphaVsBtc4h: null,
2792
- alphaVsBtc24h: null,
2793
- betaToBtc20: null,
2794
- correlationToBtc20: null,
2795
- ratioTrend: null
2796
- };
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);
2797
2745
  }
2798
2746
  return {
2799
- source: String(targetVsBtc.source ?? ""),
2800
- available: true,
2801
- ratioReturn1h: toFiniteNumber(targetVsBtc.ratioReturn1h),
2802
- ratioReturn4h: toFiniteNumber(targetVsBtc.ratioReturn4h),
2803
- ratioReturn24h: toFiniteNumber(targetVsBtc.ratioReturn24h),
2804
- alphaVsBtc1h: toFiniteNumber(targetVsBtc.alphaVsBtc1h),
2805
- alphaVsBtc4h: toFiniteNumber(targetVsBtc.alphaVsBtc4h),
2806
- alphaVsBtc24h: toFiniteNumber(targetVsBtc.alphaVsBtc24h),
2807
- betaToBtc20: toFiniteNumber(targetVsBtc.betaToBtc20),
2808
- correlationToBtc20: toFiniteNumber(targetVsBtc.correlationToBtc20),
2809
- ratioTrend: typeof targetVsBtc.ratioTrend === "string" ? targetVsBtc.ratioTrend : null
2747
+ projectRoot,
2748
+ state
2810
2749
  };
2811
2750
  };
2812
- var buildBtcAltRegimeContextFromSignal = (signal) => {
2813
- const baseContext = toRecord(signal.additionalIndicators?.baseContext);
2814
- const relative = toRecord(baseContext?.relative);
2815
- const btcAltRegime = toRecord(relative?.btcAltRegime);
2816
- if (!btcAltRegime) {
2817
- return {
2818
- source: null,
2819
- available: false,
2820
- universe: null,
2821
- interval: null,
2822
- stale: null,
2823
- regime: null,
2824
- btcReturn24h: null,
2825
- altBasketReturn24h: null,
2826
- btcVsAltReturn24h: null,
2827
- btcTurnoverShare24h: null,
2828
- btcTurnoverShareChange24h: null,
2829
- altVolToBtcVol24h: null,
2830
- altDispersion24h: null
2831
- };
2832
- }
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);
2833
2756
  return {
2834
- source: String(btcAltRegime.source ?? ""),
2835
- available: true,
2836
- universe: String(btcAltRegime.universe ?? ""),
2837
- interval: String(btcAltRegime.interval ?? ""),
2838
- stale: typeof btcAltRegime.stale === "boolean" ? btcAltRegime.stale : null,
2839
- regime: typeof btcAltRegime.regime === "string" ? btcAltRegime.regime : null,
2840
- btcReturn24h: toFiniteNumber(btcAltRegime.btcReturn24h),
2841
- altBasketReturn24h: toFiniteNumber(btcAltRegime.altBasketReturn24h),
2842
- btcVsAltReturn24h: toFiniteNumber(btcAltRegime.btcVsAltReturn24h),
2843
- btcTurnoverShare24h: toFiniteNumber(btcAltRegime.btcTurnoverShare24h),
2844
- btcTurnoverShareChange24h: toFiniteNumber(
2845
- btcAltRegime.btcTurnoverShareChange24h
2846
- ),
2847
- altVolToBtcVol24h: toFiniteNumber(btcAltRegime.altVolToBtcVol24h),
2848
- altDispersion24h: toFiniteNumber(btcAltRegime.altDispersion24h)
2757
+ strategyModules: toUniqueModules2(config.strategies),
2758
+ indicatorModules: toUniqueModules2(config.indicators)
2849
2759
  };
2850
2760
  };
2851
- var buildCmcGlobalContextFromSignal = (signal) => {
2852
- const baseContext = toRecord(signal.additionalIndicators?.baseContext);
2853
- const relative = toRecord(baseContext?.relative);
2854
- const cmcGlobal = toRecord(relative?.cmcGlobal);
2855
- if (!cmcGlobal) {
2856
- return {
2857
- source: null,
2858
- available: false,
2859
- interval: null,
2860
- asOfTs: null,
2861
- stale: null,
2862
- totalMarketCapUsd: null,
2863
- totalVolumeUsd: null,
2864
- totalVolumeReportedUsd: null,
2865
- altMarketCapUsd: null,
2866
- altVolumeUsd: null,
2867
- altVolumeReportedUsd: null,
2868
- btcDominancePct: null,
2869
- ethDominancePct: null,
2870
- btcDominanceChange24hPct: null,
2871
- ethDominanceChange24hPct: null,
2872
- altMarketCapChange24hPct: null,
2873
- altVolumeChange24hPct: null,
2874
- activeCryptocurrencies: null,
2875
- activeExchanges: null,
2876
- activeMarketPairs: null,
2877
- altLiquidityRegime: null
2878
- };
2879
- }
2880
- return {
2881
- source: String(cmcGlobal.source ?? ""),
2882
- available: true,
2883
- interval: typeof cmcGlobal.interval === "string" ? cmcGlobal.interval : null,
2884
- asOfTs: toFiniteNumber(cmcGlobal.asOfTs),
2885
- stale: typeof cmcGlobal.stale === "boolean" ? cmcGlobal.stale : null,
2886
- totalMarketCapUsd: toFiniteNumber(cmcGlobal.totalMarketCapUsd),
2887
- totalVolumeUsd: toFiniteNumber(cmcGlobal.totalVolumeUsd),
2888
- totalVolumeReportedUsd: toFiniteNumber(cmcGlobal.totalVolumeReportedUsd),
2889
- altMarketCapUsd: toFiniteNumber(cmcGlobal.altMarketCapUsd),
2890
- altVolumeUsd: toFiniteNumber(cmcGlobal.altVolumeUsd),
2891
- altVolumeReportedUsd: toFiniteNumber(cmcGlobal.altVolumeReportedUsd),
2892
- btcDominancePct: toFiniteNumber(cmcGlobal.btcDominancePct),
2893
- ethDominancePct: toFiniteNumber(cmcGlobal.ethDominancePct),
2894
- btcDominanceChange24hPct: toFiniteNumber(
2895
- cmcGlobal.btcDominanceChange24hPct
2896
- ),
2897
- ethDominanceChange24hPct: toFiniteNumber(
2898
- cmcGlobal.ethDominanceChange24hPct
2899
- ),
2900
- altMarketCapChange24hPct: toFiniteNumber(
2901
- cmcGlobal.altMarketCapChange24hPct
2761
+ var extractModuleEntries = (moduleExport, key) => {
2762
+ if (!moduleExport || typeof moduleExport !== "object") {
2763
+ return null;
2764
+ }
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`
2902
3381
  ),
2903
- altVolumeChange24hPct: toFiniteNumber(cmcGlobal.altVolumeChange24hPct),
2904
- activeCryptocurrencies: toFiniteNumber(cmcGlobal.activeCryptocurrencies),
2905
- activeExchanges: toFiniteNumber(cmcGlobal.activeExchanges),
2906
- activeMarketPairs: toFiniteNumber(cmcGlobal.activeMarketPairs),
2907
- 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
2908
3460
  };
2909
3461
  };
2910
- var buildCmcReferenceAssetsContextFromSignal = (signal) => {
2911
- const baseContext = toRecord(signal.additionalIndicators?.baseContext);
2912
- const relative = toRecord(baseContext?.relative);
2913
- const cmcReferenceAssets = toRecord(relative?.cmcReferenceAssets);
2914
- 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
+ );
2915
3503
  return {
2916
- source: null,
2917
- available: false,
2918
- interval: null,
2919
- asOfTs: null,
2920
- stale: null,
2921
- btcMarketCapUsd: null,
2922
- ethMarketCapUsd: null,
2923
- btcVolumeUsd: null,
2924
- ethVolumeUsd: null,
2925
- btcVolumeToMarketCap: null,
2926
- ethVolumeToMarketCap: null,
2927
- ethBtcMarketCapRatio: null,
2928
- ethBtcMarketCapRatioChange24hPct: null,
2929
- ethVsBtcVolumeRatio: null,
2930
- referenceLiquidityRegime: null
3504
+ arrivalSnapshotTime: Date.now(),
3505
+ arrivalSource: "top_of_book_error",
3506
+ bid: null,
3507
+ ask: null,
3508
+ arrivalMid: null,
3509
+ spreadBps: null
2931
3510
  };
2932
3511
  }
2933
- return {
2934
- source: String(cmcReferenceAssets.source ?? ""),
2935
- available: true,
2936
- interval: typeof cmcReferenceAssets.interval === "string" ? cmcReferenceAssets.interval : null,
2937
- asOfTs: toFiniteNumber(cmcReferenceAssets.asOfTs),
2938
- stale: typeof cmcReferenceAssets.stale === "boolean" ? cmcReferenceAssets.stale : null,
2939
- btcMarketCapUsd: toFiniteNumber(cmcReferenceAssets.btcMarketCapUsd),
2940
- ethMarketCapUsd: toFiniteNumber(cmcReferenceAssets.ethMarketCapUsd),
2941
- btcVolumeUsd: toFiniteNumber(cmcReferenceAssets.btcVolumeUsd),
2942
- ethVolumeUsd: toFiniteNumber(cmcReferenceAssets.ethVolumeUsd),
2943
- btcVolumeToMarketCap: toFiniteNumber(
2944
- cmcReferenceAssets.btcVolumeToMarketCap
2945
- ),
2946
- ethVolumeToMarketCap: toFiniteNumber(
2947
- cmcReferenceAssets.ethVolumeToMarketCap
2948
- ),
2949
- ethBtcMarketCapRatio: toFiniteNumber(
2950
- cmcReferenceAssets.ethBtcMarketCapRatio
2951
- ),
2952
- ethBtcMarketCapRatioChange24hPct: toFiniteNumber(
2953
- cmcReferenceAssets.ethBtcMarketCapRatioChange24hPct
2954
- ),
2955
- ethVsBtcVolumeRatio: toFiniteNumber(cmcReferenceAssets.ethVsBtcVolumeRatio),
2956
- 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);
2957
3602
  };
2958
3603
  };
2959
- 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) => {
2960
3675
  const baseContext = toRecord(signal.additionalIndicators?.baseContext);
2961
3676
  const relative = toRecord(baseContext?.relative);
2962
- const cmcExchangeLiquidity = toRecord(relative?.cmcExchangeLiquidity);
2963
- if (!cmcExchangeLiquidity) {
2964
- return {
2965
- source: null,
2966
- available: false,
2967
- interval: null,
2968
- asOfTs: null,
2969
- stale: null,
2970
- exchangesCount: null,
2971
- totalVolumeUsd: null,
2972
- totalVolumeChange24hPct: null,
2973
- binanceVolumeUsd: null,
2974
- binanceVolumeShare: null,
2975
- topExchangeVolumeShare: null,
2976
- liquidityRegime: null
2977
- };
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();
2978
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";
2979
3688
  return {
2980
- source: String(cmcExchangeLiquidity.source ?? ""),
3689
+ source: "payload.additionalIndicators.baseContext.relative.execution.venueSpread",
3690
+ indicatorKey: "payload.additionalIndicators.baseContext.relative.execution.venueSpread",
2981
3691
  available: true,
2982
- interval: typeof cmcExchangeLiquidity.interval === "string" ? cmcExchangeLiquidity.interval : null,
2983
- asOfTs: toFiniteNumber(cmcExchangeLiquidity.asOfTs),
2984
- stale: typeof cmcExchangeLiquidity.stale === "boolean" ? cmcExchangeLiquidity.stale : null,
2985
- exchangesCount: toFiniteNumber(cmcExchangeLiquidity.exchangesCount),
2986
- totalVolumeUsd: toFiniteNumber(cmcExchangeLiquidity.totalVolumeUsd),
2987
- totalVolumeChange24hPct: toFiniteNumber(
2988
- cmcExchangeLiquidity.totalVolumeChange24hPct
2989
- ),
2990
- binanceVolumeUsd: toFiniteNumber(cmcExchangeLiquidity.binanceVolumeUsd),
2991
- binanceVolumeShare: toFiniteNumber(cmcExchangeLiquidity.binanceVolumeShare),
2992
- topExchangeVolumeShare: toFiniteNumber(
2993
- cmcExchangeLiquidity.topExchangeVolumeShare
2994
- ),
2995
- liquidityRegime: typeof cmcExchangeLiquidity.liquidityRegime === "string" ? cmcExchangeLiquidity.liquidityRegime : null
3692
+ value,
3693
+ zScore,
3694
+ bps,
3695
+ absBps,
3696
+ bias,
3697
+ severity
2996
3698
  };
2997
3699
  };
2998
- var buildCmcFearGreedContextFromSignal = (signal) => {
2999
- const baseContext = toRecord(signal.additionalIndicators?.baseContext);
3000
- const relative = toRecord(baseContext?.relative);
3001
- const cmcFearGreed = toRecord(relative?.cmcFearGreed);
3002
- if (!cmcFearGreed) {
3700
+ var buildTrueDeltaContextFromSignal = (signal) => {
3701
+ const baseContext = toRecord(signal.additionalIndicators?.baseContext);
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) {
3003
3707
  return {
3004
- source: null,
3708
+ source: source || null,
3005
3709
  available: false,
3006
- interval: null,
3007
- asOfTs: null,
3008
- stale: null,
3009
- value: null,
3010
- valueChange24h: null,
3011
- valueChange7d: null,
3012
- classification: null,
3013
- sentimentRegime: null
3710
+ buyPressurePct: null,
3711
+ buyVolume: null,
3712
+ sellVolume: null,
3713
+ netDelta: null,
3714
+ deltaPct: null,
3715
+ signedVolumeZScore: null
3014
3716
  };
3015
3717
  }
3016
3718
  return {
3017
- source: String(cmcFearGreed.source ?? ""),
3719
+ source,
3018
3720
  available: true,
3019
- interval: typeof cmcFearGreed.interval === "string" ? cmcFearGreed.interval : null,
3020
- asOfTs: toFiniteNumber(cmcFearGreed.asOfTs),
3021
- stale: typeof cmcFearGreed.stale === "boolean" ? cmcFearGreed.stale : null,
3022
- value: toFiniteNumber(cmcFearGreed.value),
3023
- valueChange24h: toFiniteNumber(cmcFearGreed.valueChange24h),
3024
- valueChange7d: toFiniteNumber(cmcFearGreed.valueChange7d),
3025
- classification: typeof cmcFearGreed.classification === "string" ? cmcFearGreed.classification : null,
3026
- 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)
3027
3727
  };
3028
3728
  };
3029
- var buildCmcIndexesContextFromSignal = (signal) => {
3729
+ var buildTradeFlowContextFromSignal = (signal) => {
3030
3730
  const baseContext = toRecord(signal.additionalIndicators?.baseContext);
3031
- const relative = toRecord(baseContext?.relative);
3032
- const cmcIndexes = toRecord(relative?.cmcIndexes);
3033
- if (!cmcIndexes) {
3731
+ const participation = toRecord(baseContext?.participation);
3732
+ const tradeFlow = toRecord(participation?.tradeFlow);
3733
+ if (!tradeFlow) {
3034
3734
  return {
3035
3735
  source: null,
3036
3736
  available: false,
3037
3737
  interval: null,
3038
- asOfTs: null,
3039
3738
  stale: null,
3040
- cmc100Value: null,
3041
- cmc100Change24hPct: null,
3042
- cmc100TopConstituentSymbol: null,
3043
- cmc100TopConstituentWeightPct: null,
3044
- cmc20Value: null,
3045
- cmc20Change24hPct: null,
3046
- cmc20TopConstituentSymbol: null,
3047
- cmc20TopConstituentWeightPct: null,
3048
- cmc20ToCmc100Ratio: null,
3049
- cmc20ToCmc100RatioChange24hPct: null,
3050
- indexRegime: null
3739
+ trades: null,
3740
+ buyPressurePct: null,
3741
+ netBaseDelta: null,
3742
+ netQuoteDelta: null
3051
3743
  };
3052
3744
  }
3053
3745
  return {
3054
- source: String(cmcIndexes.source ?? ""),
3746
+ source: String(tradeFlow.source ?? ""),
3055
3747
  available: true,
3056
- interval: typeof cmcIndexes.interval === "string" ? cmcIndexes.interval : null,
3057
- asOfTs: toFiniteNumber(cmcIndexes.asOfTs),
3058
- stale: typeof cmcIndexes.stale === "boolean" ? cmcIndexes.stale : null,
3059
- cmc100Value: toFiniteNumber(cmcIndexes.cmc100Value),
3060
- cmc100Change24hPct: toFiniteNumber(cmcIndexes.cmc100Change24hPct),
3061
- cmc100TopConstituentSymbol: typeof cmcIndexes.cmc100TopConstituentSymbol === "string" ? cmcIndexes.cmc100TopConstituentSymbol : null,
3062
- cmc100TopConstituentWeightPct: toFiniteNumber(
3063
- cmcIndexes.cmc100TopConstituentWeightPct
3064
- ),
3065
- cmc20Value: toFiniteNumber(cmcIndexes.cmc20Value),
3066
- cmc20Change24hPct: toFiniteNumber(cmcIndexes.cmc20Change24hPct),
3067
- cmc20TopConstituentSymbol: typeof cmcIndexes.cmc20TopConstituentSymbol === "string" ? cmcIndexes.cmc20TopConstituentSymbol : null,
3068
- cmc20TopConstituentWeightPct: toFiniteNumber(
3069
- cmcIndexes.cmc20TopConstituentWeightPct
3070
- ),
3071
- cmc20ToCmc100Ratio: toFiniteNumber(cmcIndexes.cmc20ToCmc100Ratio),
3072
- cmc20ToCmc100RatioChange24hPct: toFiniteNumber(
3073
- cmcIndexes.cmc20ToCmc100RatioChange24hPct
3074
- ),
3075
- 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)
3076
3754
  };
3077
3755
  };
3078
- var buildReferenceTradeFlowContextFromSignal = (signal) => {
3756
+ var buildMarketBreadthContextFromSignal = (signal) => {
3079
3757
  const baseContext = toRecord(signal.additionalIndicators?.baseContext);
3080
3758
  const relative = toRecord(baseContext?.relative);
3081
- const refs = toRecord(relative?.referenceTradeFlow);
3082
- const primaryReferenceSymbol = typeof refs?.primaryReferenceSymbol === "string" ? refs.primaryReferenceSymbol : null;
3083
- const tradeFlowBySymbol = toRecord(refs?.tradeFlowBySymbol);
3084
- const primaryTradeFlow = primaryReferenceSymbol != null ? toRecord(tradeFlowBySymbol?.[primaryReferenceSymbol]) : null;
3085
- if (!refs) {
3759
+ const breadth = toRecord(relative?.marketBreadth);
3760
+ if (!breadth) {
3086
3761
  return {
3087
3762
  source: null,
3088
3763
  available: false,
3089
- primaryReferenceSymbol: null,
3090
- referenceSymbols: [],
3091
- primaryTradeFlowBuyPressurePct: null,
3092
- 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
3093
3774
  };
3094
3775
  }
3095
3776
  return {
3096
- source: String(refs.source ?? ""),
3777
+ source: String(breadth.source ?? ""),
3097
3778
  available: true,
3098
- primaryReferenceSymbol,
3099
- referenceSymbols: Array.isArray(refs.referenceSymbols) ? refs.referenceSymbols.map(String) : [],
3100
- primaryTradeFlowBuyPressurePct: toFiniteNumber(
3101
- primaryTradeFlow?.buyPressurePct
3102
- ),
3103
- primaryTradeFlowStale: typeof primaryTradeFlow?.stale === "boolean" ? primaryTradeFlow.stale : null
3104
- };
3105
- };
3106
- var buildAiMarketContext = (signal) => ({
3107
- execution: {
3108
- binanceCoinbaseSpread: buildSpreadContextFromSignal(signal)
3109
- },
3110
- participation: {
3111
- trueDelta: buildTrueDeltaContextFromSignal(signal),
3112
- tradeFlow: buildTradeFlowContextFromSignal(signal)
3113
- },
3114
- relative: {
3115
- marketBreadth: buildMarketBreadthContextFromSignal(signal),
3116
- marketBreadths: buildMarketBreadthsContextFromSignal(signal),
3117
- targetVsBtc: buildTargetVsBtcContextFromSignal(signal),
3118
- btcAltRegime: buildBtcAltRegimeContextFromSignal(signal),
3119
- cmcGlobal: buildCmcGlobalContextFromSignal(signal),
3120
- cmcReferenceAssets: buildCmcReferenceAssetsContextFromSignal(signal),
3121
- cmcExchangeLiquidity: buildCmcExchangeLiquidityContextFromSignal(signal),
3122
- cmcFearGreed: buildCmcFearGreedContextFromSignal(signal),
3123
- cmcIndexes: buildCmcIndexesContextFromSignal(signal),
3124
- referenceTradeFlow: buildReferenceTradeFlowContextFromSignal(signal)
3125
- }
3126
- });
3127
-
3128
- // src/strategy/manifests.ts
3129
- var import_indicators = require("@tradejs/core/indicators");
3130
- var SHARED_STRATEGY_REGISTRY_KEY = "__tradejsNodeSharedStrategyRegistryV1__";
3131
- var sharedRegistryScope = globalThis;
3132
- var sharedStrategyRegistry = sharedRegistryScope[SHARED_STRATEGY_REGISTRY_KEY] ?? (sharedRegistryScope[SHARED_STRATEGY_REGISTRY_KEY] = {
3133
- registryStateByProjectRoot: /* @__PURE__ */ new Map()
3134
- });
3135
- var createStrategyRegistryState = () => ({
3136
- strategyCreators: /* @__PURE__ */ new Map(),
3137
- strategyManifestsMap: /* @__PURE__ */ new Map(),
3138
- strategyEntriesMap: /* @__PURE__ */ new Map(),
3139
- strategySourcesMap: /* @__PURE__ */ new Map(),
3140
- strategyModuleResolutions: /* @__PURE__ */ new Map(),
3141
- pluginsLoadPromise: null
3142
- });
3143
- var registryStateByProjectRoot2 = sharedStrategyRegistry.registryStateByProjectRoot;
3144
- var getStrategyRegistryState = (cwd = getTradejsProjectCwd()) => {
3145
- const projectRoot = getTradejsProjectCwd(cwd);
3146
- let state = registryStateByProjectRoot2.get(projectRoot);
3147
- if (!state) {
3148
- state = createStrategyRegistryState();
3149
- registryStateByProjectRoot2.set(projectRoot, state);
3150
- }
3151
- return {
3152
- projectRoot,
3153
- state
3154
- };
3155
- };
3156
- var toUniqueModules2 = (modules = []) => [
3157
- ...new Set(modules.map((moduleName) => moduleName.trim()).filter(Boolean))
3158
- ];
3159
- var getConfiguredPluginModuleNames = async (cwd = getTradejsProjectCwd()) => {
3160
- const config = await loadTradejsConfig(cwd);
3161
- return {
3162
- strategyModules: toUniqueModules2(config.strategies),
3163
- 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)
3164
3789
  };
3165
3790
  };
3166
- var extractModuleEntries = (moduleExport, key) => {
3167
- if (!moduleExport || typeof moduleExport !== "object") {
3168
- return null;
3169
- }
3170
- const candidate = moduleExport;
3171
- if (Array.isArray(candidate[key])) {
3172
- return candidate[key];
3173
- }
3174
- const defaultExport = candidate.default;
3175
- if (defaultExport && Array.isArray(defaultExport[key])) {
3176
- return defaultExport[key];
3177
- }
3178
- return null;
3179
- };
3180
- var extractStrategyPluginDefinition = (moduleExport) => {
3181
- const strategyEntries = extractModuleEntries(
3182
- moduleExport,
3183
- "strategyEntries"
3184
- );
3185
- return strategyEntries ? { strategyEntries } : null;
3186
- };
3187
- var extractIndicatorPluginDefinition = (moduleExport) => {
3188
- const indicatorEntries = extractModuleEntries(
3189
- moduleExport,
3190
- "indicatorEntries"
3191
- );
3192
- return indicatorEntries ? { indicatorEntries } : null;
3193
- };
3194
- var validateStrategyEntries = (moduleName, entries, state) => {
3195
- const issues = [];
3196
- const names = /* @__PURE__ */ new Set();
3197
- entries.forEach((entry, index) => {
3198
- const entryPath = `${moduleName}.strategyEntries[${index}]`;
3199
- const strategyName = entry?.manifest?.name;
3200
- if (typeof strategyName !== "string" || !strategyName.trim()) {
3201
- issues.push(`${entryPath}: manifest.name is required`);
3202
- } else if (names.has(strategyName) || state?.strategyEntriesMap.has(strategyName)) {
3203
- issues.push(`${entryPath}: duplicate strategy ${strategyName}`);
3204
- } else {
3205
- names.add(strategyName);
3206
- }
3207
- if (!entry || typeof entry !== "object" || !entry.defaults || typeof entry.defaults !== "object" || Array.isArray(entry.defaults)) {
3208
- issues.push(`${entryPath}: defaults must be an object`);
3209
- }
3210
- if (typeof entry?.parseConfig !== "function") {
3211
- issues.push(`${entryPath}: parseConfig is required`);
3212
- }
3213
- if (typeof entry?.createCore !== "function") {
3214
- 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
+ };
3215
3812
  }
3216
- });
3217
- return issues;
3218
- };
3219
- var registerEntries2 = (entries, source, state) => {
3220
- for (const entry of entries) {
3221
- const strategyName = entry.manifest.name;
3222
- state.strategyManifestsMap.set(strategyName, entry.manifest);
3223
- state.strategyEntriesMap.set(strategyName, entry);
3224
- state.strategySourcesMap.set(strategyName, source);
3225
- materializeStrategyCreator(strategyName, state);
3226
- }
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
+ };
3227
3835
  };
3228
- var materializeStrategyCreator = (strategyName, state) => {
3229
- if (state.strategyCreators.has(strategyName) || !sharedStrategyRegistry.strategyRuntimeFactory) {
3230
- return;
3231
- }
3232
- const entry = state.strategyEntriesMap.get(strategyName);
3233
- if (!entry) return;
3234
- state.strategyCreators.set(
3235
- strategyName,
3236
- sharedStrategyRegistry.strategyRuntimeFactory({
3237
- strategyName,
3238
- defaults: entry.defaults,
3239
- createCore: entry.createCore,
3240
- manifest: entry.manifest,
3241
- detectorKey: entry.detectorKey,
3242
- detectorNoSignalSkipReason: entry.detectorNoSignalSkipReason,
3243
- resolveRegisteredManifest: (name) => state.strategyManifestsMap.get(name)
3244
- })
3245
- );
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
+ };
3246
3853
  };
3247
- var setStrategyRuntimeFactory = (factory) => {
3248
- sharedStrategyRegistry.strategyRuntimeFactory = factory;
3249
- for (const state of registryStateByProjectRoot2.values()) {
3250
- for (const strategyName of state.strategyEntriesMap.keys()) {
3251
- materializeStrategyCreator(strategyName, state);
3252
- }
3253
- }
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
+ };
3254
3864
  };
3255
- var importStrategyPluginModule = async (moduleName, cwd = getTradejsProjectCwd()) => {
3256
- if (typeof importTradejsModule === "function") {
3257
- return importTradejsModule(moduleName, cwd);
3258
- }
3259
- return import(
3260
- /* webpackIgnore: true */
3261
- moduleName
3262
- );
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
+ };
3263
3875
  };
3264
- var resolveStrategyModuleEntry = async ({
3265
- strategyName,
3266
- moduleName,
3267
- cwd
3268
- }) => {
3269
- const { projectRoot, state } = getStrategyRegistryState(cwd);
3270
- const source = moduleName.trim();
3271
- const resolvedModuleName = resolvePluginModuleSpecifier(source, projectRoot);
3272
- const cacheKey = `${resolvedModuleName}\0${strategyName}`;
3273
- const cached = state.strategyModuleResolutions.get(cacheKey);
3274
- if (cached) return cached;
3275
- const resolution = (async () => {
3276
- const moduleExport = await importStrategyPluginModule(
3277
- resolvedModuleName,
3278
- projectRoot
3279
- );
3280
- const pluginDefinition = extractStrategyPluginDefinition(moduleExport);
3281
- if (!pluginDefinition) {
3282
- throw new Error(`${source}: export { strategyEntries } is missing`);
3283
- }
3284
- const issues = validateStrategyEntries(
3285
- source,
3286
- pluginDefinition.strategyEntries
3287
- );
3288
- if (issues.length > 0) {
3289
- throw new Error(
3290
- ["Invalid TradeJS strategy module:", ...issues].join("\n")
3291
- );
3292
- }
3293
- const entry = pluginDefinition.strategyEntries.find(
3294
- (candidate) => candidate.manifest.name === strategyName
3295
- );
3296
- if (!entry) {
3297
- throw new Error(`${source}: strategy ${strategyName} is missing`);
3298
- }
3299
- const factory = sharedStrategyRegistry.strategyRuntimeFactory;
3300
- if (!factory) {
3301
- throw new Error("Strategy runtime factory is not configured");
3302
- }
3303
- const manifestsByName = new Map(
3304
- pluginDefinition.strategyEntries.map(
3305
- (candidate) => [candidate.manifest.name, candidate.manifest]
3306
- )
3307
- );
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) {
3308
3881
  return {
3309
- entry,
3310
- creator: factory({
3311
- strategyName,
3312
- defaults: entry.defaults,
3313
- createCore: entry.createCore,
3314
- manifest: entry.manifest,
3315
- detectorKey: entry.detectorKey,
3316
- detectorNoSignalSkipReason: entry.detectorNoSignalSkipReason,
3317
- resolveRegisteredManifest: (name) => manifestsByName.get(name) ?? state.strategyManifestsMap.get(name)
3318
- }),
3319
- 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
3320
3895
  };
3321
- })();
3322
- state.strategyModuleResolutions.set(cacheKey, resolution);
3323
- try {
3324
- return await resolution;
3325
- } catch (error) {
3326
- state.strategyModuleResolutions.delete(cacheKey);
3327
- throw error;
3328
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
+ };
3329
3914
  };
3330
- var ensureStrategyPluginsLoaded = async (cwd = getTradejsProjectCwd()) => {
3331
- const { projectRoot, state } = getStrategyRegistryState(cwd);
3332
- if (!state.pluginsLoadPromise) {
3333
- (0, import_indicators.resetIndicatorRegistryCache)(projectRoot);
3334
- state.pluginsLoadPromise = (async () => {
3335
- const { strategyModules, indicatorModules } = await getConfiguredPluginModuleNames(projectRoot);
3336
- const strategySet = new Set(strategyModules);
3337
- const indicatorSet = new Set(indicatorModules);
3338
- const pluginModuleNames = [
3339
- .../* @__PURE__ */ new Set([...strategyModules, ...indicatorModules])
3340
- ];
3341
- if (!pluginModuleNames.length) {
3342
- return;
3343
- }
3344
- const issues = [];
3345
- for (const moduleName of pluginModuleNames) {
3346
- try {
3347
- const resolvedModuleName = resolvePluginModuleSpecifier(
3348
- moduleName,
3349
- projectRoot
3350
- );
3351
- const moduleExport = await importStrategyPluginModule(
3352
- resolvedModuleName,
3353
- projectRoot
3354
- );
3355
- if (strategySet.has(moduleName)) {
3356
- const pluginDefinition = extractStrategyPluginDefinition(moduleExport);
3357
- if (!pluginDefinition) {
3358
- issues.push(
3359
- `${moduleName}: export { strategyEntries } is missing`
3360
- );
3361
- } else {
3362
- const entryIssues = validateStrategyEntries(
3363
- moduleName,
3364
- pluginDefinition.strategyEntries,
3365
- state
3366
- );
3367
- issues.push(...entryIssues);
3368
- if (entryIssues.length === 0) {
3369
- registerEntries2(
3370
- pluginDefinition.strategyEntries,
3371
- moduleName,
3372
- state
3373
- );
3374
- }
3375
- }
3376
- }
3377
- if (indicatorSet.has(moduleName)) {
3378
- const indicatorPluginDefinition = extractIndicatorPluginDefinition(moduleExport);
3379
- if (!indicatorPluginDefinition) {
3380
- issues.push(
3381
- `${moduleName}: export { indicatorEntries } is missing`
3382
- );
3383
- } else {
3384
- (0, import_indicators.registerIndicatorEntries)(
3385
- indicatorPluginDefinition.indicatorEntries,
3386
- moduleName,
3387
- projectRoot
3388
- );
3389
- }
3390
- }
3391
- if (!strategySet.has(moduleName) && !indicatorSet.has(moduleName)) {
3392
- issues.push(`${moduleName}: plugin is not declared in config`);
3393
- }
3394
- } catch (error) {
3395
- issues.push(`${moduleName}: failed to import: ${String(error)}`);
3396
- }
3397
- }
3398
- if (issues.length > 0) {
3399
- throw new Error(
3400
- ["Invalid TradeJS plugin catalog:", ...issues].join("\n")
3401
- );
3402
- }
3403
- })();
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
+ };
3404
3943
  }
3405
- 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
+ };
3406
3973
  };
3407
- var getStrategyCreator = async (name, cwd = getTradejsProjectCwd(), moduleName) => {
3408
- if (moduleName?.trim()) {
3409
- await ensureStrategyPluginsLoaded(cwd);
3410
- 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
+ };
3411
3996
  }
3412
- await ensureStrategyPluginsLoaded(cwd);
3413
- const { state } = getStrategyRegistryState(cwd);
3414
- 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
+ };
3415
4022
  };
3416
- var getStrategyEntry = async (name, cwd = getTradejsProjectCwd(), moduleName) => {
3417
- if (moduleName?.trim()) {
3418
- await ensureStrategyPluginsLoaded(cwd);
3419
- 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
+ };
3420
4042
  }
3421
- await ensureStrategyPluginsLoaded(cwd);
3422
- const { state } = getStrategyRegistryState(cwd);
3423
- 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
+ };
3424
4061
  };
3425
- var getStrategyPluginSource = async (name, cwd = getTradejsProjectCwd(), moduleName) => {
3426
- if (moduleName?.trim()) {
3427
- await ensureStrategyPluginsLoaded(cwd);
3428
- 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
+ };
3429
4079
  }
3430
- await ensureStrategyPluginsLoaded(cwd);
3431
- const { state } = getStrategyRegistryState(cwd);
3432
- 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
+ };
3433
4092
  };
3434
- var getStrategyManifest = (name, cwd = getTradejsProjectCwd()) => {
3435
- if (!name) {
3436
- 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
+ };
3437
4116
  }
3438
- const { state } = getStrategyRegistryState(cwd);
3439
- 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
+ };
3440
4169
  };
3441
- var strategies = new Proxy(
3442
- {},
3443
- {
3444
- get: (_target, property) => {
3445
- if (typeof property !== "string") {
3446
- return void 0;
3447
- }
3448
- return getStrategyRegistryState().state.strategyCreators.get(property);
3449
- },
3450
- ownKeys: () => {
3451
- return [...getStrategyRegistryState().state.strategyCreators.keys()];
3452
- },
3453
- getOwnPropertyDescriptor: () => ({
3454
- enumerable: true,
3455
- configurable: true
3456
- })
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)
3457
4190
  }
3458
- );
4191
+ });
3459
4192
 
3460
4193
  // src/strategy/policyProfiles.ts
3461
4194
  var profileMatches = (profile, universe, assetClass) => {
@@ -3557,12 +4290,61 @@ var buildBaseAiPayload = (signal) => {
3557
4290
  };
3558
4291
  };
3559
4292
  var defaultAiAdapter = {};
4293
+ var tickerProfileFeatureIds = new Set(
4294
+ import_aiFeatures.TICKER_PROFILE_AI_FEATURES.map(({ id }) => id)
4295
+ );
3560
4296
  var getStrategyAiAdapter = (strategy, profileId) => getStrategyProfileAiAdapter(getStrategyManifest(strategy), profileId) ?? defaultAiAdapter;
3561
4297
  var getSignalAiAdapter = (signal) => getStrategyAiAdapter(signal.strategy, signal.policyProfileId);
3562
4298
  var buildAiPayloadByStrategy = (signal) => {
3563
4299
  const basePayload = buildBaseAiPayload(signal);
3564
4300
  const adapter = getSignalAiAdapter(signal);
3565
- 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
+ };
3566
4348
  };
3567
4349
  var buildAiSystemPromptAddonByStrategy = (signal) => getSignalAiAdapter(signal).buildSystemPromptAddon?.({ signal }) ?? "";
3568
4350
  var buildAiHumanPromptAddonByStrategy = (signal, payload) => getSignalAiAdapter(signal).buildHumanPromptAddon?.({
@@ -3972,356 +4754,108 @@ var runAiPrompt = async ({ systemPrompt, humanPrompt }, options = {}) => {
3972
4754
  userName: options.userName,
3973
4755
  model: options.model,
3974
4756
  messages: [
3975
- { role: "system", content: systemPrompt },
3976
- {
3977
- role: "system",
3978
- content: `Write all user-visible text fields in ${responseLanguage}. Keep field names and JSON syntax unchanged.`
3979
- },
3980
- { role: "user", content: humanPrompt }
3981
- ]
3982
- });
3983
- const parsed = parseAIResponse(response.content);
3984
- const normalized = normalizeAnalysis(parsed);
3985
- if (!options.signal) {
3986
- return normalized;
3987
- }
3988
- return postProcessAiAnalysisByStrategy(
3989
- options.signal,
3990
- normalized,
3991
- options.payload
3992
- );
3993
- };
3994
- var runAiPromptLocal = async (signal, options = {}) => {
3995
- const payload = options.payload ?? buildAiPayload(signal);
3996
- const gateContext = getDeterministicAiGateContext(payload);
3997
- const signalDirection = getSignalDirection(signal);
3998
- const deterministicQuality = getDeterministicQuality(gateContext);
3999
- const approvalAllowedNow = typeof gateContext?.approvalAllowedNow === "boolean" ? gateContext.approvalAllowedNow : deterministicQuality >= 4;
4000
- return postProcessLocalAiAnalysisByStrategy(
4001
- signal,
4002
- {
4003
- direction: approvalAllowedNow ? signalDirection : null,
4004
- quality: deterministicQuality,
4005
- needRetest: !approvalAllowedNow,
4006
- retestPrice: null,
4007
- takeProfitPrice: approvalAllowedNow ? signal.prices?.takeProfitPrice ?? null : null,
4008
- stopLossPrice: approvalAllowedNow ? signal.prices?.stopLossPrice ?? null : null
4009
- },
4010
- payload
4011
- );
4012
- };
4013
- var askAI = async (signal, options = {}) => {
4014
- const { symbol } = signal;
4015
- const payload = buildAiPayload(signal);
4016
- const content = await runAiPrompt(
4017
- {
4018
- systemPrompt: buildAiSystemPrompt(signal),
4019
- humanPrompt: buildAiHumanPrompt(signal, payload)
4020
- },
4021
- {
4022
- ...options,
4023
- signal,
4024
- payload
4025
- }
4026
- );
4027
- await (0, import_redis2.setData)(import_redis2.redisKeys.analysis(symbol, signal.signalId), content);
4028
- return content;
4029
- };
4030
-
4031
- // src/strategyAdapters/ml.ts
4032
- var defaultMlAdapter = {
4033
- normalizeStrategyConfig: (strategyConfig) => strategyConfig
4034
- };
4035
- var getStrategyMlAdapter = (strategy, profileId) => {
4036
- const strategyAdapter = getStrategyProfileMlAdapter(
4037
- getStrategyManifest(strategy),
4038
- profileId
4039
- );
4040
- if (!strategyAdapter) return defaultMlAdapter;
4041
- return {
4042
- ...defaultMlAdapter,
4043
- ...strategyAdapter
4044
- };
4045
- };
4046
-
4047
- // src/mlPayload.ts
4048
- var normalizeStrategyConfig = (strategyConfig, strategyName, profileId) => {
4049
- return getStrategyMlAdapter(
4050
- strategyName,
4051
- profileId
4052
- ).normalizeStrategyConfig?.(strategyConfig);
4053
- };
4054
- var buildMlPayload = (payload) => {
4055
- const strategyName = payload.signal?.strategy ?? payload.context?.strategyName;
4056
- const profileId = payload.signal?.policyProfileId;
4057
- const mlAdapter = getStrategyMlAdapter(strategyName, profileId);
4058
- const normalizedSignal = mlAdapter.normalizeSignal?.(payload.signal) ?? payload.signal;
4059
- const nextSignal = {
4060
- ...normalizedSignal,
4061
- indicators: {
4062
- ...normalizedSignal?.indicators ?? {}
4063
- }
4064
- };
4065
- const nextContext = payload.context ? {
4066
- ...payload.context,
4067
- strategyConfig: normalizeStrategyConfig(
4068
- payload.context.strategyConfig,
4069
- strategyName,
4070
- profileId
4071
- )
4072
- } : void 0;
4073
- return {
4074
- signal: nextSignal,
4075
- context: nextContext
4076
- };
4077
- };
4078
-
4079
- // src/runtimeJournal.ts
4080
- var import_node_crypto = require("crypto");
4081
- var import_constants3 = require("@tradejs/core/constants");
4082
- var import_time3 = require("@tradejs/core/time");
4083
- var import_trade = require("@tradejs/core/trade");
4084
- var import_logger6 = require("@tradejs/infra/logger");
4085
- var import_redis3 = require("@tradejs/infra/redis");
4086
- var now = () => Date.now();
4087
- var toRandomOrderSuffix = () => (0, import_node_crypto.randomUUID)().replace(/-/g, "").slice(0, 12).toLowerCase();
4088
- var calculateClosedPnl = ({
4089
- direction,
4090
- entryPrice,
4091
- exitPrice,
4092
- qty
4093
- }) => {
4094
- const pnl = direction === "LONG" ? (exitPrice - entryPrice) * qty : (entryPrice - exitPrice) * qty;
4095
- return Number.isFinite(pnl) ? pnl : null;
4096
- };
4097
- var createRuntimeOrderId = (strategy) => {
4098
- const prefix = (0, import_trade.createRuntimeOrderLinkPrefix)(strategy);
4099
- if (prefix === "tjs-") {
4100
- return `tjs-${(0, import_node_crypto.randomUUID)().replace(/-/g, "").slice(0, 24).toLowerCase()}`;
4101
- }
4102
- return `${prefix}${toRandomOrderSuffix()}`;
4103
- };
4104
- var recordRuntimeTradeOpen = async (params) => {
4105
- const { userName } = params;
4106
- if (!userName) {
4107
- return null;
4108
- }
4109
- const record = {
4110
- ...params,
4111
- entryCount: 1,
4112
- lastEntryPrice: params.entryPrice,
4113
- lastEntryQty: params.qty,
4114
- lastEntryTimestamp: params.entryTimestamp,
4115
- status: "active",
4116
- currentPrice: params.entryPrice,
4117
- currentPnl: 0,
4118
- closedPnl: null,
4119
- exitPrice: null,
4120
- exitTimestamp: null,
4121
- lastSyncedAt: now()
4122
- };
4123
- const dayKey = (0, import_time3.getRuntimeStorageDayKey)(record.entryTimestamp);
4124
- const runtimeScopeId = record.deploymentId ?? record.accountId;
4125
- try {
4126
- await Promise.all([
4127
- (0, import_redis3.setData)(import_redis3.redisKeys.runtimeTrade(userName, record.orderId), record, {
4128
- expire: 0
4129
- }),
4130
- (0, import_redis3.setHashJsonField)(
4131
- import_redis3.redisKeys.runtimeTradeBucket(userName, dayKey),
4132
- record.orderId,
4133
- record,
4134
- { expire: 0 }
4135
- ),
4136
- (0, import_redis3.setData)(
4137
- import_redis3.redisKeys.runtimeActiveTrade(userName, record.symbol, runtimeScopeId),
4138
- { orderId: record.orderId },
4139
- { expire: 0 }
4140
- )
4141
- ]);
4142
- } catch (error) {
4143
- import_logger6.logger.error(
4144
- "runtime trade open journal failed: %s %s",
4145
- record.symbol,
4146
- error?.message || String(error)
4147
- );
4148
- }
4149
- return record;
4150
- };
4151
- var recordRuntimeTradeIncrease = async (params) => {
4152
- const {
4153
- userName,
4154
- strategy,
4155
- symbol,
4156
- direction,
4157
- resultingQty,
4158
- resultingEntryPrice,
4159
- addedQty,
4160
- addedEntryPrice,
4161
- entryTimestamp,
4162
- fee,
4163
- accountId,
4164
- deploymentId
4165
- } = params;
4166
- if (!userName) {
4167
- return null;
4168
- }
4169
- const existing = await getActiveRuntimeTrade({
4170
- userName,
4171
- symbol,
4172
- accountId,
4173
- deploymentId
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
+ ]
4174
4764
  });
4175
- if (!existing || existing.strategy !== strategy || existing.direction !== direction) {
4176
- return null;
4177
- }
4178
- const addedFee = typeof fee === "number" && Number.isFinite(fee) ? fee : 0;
4179
- const openFee = (existing.openFee ?? existing.fee ?? 0) + addedFee;
4180
- const totalFee = (existing.totalFee ?? existing.fee ?? 0) + addedFee;
4181
- const next = {
4182
- ...existing,
4183
- ...params.orderLeverage ? { orderLeverage: params.orderLeverage } : {},
4184
- qty: resultingQty,
4185
- entryPrice: resultingEntryPrice,
4186
- entryCount: Math.max(1, existing.entryCount ?? 1) + 1,
4187
- lastEntryPrice: addedEntryPrice,
4188
- lastEntryQty: addedQty,
4189
- lastEntryTimestamp: entryTimestamp,
4190
- currentPrice: resultingEntryPrice,
4191
- currentPnl: 0,
4192
- fee: openFee,
4193
- openFee,
4194
- totalFee,
4195
- lastSyncedAt: now()
4196
- };
4197
- const dayKey = (0, import_time3.getRuntimeStorageDayKey)(existing.entryTimestamp);
4198
- try {
4199
- await Promise.all([
4200
- (0, import_redis3.setData)(import_redis3.redisKeys.runtimeTrade(userName, existing.orderId), next, {
4201
- expire: 0
4202
- }),
4203
- (0, import_redis3.setHashJsonField)(
4204
- import_redis3.redisKeys.runtimeTradeBucket(userName, dayKey),
4205
- existing.orderId,
4206
- next,
4207
- { expire: 0 }
4208
- )
4209
- ]);
4210
- } catch (error) {
4211
- import_logger6.logger.error(
4212
- "runtime trade increase journal failed: %s %s",
4213
- symbol,
4214
- error?.message || String(error)
4215
- );
4765
+ const parsed = parseAIResponse(response.content);
4766
+ const normalized = normalizeAnalysis(parsed);
4767
+ if (!options.signal) {
4768
+ return normalized;
4216
4769
  }
4217
- return next;
4770
+ return postProcessAiAnalysisByStrategy(
4771
+ options.signal,
4772
+ normalized,
4773
+ options.payload
4774
+ );
4218
4775
  };
4219
- var getActiveRuntimeTrade = async (params) => {
4220
- const { userName, symbol, accountId, deploymentId } = params;
4221
- if (!userName) {
4222
- return null;
4223
- }
4224
- const activeRef = await (0, import_redis3.getData)(
4225
- import_redis3.redisKeys.runtimeActiveTrade(userName, symbol, deploymentId ?? accountId),
4226
- 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
4227
4793
  );
4228
- const orderId = String(activeRef?.orderId || "").trim();
4229
- if (!orderId) {
4230
- return null;
4231
- }
4232
- const existing = await (0, import_redis3.getData)(
4233
- import_redis3.redisKeys.runtimeTrade(userName, orderId),
4234
- 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
+ }
4235
4808
  );
4236
- if (!existing || existing.status !== "active") {
4237
- await (0, import_redis3.delKey)(
4238
- import_redis3.redisKeys.runtimeActiveTrade(userName, symbol, deploymentId ?? accountId)
4239
- );
4240
- return null;
4241
- }
4242
- return existing;
4809
+ await (0, import_redis4.setData)(import_redis4.redisKeys.analysis(symbol, signal.signalId), content);
4810
+ return content;
4243
4811
  };
4244
- var markRuntimeTradeClosed = async (params) => {
4245
- const {
4246
- userName,
4247
- symbol,
4248
- strategy,
4249
- exitPrice,
4250
- exitTimestamp,
4251
- closedPnl,
4252
- exitType,
4253
- accountId,
4254
- deploymentId
4255
- } = params;
4256
- if (!userName) {
4257
- return null;
4258
- }
4259
- const existing = await getActiveRuntimeTrade({
4260
- userName,
4261
- symbol,
4262
- accountId,
4263
- deploymentId
4264
- });
4265
- if (!existing) {
4266
- return null;
4267
- }
4268
- const orderId = existing.orderId;
4269
- if (strategy && existing.strategy !== strategy) {
4270
- return null;
4271
- }
4272
- const resolvedExitPrice = typeof exitPrice === "number" && Number.isFinite(exitPrice) ? exitPrice : existing.currentPrice ?? existing.entryPrice;
4273
- const resolvedClosedPnl = typeof closedPnl === "number" && Number.isFinite(closedPnl) ? closedPnl : typeof resolvedExitPrice === "number" && Number.isFinite(resolvedExitPrice) ? calculateClosedPnl({
4274
- direction: existing.direction,
4275
- entryPrice: existing.entryPrice,
4276
- exitPrice: resolvedExitPrice,
4277
- qty: existing.qty
4278
- }) : existing.closedPnl ?? existing.currentPnl ?? null;
4279
- const next = {
4280
- ...existing,
4281
- status: "closed",
4282
- currentPrice: resolvedExitPrice,
4283
- currentPnl: resolvedClosedPnl,
4284
- closedPnl: resolvedClosedPnl,
4285
- exitPrice: resolvedExitPrice,
4286
- exitTimestamp: typeof exitTimestamp === "number" && Number.isFinite(exitTimestamp) ? exitTimestamp : now(),
4287
- exitType: exitType ?? existing.exitType ?? null,
4288
- 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
4289
4858
  };
4290
- const dayKey = (0, import_time3.getRuntimeStorageDayKey)(existing.entryTimestamp);
4291
- const closeDayKey = (0, import_time3.getRuntimeStorageDayKey)(next.exitTimestamp);
4292
- try {
4293
- await Promise.all([
4294
- (0, import_redis3.setData)(import_redis3.redisKeys.runtimeTrade(userName, orderId), next, {
4295
- expire: import_constants3.TTL_1M
4296
- }),
4297
- (0, import_redis3.setHashJsonField)(
4298
- import_redis3.redisKeys.runtimeTradeBucket(userName, dayKey),
4299
- orderId,
4300
- next,
4301
- { expire: import_constants3.TTL_1M }
4302
- ),
4303
- (0, import_redis3.setHashJsonField)(
4304
- import_redis3.redisKeys.runtimeClosedTradeBucket(userName, closeDayKey),
4305
- orderId,
4306
- next,
4307
- { expire: import_constants3.TTL_1M }
4308
- ),
4309
- (0, import_redis3.delKey)(
4310
- import_redis3.redisKeys.runtimeActiveTrade(
4311
- userName,
4312
- symbol,
4313
- deploymentId ?? accountId
4314
- )
4315
- )
4316
- ]);
4317
- } catch (error) {
4318
- import_logger6.logger.error(
4319
- "runtime trade close journal failed: %s %s",
4320
- symbol,
4321
- error?.message || String(error)
4322
- );
4323
- }
4324
- return next;
4325
4859
  };
4326
4860
 
4327
4861
  // src/strategyHelpers/marketContextStages.ts
@@ -4333,7 +4867,7 @@ var import_logger7 = require("@tradejs/infra/logger");
4333
4867
  var import_strategies = require("@tradejs/core/strategies");
4334
4868
 
4335
4869
  // src/binanceBreadthUniverses.ts
4336
- var import_node_crypto2 = require("crypto");
4870
+ var import_node_crypto3 = require("crypto");
4337
4871
 
4338
4872
  // src/config/binanceBreadthUniverses.json
4339
4873
  var binanceBreadthUniverses_default = {
@@ -4572,7 +5106,7 @@ var BINANCE_BREADTH_UNIVERSE_KEYS = [
4572
5106
  "top50",
4573
5107
  "top100"
4574
5108
  ];
4575
- 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);
4576
5110
  var normalizeSymbols = (symbols) => symbols.map((symbol) => symbol.trim().toUpperCase()).filter(Boolean);
4577
5111
  var buildBinanceBreadthUniverseSnapshot = ({
4578
5112
  rankedSymbols,
@@ -5596,7 +6130,7 @@ var enrichSignalWithCoinMarketCapContext = async (params) => {
5596
6130
  };
5597
6131
 
5598
6132
  // src/strategyHelpers/derivativesContext.ts
5599
- var import_indicators2 = require("@tradejs/core/indicators");
6133
+ var import_indicators3 = require("@tradejs/core/indicators");
5600
6134
  var import_data3 = require("@tradejs/core/data");
5601
6135
  var import_strategies3 = require("@tradejs/core/strategies");
5602
6136
  var import_constants4 = require("@tradejs/core/constants");
@@ -5630,7 +6164,7 @@ var parseLookbackMs = () => {
5630
6164
  };
5631
6165
  var withHourlyFallbackRows = (rowsByInterval) => ({
5632
6166
  "15m": rowsByInterval["15m"] ?? [],
5633
- "1h": (0, import_indicators2.buildCoinalyzeHourlyRowsWithFallback)({
6167
+ "1h": (0, import_indicators3.buildCoinalyzeHourlyRowsWithFallback)({
5634
6168
  rows15m: rowsByInterval["15m"],
5635
6169
  fallbackRows1h: rowsByInterval["1h"]
5636
6170
  })
@@ -5758,7 +6292,7 @@ var enrichSignalWithDerivativesContext = async (params) => {
5758
6292
  const targetSymbol = normalizeSymbol(signal.symbol);
5759
6293
  const lookbackMs = parseLookbackMs();
5760
6294
  const decisionTimeMs = signal.timestamp + (0, import_data3.intervalToMs)(signal.interval);
5761
- const derivativesEndMs = (0, import_indicators2.getLastClosedDerivativesBarStartMs)(
6295
+ const derivativesEndMs = (0, import_indicators3.getLastClosedDerivativesBarStartMs)(
5762
6296
  decisionTimeMs,
5763
6297
  "15m"
5764
6298
  );
@@ -5773,7 +6307,7 @@ var enrichSignalWithDerivativesContext = async (params) => {
5773
6307
  });
5774
6308
  return [
5775
6309
  symbol,
5776
- (0, import_indicators2.buildDerivativesContext)({
6310
+ (0, import_indicators3.buildDerivativesContext)({
5777
6311
  symbol,
5778
6312
  direction: signal.direction,
5779
6313
  timestamp: derivativesEndMs,
@@ -5800,7 +6334,7 @@ var enrichSignalWithDerivativesContext = async (params) => {
5800
6334
  lookbackMs,
5801
6335
  ...params.abortSignal ? { signal: params.abortSignal } : {}
5802
6336
  });
5803
- const context = (0, import_indicators2.buildDerivativesContext)({
6337
+ const context = (0, import_indicators3.buildDerivativesContext)({
5804
6338
  symbol: targetSymbol,
5805
6339
  direction: signal.direction,
5806
6340
  timestamp: derivativesEndMs,
@@ -5848,7 +6382,7 @@ var import_hyperliquidWhales2 = require("@tradejs/infra/timescale/hyperliquidWha
5848
6382
  var import_logger10 = require("@tradejs/infra/logger");
5849
6383
 
5850
6384
  // src/hyperliquidWhaleUniverse.ts
5851
- var import_node_crypto3 = require("crypto");
6385
+ var import_node_crypto4 = require("crypto");
5852
6386
 
5853
6387
  // src/config/hyperliquidPerpUniverse.json
5854
6388
  var hyperliquidPerpUniverse_default = {
@@ -6020,7 +6554,7 @@ var hyperliquidWhales_default = {
6020
6554
  };
6021
6555
 
6022
6556
  // src/hyperliquidWhaleUniverse.ts
6023
- 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);
6024
6558
  var validateUnique = (values, label) => {
6025
6559
  if (new Set(values).size !== values.length) {
6026
6560
  throw new Error(`Hyperliquid ${label} snapshot contains duplicates`);
@@ -6507,6 +7041,7 @@ var enrichSignalWithMarketContextStages = async ({
6507
7041
  })
6508
7042
  });
6509
7043
  }
7044
+ await enrichSignalWithTickerProfile(signal);
6510
7045
  const results = [];
6511
7046
  for (const stage of stages) {
6512
7047
  if (abortSignal?.aborted) break;
@@ -6554,12 +7089,12 @@ var findReplayAiAnalysis = ({
6554
7089
  direction,
6555
7090
  ai
6556
7091
  }) => {
6557
- const snapshots = ai?.replayAnalyses;
6558
- if (!Array.isArray(snapshots) || !snapshots.length) {
7092
+ const snapshots2 = ai?.replayAnalyses;
7093
+ if (!Array.isArray(snapshots2) || !snapshots2.length) {
6559
7094
  return void 0;
6560
7095
  }
6561
7096
  let best = null;
6562
- for (const snapshot2 of snapshots) {
7097
+ for (const snapshot2 of snapshots2) {
6563
7098
  if (snapshot2.symbol !== signal.symbol || snapshot2.direction !== direction || snapshot2.strategy && snapshot2.strategy !== signal.strategy) {
6564
7099
  continue;
6565
7100
  }
@@ -6719,7 +7254,9 @@ var executeEntryOrder = async ({
6719
7254
  beforePlaceOrder,
6720
7255
  recordRuntimeTrade = true,
6721
7256
  leverage,
6722
- executionLimits
7257
+ executionLimits,
7258
+ markPortfolioSubmitted,
7259
+ rejectPortfolioSubmission
6723
7260
  }) => {
6724
7261
  await beforePlaceOrder?.();
6725
7262
  const previousPosition = positionIntent === "increase" ? await connector.getPosition(symbol) : null;
@@ -6752,12 +7289,12 @@ var executeEntryOrder = async ({
6752
7289
  if (executionLimits?.maxLossValue != null) {
6753
7290
  signal.riskDecision = {
6754
7291
  stage: "order_plan",
6755
- status: "approved",
6756
- reason: "PLANNED_LOSS_WITHIN_MAX_LOSS_VALUE",
7292
+ status: economics2.riskExceeded ? "warning" : "approved",
7293
+ reason: economics2.riskExceeded ? "ENTRY_RISK_EXCEEDS_MAX_LOSS_AT_SUBMISSION" : "PLANNED_LOSS_WITHIN_MAX_LOSS_VALUE",
6757
7294
  plannedLossValue: economics2.plannedLossValue,
6758
7295
  maxLossValue: executionLimits.maxLossValue,
6759
7296
  limitUtilization: economics2.plannedLossValue / executionLimits.maxLossValue,
6760
- enforced: true
7297
+ enforced: false
6761
7298
  };
6762
7299
  }
6763
7300
  }
@@ -6792,7 +7329,13 @@ var executeEntryOrder = async ({
6792
7329
  ...typeof leverage === "number" && Number.isFinite(leverage) ? { leverage } : {},
6793
7330
  orderId,
6794
7331
  signal,
6795
- ...beforeSubmit ? { beforeSubmit } : {}
7332
+ ...rejectPortfolioSubmission ? { onRejected: rejectPortfolioSubmission } : {},
7333
+ ...beforeSubmit || markPortfolioSubmitted ? {
7334
+ beforeSubmit: async (context) => {
7335
+ await beforeSubmit?.(context);
7336
+ await markPortfolioSubmitted?.(context.qty);
7337
+ }
7338
+ } : {}
6796
7339
  });
6797
7340
  const orderAckTime = Date.now();
6798
7341
  const placedQty = typeof signal.orderQty === "number" && Number.isFinite(signal.orderQty) && signal.orderQty > 0 ? signal.orderQty : qty;
@@ -7780,7 +8323,7 @@ var handleProtectDecision = async ({
7780
8323
  }
7781
8324
  return decision.code;
7782
8325
  };
7783
- var executeEntryDecision = async ({
8326
+ var executeAdmittedEntryDecision = async ({
7784
8327
  connector,
7785
8328
  symbol,
7786
8329
  decision,
@@ -7794,7 +8337,9 @@ var executeEntryDecision = async ({
7794
8337
  ai,
7795
8338
  recordRuntimeJournal,
7796
8339
  invokeStageHooks,
7797
- notifyRuntimeError
8340
+ notifyRuntimeError,
8341
+ markPortfolioSubmitted,
8342
+ rejectPortfolioSubmission
7798
8343
  }) => {
7799
8344
  const signal = decision.signal;
7800
8345
  const beforePlaceOrder = async () => {
@@ -7841,6 +8386,8 @@ var executeEntryDecision = async ({
7841
8386
  ...Number.isFinite(Number(hookCtx.strategyConfig.LEVERAGE)) ? { leverage: Number(hookCtx.strategyConfig.LEVERAGE) } : {},
7842
8387
  signal,
7843
8388
  beforePlaceOrder,
8389
+ markPortfolioSubmitted,
8390
+ rejectPortfolioSubmission,
7844
8391
  recordRuntimeTrade: recordRuntimeJournal,
7845
8392
  executionLimits: resolveEntryExecutionLimits(
7846
8393
  hookCtx.strategyConfig,
@@ -7981,6 +8528,37 @@ var executeEntryDecision = async ({
7981
8528
  }
7982
8529
  return signal ?? decision.code;
7983
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
+ }
7984
8562
 
7985
8563
  // src/strategyRuntime.ts
7986
8564
  var cloneWithPropertyDescriptors = (value) => Object.create(
@@ -8008,6 +8586,7 @@ var createStrategyRuntime = ({
8008
8586
  };
8009
8587
  const creator = async ({
8010
8588
  userName,
8589
+ connectorName,
8011
8590
  config: baseConfig,
8012
8591
  symbol,
8013
8592
  universe: requestedUniverse,
@@ -8046,6 +8625,17 @@ var createStrategyRuntime = ({
8046
8625
  const projectConfig = await loadTradejsConfig(projectRoot);
8047
8626
  const projectHooks = projectConfig.hooks;
8048
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;
8049
8639
  const researchPolicyValue = baseConfig?.RESEARCH_ENTRY_POLICY ?? config.RESEARCH_ENTRY_POLICY;
8050
8640
  const researchEntryPolicy = researchPolicyValue == null ? void 0 : parseResearchEntryPolicy(researchPolicyValue);
8051
8641
  if (researchEntryPolicy && (env !== "BACKTEST" || Reflect.get(connector, "__tradejsTestConnector") !== true))
@@ -8083,6 +8673,9 @@ var createStrategyRuntime = ({
8083
8673
  });
8084
8674
  const strategyPolicyProfile = getPolicyProfile();
8085
8675
  const indicatorPeriods = (0, import_strategies6.buildDefaultIndicatorPeriods)(config);
8676
+ const strategyConfig = { ...config };
8677
+ delete strategyConfig.RECORD_RUNTIME_TRADES;
8678
+ delete strategyConfig.SIMULATE_ORDERS;
8086
8679
  const hookBase = {
8087
8680
  connector,
8088
8681
  strategyName,
@@ -8093,7 +8686,7 @@ var createStrategyRuntime = ({
8093
8686
  accountId,
8094
8687
  deploymentId,
8095
8688
  policyProfileId: strategyPolicyProfile?.id ?? requestedPolicyProfileId,
8096
- strategyConfig: config,
8689
+ strategyConfig,
8097
8690
  env,
8098
8691
  isConfigFromBacktest
8099
8692
  };
@@ -8161,7 +8754,7 @@ var createStrategyRuntime = ({
8161
8754
  );
8162
8755
  }
8163
8756
  };
8164
- const invokeHook = async (stage, hook, params, errorContext = {}) => {
8757
+ const invokeHook = async (stage, hook, params, errorContext = {}, failureResult) => {
8165
8758
  if (!hook) {
8166
8759
  return void 0;
8167
8760
  }
@@ -8181,17 +8774,18 @@ var createStrategyRuntime = ({
8181
8774
  entry: errorContext.entry,
8182
8775
  market: errorContext.market
8183
8776
  });
8184
- return void 0;
8777
+ return failureResult;
8185
8778
  }
8186
8779
  };
8187
- const invokeProjectHooks = async (stage, params, errorContext = {}) => {
8780
+ const invokeProjectHooks = async (stage, params, errorContext = {}, failureResult) => {
8188
8781
  const results = [];
8189
8782
  for (const hook of getProjectHookList(stage)) {
8190
8783
  const result = await invokeHook(
8191
8784
  stage,
8192
8785
  hook,
8193
8786
  params,
8194
- errorContext
8787
+ errorContext,
8788
+ failureResult
8195
8789
  );
8196
8790
  if (result !== void 0) {
8197
8791
  results.push(result);
@@ -8206,10 +8800,12 @@ var createStrategyRuntime = ({
8206
8800
  return invokeHook(stage, hook, params, errorContext);
8207
8801
  };
8208
8802
  const invokeGateHooks = async (stage, hook, params, errorContext = {}) => {
8803
+ const failureResult = stage === "beforeEntryGate" ? { allow: false, reason: "ERROR" } : void 0;
8209
8804
  const projectResults = await invokeProjectHooks(
8210
8805
  stage,
8211
8806
  params,
8212
- errorContext
8807
+ errorContext,
8808
+ failureResult
8213
8809
  );
8214
8810
  const projectBlock = projectResults.find(
8215
8811
  (result) => result?.allow === false
@@ -8221,7 +8817,8 @@ var createStrategyRuntime = ({
8221
8817
  stage,
8222
8818
  hook,
8223
8819
  params,
8224
- errorContext
8820
+ errorContext,
8821
+ failureResult
8225
8822
  );
8226
8823
  };
8227
8824
  const applyProjectAfterCoreDecisionHooks = async ({
@@ -8337,6 +8934,20 @@ var createStrategyRuntime = ({
8337
8934
  interval: decisionInterval
8338
8935
  }) => {
8339
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
+ }
8340
8951
  if (!coreContextRequirements.has("hyperliquidWhales")) {
8341
8952
  return baseContext;
8342
8953
  }
@@ -8367,11 +8978,8 @@ var createStrategyRuntime = ({
8367
8978
  return enrichedBaseContext;
8368
8979
  }
8369
8980
  });
8370
- const coreConfig = { ...config };
8371
- delete coreConfig.RECORD_RUNTIME_TRADES;
8372
- delete coreConfig.SIMULATE_ORDERS;
8373
8981
  const core = await createCore({
8374
- config: coreConfig,
8982
+ config: strategyConfig,
8375
8983
  data,
8376
8984
  strategyApi,
8377
8985
  indicatorsState
@@ -8507,6 +9115,8 @@ var createStrategyRuntime = ({
8507
9115
  runtime: pending2.runtime
8508
9116
  });
8509
9117
  return executeEntryDecision({
9118
+ portfolio,
9119
+ portfolioTime: env === "BACKTEST" || env === "PARITY" ? candle.timestamp : Date.now(),
8510
9120
  connector,
8511
9121
  symbol,
8512
9122
  decision: pending2.decision,
@@ -8524,6 +9134,10 @@ var createStrategyRuntime = ({
8524
9134
  });
8525
9135
  };
8526
9136
  const runWithDecisionOverride = async (candle, btcCandle, options = {}) => {
9137
+ if (portfolioContext)
9138
+ await portfolioContext(
9139
+ env === "BACKTEST" || env === "PARITY" ? candle.timestamp : Date.now()
9140
+ );
8527
9141
  appendCurrentMarketData(candle, btcCandle, options.ethCandle);
8528
9142
  const ethCandle = resolveEthCandle(candle, options.ethCandle);
8529
9143
  indicatorsState.setCurrentBar(candle, btcCandle, ethCandle);
@@ -8694,6 +9308,13 @@ var createStrategyRuntime = ({
8694
9308
  });
8695
9309
  const signal = decision.signal;
8696
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
+ };
8697
9318
  if (runtimeLineage) signal.runtimeLineage = runtimeLineage;
8698
9319
  if (universe) signal.universe = universe;
8699
9320
  if (assetClass) signal.assetClass = assetClass;
@@ -8933,6 +9554,8 @@ var createStrategyRuntime = ({
8933
9554
  });
8934
9555
  }
8935
9556
  return executeEntryDecision({
9557
+ portfolio,
9558
+ portfolioTime: env === "BACKTEST" || env === "PARITY" ? candle.timestamp : Date.now(),
8936
9559
  connector,
8937
9560
  symbol,
8938
9561
  decision,
@@ -9044,7 +9667,7 @@ var normalizeForCanonicalJson = (value) => {
9044
9667
  }
9045
9668
  return value;
9046
9669
  };
9047
- 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)}`;
9048
9671
  var computeStrategyRevision = ({
9049
9672
  strategyName,
9050
9673
  strategyPackage,
@@ -9629,18 +10252,18 @@ var loadRuntimeTrades = async (userName, {
9629
10252
  const [bucketTradeGroups, legacyKeys] = await Promise.all([
9630
10253
  Promise.all(
9631
10254
  dayKeys.map(
9632
- (dayKey) => (0, import_redis4.getHashJsonValues)(
9633
- import_redis4.redisKeys.runtimeTradeBucket(userName, dayKey)
10255
+ (dayKey) => (0, import_redis5.getHashJsonValues)(
10256
+ import_redis5.redisKeys.runtimeTradeBucket(userName, dayKey)
9634
10257
  )
9635
10258
  )
9636
10259
  ),
9637
- (0, import_redis4.getKeys)(import_redis4.redisKeys.runtimeTrades(userName))
10260
+ (0, import_redis5.getKeys)(import_redis5.redisKeys.runtimeTrades(userName))
9638
10261
  ]);
9639
10262
  const legacyTradeKeys = legacyKeys.filter(
9640
- (key) => !key.startsWith(import_redis4.redisKeys.runtimeTradeBuckets(userName))
10263
+ (key) => !key.startsWith(import_redis5.redisKeys.runtimeTradeBuckets(userName))
9641
10264
  );
9642
10265
  const legacyTrades = await Promise.all(
9643
- legacyTradeKeys.map((key) => (0, import_redis4.getData)(key, null))
10266
+ legacyTradeKeys.map((key) => (0, import_redis5.getData)(key, null))
9644
10267
  );
9645
10268
  const dedupedTrades = /* @__PURE__ */ new Map();
9646
10269
  for (const trade of [...legacyTrades, ...bucketTradeGroups.flat()]) {
@@ -9727,8 +10350,8 @@ var loadExchangeRange = async ({
9727
10350
  }
9728
10351
  };
9729
10352
  var loadActiveRuntimeOrderIds = async (userName) => {
9730
- const keys = await (0, import_redis4.getKeys)(import_redis4.redisKeys.runtimeActiveTrades(userName));
9731
- 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)));
9732
10355
  return new Set(
9733
10356
  refs.map(
9734
10357
  (ref) => typeof ref?.orderId === "string" && ref.orderId.trim() ? ref.orderId.trim() : null