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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -784,7 +784,7 @@ var import_time4 = require("@tradejs/core/time");
784
784
  var import_logger15 = require("@tradejs/infra/logger");
785
785
  var import_runtimeDeploymentEvents = require("@tradejs/infra/runtimeDeploymentEvents");
786
786
  var import_tradingAccounts3 = require("@tradejs/infra/tradingAccounts");
787
- var import_redis4 = require("@tradejs/infra/redis");
787
+ var import_redis5 = require("@tradejs/infra/redis");
788
788
 
789
789
  // src/connectorsRegistry.ts
790
790
  var import_logger4 = require("@tradejs/infra/logger");
@@ -2083,1376 +2083,2156 @@ 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
+ let retry = 0;
2229
+ let attemptId = entry.id;
2230
+ let needsReservation = false;
2231
+ try {
2232
+ const value = await send(
2233
+ async (qty) => {
2234
+ if (!Number.isFinite(qty) || qty <= 0)
2235
+ throw new Error("Invalid submitted portfolio quantity");
2236
+ if (needsReservation) {
2237
+ attemptId = `${entry.id.slice(0, 30)}r${++retry}`;
2238
+ const admission = await transact(
2239
+ now2,
2240
+ (s, o) => {
2241
+ const r = (0, import_portfolio.reservePortfolioEntry)(policy, strategy, s, o, {
2242
+ ...entry,
2243
+ id: attemptId,
2244
+ qty,
2245
+ submitted: true,
2246
+ oppositeHandled: false
2247
+ });
2248
+ return { state: r.state, result: r.reason };
2249
+ },
2250
+ true
2251
+ );
2252
+ if (admission.result) throw new Error(admission.result);
2253
+ needsReservation = false;
2254
+ submitted = true;
2255
+ return attemptId;
2256
+ }
2257
+ const update = await updateReservation(now2, attemptId, true, qty);
2258
+ if (!update.result)
2259
+ throw new Error("PORTFOLIO_SUBMISSION_NO_LONGER_ADMITTED");
2260
+ submitted = true;
2261
+ return attemptId;
2262
+ },
2263
+ async (retrying = false) => {
2264
+ await transact(
2265
+ now2,
2266
+ (s) => ({
2267
+ state: {
2268
+ ...s,
2269
+ reservations: s.reservations.filter(
2270
+ (r) => r.id !== attemptId
2271
+ ),
2272
+ completedSignals: {
2273
+ ...Object.fromEntries(
2274
+ Object.entries(s.completedSignals).filter(
2275
+ ([id]) => !retrying || id !== entry.signalId
2276
+ )
2277
+ ),
2278
+ ...retrying ? {} : { [entry.signalId]: now2 }
2279
+ }
2280
+ },
2281
+ result: void 0
2282
+ }),
2283
+ true
2284
+ );
2285
+ submitted = false;
2286
+ needsReservation = retrying;
2287
+ }
2288
+ );
2289
+ if (!submitted) await updateReservation(now2, attemptId, false);
2290
+ else await tick(now2, true);
2291
+ return { value };
2292
+ } catch (error) {
2293
+ if (!submitted) await updateReservation(now2, attemptId, false);
2294
+ throw error;
2295
+ }
2278
2296
  }
2279
2297
  };
2280
- };
2281
- var assessSignalWithJev = async ({
2282
- signal,
2283
- config,
2284
- userName,
2285
- projectRoot,
2286
- strict,
2287
- scope = "runtime"
2298
+ }
2299
+ function createMemoryPortfolioStore() {
2300
+ let state = null;
2301
+ return {
2302
+ read: async () => state === null ? null : structuredClone(state),
2303
+ compare: async (expected, next) => {
2304
+ if ((state?.revision ?? -1) !== (expected?.revision ?? -1)) return false;
2305
+ state = structuredClone(next);
2306
+ return true;
2307
+ }
2308
+ };
2309
+ }
2310
+
2311
+ // src/runtimeJournal.ts
2312
+ var import_node_crypto = require("crypto");
2313
+ var import_constants3 = require("@tradejs/core/constants");
2314
+ var import_time3 = require("@tradejs/core/time");
2315
+ var import_trade = require("@tradejs/core/trade");
2316
+ var import_logger5 = require("@tradejs/infra/logger");
2317
+ var import_redis2 = require("@tradejs/infra/redis");
2318
+ var now = () => Date.now();
2319
+ var toRandomOrderSuffix = () => (0, import_node_crypto.randomUUID)().replace(/-/g, "").slice(0, 12).toLowerCase();
2320
+ var calculateClosedPnl = ({
2321
+ direction,
2322
+ entryPrice,
2323
+ exitPrice,
2324
+ qty
2288
2325
  }) => {
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)
2319
- };
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;
2326
+ const pnl = direction === "LONG" ? (exitPrice - entryPrice) * qty : (entryPrice - exitPrice) * qty;
2327
+ return Number.isFinite(pnl) ? pnl : null;
2340
2328
  };
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");
2351
-
2352
- // src/strategy/entrySubmissionValidation.ts
2353
- 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";
2329
+ var createRuntimeOrderId = (strategy) => {
2330
+ const prefix = (0, import_trade.createRuntimeOrderLinkPrefix)(strategy);
2331
+ if (prefix === "tjs-") {
2332
+ return `tjs-${(0, import_node_crypto.randomUUID)().replace(/-/g, "").slice(0, 24).toLowerCase()}`;
2360
2333
  }
2334
+ return `${prefix}${toRandomOrderSuffix()}`;
2361
2335
  };
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 = ({
2378
- direction,
2379
- price,
2380
- qty,
2381
- stopLossPrice,
2382
- takeProfitPrice,
2383
- limits
2384
- }) => {
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"
2336
+ var recordRuntimeTradeOpen = async (params) => {
2337
+ const { userName } = params;
2338
+ if (!userName) {
2339
+ return null;
2340
+ }
2341
+ const record = {
2342
+ ...params,
2343
+ entryCount: 1,
2344
+ lastEntryPrice: params.entryPrice,
2345
+ lastEntryQty: params.qty,
2346
+ lastEntryTimestamp: params.entryTimestamp,
2347
+ status: "active",
2348
+ currentPrice: params.entryPrice,
2349
+ currentPnl: 0,
2350
+ closedPnl: null,
2351
+ exitPrice: null,
2352
+ exitTimestamp: null,
2353
+ lastSyncedAt: now()
2354
+ };
2355
+ const dayKey = (0, import_time3.getRuntimeStorageDayKey)(record.entryTimestamp);
2356
+ const runtimeScopeId = record.deploymentId ?? record.accountId;
2357
+ try {
2358
+ await Promise.all([
2359
+ (0, import_redis2.setData)(import_redis2.redisKeys.runtimeTrade(userName, record.orderId), record, {
2360
+ expire: 0
2361
+ }),
2362
+ (0, import_redis2.setHashJsonField)(
2363
+ import_redis2.redisKeys.runtimeTradeBucket(userName, dayKey),
2364
+ record.orderId,
2365
+ record,
2366
+ { expire: 0 }
2367
+ ),
2368
+ (0, import_redis2.setData)(
2369
+ import_redis2.redisKeys.runtimeActiveTrade(userName, record.symbol, runtimeScopeId),
2370
+ { orderId: record.orderId },
2371
+ { expire: 0 }
2372
+ )
2373
+ ]);
2374
+ } catch (error) {
2375
+ import_logger5.logger.error(
2376
+ "runtime trade open journal failed: %s %s",
2377
+ record.symbol,
2378
+ error?.message || String(error)
2411
2379
  );
2412
- const riskExceeded = limits.maxLossValue != null && plannedLossValue > limits.maxLossValue * (1 + 1e-9);
2413
- return { netRiskRatio, plannedLossValue, riskExceeded };
2380
+ }
2381
+ return record;
2414
2382
  };
2415
-
2416
- // src/strategy/entrySubmissionValidation.ts
2417
- var MAX_ENTRY_QUOTE_AGE_MS = 5e3;
2418
- var toFiniteNumberOrNull = (value) => typeof value === "number" && Number.isFinite(value) ? value : null;
2419
- var getOrderArrivalSnapshot = async ({
2420
- connector,
2421
- symbol,
2422
- requireTimestamp = false
2423
- }) => {
2424
- if (typeof connector.getTopOfBookTicker !== "function") {
2425
- return {
2426
- arrivalSnapshotTime: Date.now(),
2427
- arrivalSource: "unavailable",
2428
- bid: null,
2429
- ask: null,
2430
- arrivalMid: null,
2431
- spreadBps: null
2432
- };
2383
+ var recordRuntimeTradeIncrease = async (params) => {
2384
+ const {
2385
+ userName,
2386
+ strategy,
2387
+ symbol,
2388
+ direction,
2389
+ resultingQty,
2390
+ resultingEntryPrice,
2391
+ addedQty,
2392
+ addedEntryPrice,
2393
+ entryTimestamp,
2394
+ fee,
2395
+ accountId,
2396
+ deploymentId
2397
+ } = params;
2398
+ if (!userName) {
2399
+ return null;
2400
+ }
2401
+ const existing = await getActiveRuntimeTrade({
2402
+ userName,
2403
+ symbol,
2404
+ accountId,
2405
+ deploymentId
2406
+ });
2407
+ if (!existing || existing.strategy !== strategy || existing.direction !== direction) {
2408
+ return null;
2433
2409
  }
2410
+ const addedFee = typeof fee === "number" && Number.isFinite(fee) ? fee : 0;
2411
+ const openFee = (existing.openFee ?? existing.fee ?? 0) + addedFee;
2412
+ const totalFee = (existing.totalFee ?? existing.fee ?? 0) + addedFee;
2413
+ const next = {
2414
+ ...existing,
2415
+ ...params.orderLeverage ? { orderLeverage: params.orderLeverage } : {},
2416
+ qty: resultingQty,
2417
+ entryPrice: resultingEntryPrice,
2418
+ entryCount: Math.max(1, existing.entryCount ?? 1) + 1,
2419
+ lastEntryPrice: addedEntryPrice,
2420
+ lastEntryQty: addedQty,
2421
+ lastEntryTimestamp: entryTimestamp,
2422
+ currentPrice: resultingEntryPrice,
2423
+ currentPnl: 0,
2424
+ fee: openFee,
2425
+ openFee,
2426
+ totalFee,
2427
+ lastSyncedAt: now()
2428
+ };
2429
+ const dayKey = (0, import_time3.getRuntimeStorageDayKey)(existing.entryTimestamp);
2434
2430
  try {
2435
- const ticker = await connector.getTopOfBookTicker(symbol);
2436
- const arrivalSnapshotTime = toFiniteNumberOrNull(ticker?.timestamp);
2437
- const timestampUnavailable = requireTimestamp && arrivalSnapshotTime == null;
2438
- const bid = timestampUnavailable ? null : toFiniteNumberOrNull(ticker?.bidPrice);
2439
- const ask = timestampUnavailable ? null : toFiniteNumberOrNull(ticker?.askPrice);
2440
- const arrivalMid = bid != null && ask != null ? (bid + ask) / 2 : null;
2441
- const spreadBps = bid != null && ask != null && arrivalMid != null && arrivalMid > 0 ? (ask - bid) / arrivalMid * 1e4 : null;
2442
- return {
2443
- arrivalSnapshotTime: arrivalSnapshotTime ?? Date.now(),
2444
- arrivalSource: "top_of_book",
2445
- bid,
2446
- ask,
2447
- arrivalMid,
2448
- spreadBps
2449
- };
2431
+ await Promise.all([
2432
+ (0, import_redis2.setData)(import_redis2.redisKeys.runtimeTrade(userName, existing.orderId), next, {
2433
+ expire: 0
2434
+ }),
2435
+ (0, import_redis2.setHashJsonField)(
2436
+ import_redis2.redisKeys.runtimeTradeBucket(userName, dayKey),
2437
+ existing.orderId,
2438
+ next,
2439
+ { expire: 0 }
2440
+ )
2441
+ ]);
2450
2442
  } catch (error) {
2451
- import_logger5.logger.warn(
2452
- "runtime order arrival snapshot failed: %s %s",
2443
+ import_logger5.logger.error(
2444
+ "runtime trade increase journal failed: %s %s",
2453
2445
  symbol,
2454
2446
  error?.message || String(error)
2455
2447
  );
2456
- return {
2457
- arrivalSnapshotTime: Date.now(),
2458
- arrivalSource: "top_of_book_error",
2459
- bid: null,
2460
- ask: null,
2461
- arrivalMid: null,
2462
- spreadBps: null
2463
- };
2464
2448
  }
2449
+ return next;
2465
2450
  };
2466
- var validateEntryProtectionAtArrival = ({
2467
- direction,
2468
- signalPrice,
2469
- bid,
2470
- ask,
2471
- arrivalMid,
2472
- takeProfits,
2473
- stopLossPrice
2474
- }) => {
2475
- const entryReference = direction === "LONG" ? ask ?? arrivalMid ?? signalPrice : bid ?? arrivalMid ?? signalPrice;
2476
- const stopReference = direction === "LONG" ? bid ?? arrivalMid ?? signalPrice : ask ?? arrivalMid ?? signalPrice;
2477
- if (!Number.isFinite(entryReference) || entryReference <= 0 || !Number.isFinite(stopReference) || stopReference <= 0) {
2478
- throw new Error("INVALID_ENTRY_REFERENCE_AT_ARRIVAL");
2479
- }
2480
- for (const takeProfit of takeProfits) {
2481
- if (!Number.isFinite(takeProfit.price) || takeProfit.price <= 0 || (direction === "LONG" ? takeProfit.price <= entryReference : takeProfit.price >= entryReference)) {
2482
- throw new Error("TAKE_PROFIT_CROSSED_BEFORE_ENTRY");
2483
- }
2484
- }
2485
- if (stopLossPrice != null && (!Number.isFinite(stopLossPrice) || stopLossPrice <= 0 || (direction === "LONG" ? stopLossPrice >= stopReference : stopLossPrice <= stopReference))) {
2486
- throw new Error("STOP_LOSS_CROSSED_BEFORE_ENTRY");
2451
+ var getActiveRuntimeTrade = async (params) => {
2452
+ const { userName, symbol, accountId, deploymentId } = params;
2453
+ if (!userName) {
2454
+ return null;
2487
2455
  }
2488
- };
2489
- var createEntrySubmissionValidation = ({
2490
- connector,
2491
- symbol,
2492
- direction,
2493
- takeProfits,
2494
- stopLossPrice,
2495
- limits,
2496
- onValidated
2497
- }) => {
2498
- if (!limits) return void 0;
2499
- if (!connector.capabilities?.orderSubmissionValidation)
2500
- throw new EntrySubmissionValidationError(
2501
- "ENTRY_SUBMISSION_VALIDATION_UNSUPPORTED"
2502
- );
2503
- return async ({ qty, executionPrice }) => {
2504
- const simulated = Reflect.get(connector, "__tradejsTestConnector") === true;
2505
- const snapshot2 = simulated && executionPrice != null ? {
2506
- arrivalSnapshotTime: Date.now(),
2507
- arrivalSource: "simulation",
2508
- bid: executionPrice,
2509
- ask: executionPrice,
2510
- arrivalMid: executionPrice,
2511
- spreadBps: 0
2512
- } : await getOrderArrivalSnapshot({
2513
- connector,
2514
- symbol,
2515
- requireTimestamp: true
2516
- });
2517
- const age = Date.now() - snapshot2.arrivalSnapshotTime;
2518
- if (snapshot2.bid == null || snapshot2.ask == null || !Number.isFinite(snapshot2.bid) || !Number.isFinite(snapshot2.ask) || snapshot2.bid <= 0 || snapshot2.ask < snapshot2.bid)
2519
- throw new EntrySubmissionValidationError(
2520
- "ENTRY_QUOTE_UNAVAILABLE_AT_SUBMISSION"
2521
- );
2522
- if (!Number.isFinite(age) || age < -MAX_ENTRY_QUOTE_AGE_MS || age > MAX_ENTRY_QUOTE_AGE_MS)
2523
- throw new EntrySubmissionValidationError(
2524
- "ENTRY_QUOTE_STALE_AT_SUBMISSION"
2525
- );
2526
- const price = direction === "LONG" ? snapshot2.ask : snapshot2.bid;
2527
- validateEntryProtectionAtArrival({
2528
- direction,
2529
- signalPrice: price,
2530
- bid: snapshot2.bid,
2531
- ask: snapshot2.ask,
2532
- arrivalMid: snapshot2.arrivalMid,
2533
- takeProfits,
2534
- stopLossPrice
2535
- });
2536
- const prices = takeProfits.map((tp) => tp.price);
2537
- const economics2 = validateEntryExecution({
2538
- direction,
2539
- price,
2540
- qty,
2541
- stopLossPrice,
2542
- takeProfitPrice: direction === "LONG" ? Math.max(...prices) : Math.min(...prices),
2543
- limits
2544
- });
2545
- onValidated?.(snapshot2, economics2);
2546
- };
2547
- };
2548
-
2549
- // src/ai.ts
2550
- var import_aiLanguages2 = require("@tradejs/core/aiLanguages");
2551
- var import_redis2 = require("@tradejs/infra/redis");
2552
-
2553
- // src/aiShared.ts
2554
- var MAX_AI_SERIES_POINTS = 5;
2555
- var COMPACT_INDICATORS_SNAPSHOT_SYMBOL = /* @__PURE__ */ Symbol.for(
2556
- "tradejs.indicators.compactSnapshot"
2557
- );
2558
- var COMPACT_INDICATORS_SNAPSHOT_KEY = "__tradejsCompactIndicatorsSnapshot";
2559
- var trimSeriesDeep = (value) => {
2560
- if (Array.isArray(value)) {
2561
- const trimmed = value.slice(-MAX_AI_SERIES_POINTS);
2562
- const isMatrix = trimmed.every((item) => Array.isArray(item));
2563
- if (isMatrix) {
2564
- return trimmed;
2565
- }
2566
- return trimmed.map(
2567
- (item) => item && typeof item === "object" ? trimSeriesDeep(item) : item
2568
- );
2456
+ const activeRef = await (0, import_redis2.getData)(
2457
+ import_redis2.redisKeys.runtimeActiveTrade(userName, symbol, deploymentId ?? accountId),
2458
+ null
2459
+ );
2460
+ const orderId = String(activeRef?.orderId || "").trim();
2461
+ if (!orderId) {
2462
+ return null;
2569
2463
  }
2570
- if (value && typeof value === "object") {
2571
- return Object.fromEntries(
2572
- Object.entries(value).map(([key, nested]) => [
2573
- key,
2574
- trimSeriesDeep(nested)
2575
- ])
2464
+ const existing = await (0, import_redis2.getData)(
2465
+ import_redis2.redisKeys.runtimeTrade(userName, orderId),
2466
+ null
2467
+ );
2468
+ if (!existing || existing.status !== "active") {
2469
+ await (0, import_redis2.delKey)(
2470
+ import_redis2.redisKeys.runtimeActiveTrade(userName, symbol, deploymentId ?? accountId)
2576
2471
  );
2472
+ return null;
2577
2473
  }
2578
- return value;
2474
+ return existing;
2579
2475
  };
2580
- var buildCompactAiIndicatorsSnapshot = (value) => {
2581
- const compactSnapshot = value && typeof value === "object" ? value[COMPACT_INDICATORS_SNAPSHOT_SYMBOL] ?? value[COMPACT_INDICATORS_SNAPSHOT_KEY] : void 0;
2582
- if (typeof compactSnapshot === "function") {
2583
- return compactSnapshot({ limit: MAX_AI_SERIES_POINTS });
2476
+ var markRuntimeTradeClosed = async (params) => {
2477
+ const {
2478
+ userName,
2479
+ expectedOrderId,
2480
+ symbol,
2481
+ strategy,
2482
+ exitPrice,
2483
+ exitTimestamp,
2484
+ closedPnl,
2485
+ exitType,
2486
+ accountId,
2487
+ deploymentId
2488
+ } = params;
2489
+ if (!userName) {
2490
+ return null;
2584
2491
  }
2585
- return trimSeriesDeep(value);
2586
- };
2587
-
2588
- // src/aiMarketContext.ts
2589
- var toRecord = (value) => {
2590
- if (!value || typeof value !== "object" || Array.isArray(value)) {
2492
+ const existing = expectedOrderId ? await (0, import_redis2.getDataStrict)(
2493
+ import_redis2.redisKeys.runtimeTrade(userName, expectedOrderId)
2494
+ ) : await getActiveRuntimeTrade({
2495
+ userName,
2496
+ symbol,
2497
+ accountId,
2498
+ deploymentId
2499
+ });
2500
+ if (!existing || expectedOrderId && existing.orderId !== expectedOrderId) {
2591
2501
  return null;
2592
2502
  }
2593
- return value;
2594
- };
2595
- var toFiniteNumber = (value) => {
2596
- const numeric = typeof value === "number" ? value : typeof value === "string" && value.trim() ? Number(value) : Number.NaN;
2597
- return Number.isFinite(numeric) ? numeric : null;
2598
- };
2599
- var roundTo = (value, decimals) => {
2600
- const factor = 10 ** decimals;
2601
- return Math.round(value * factor) / factor;
2602
- };
2603
- var buildMissingSpreadContext = () => ({
2604
- source: "payload.additionalIndicators.baseContext.relative.execution.venueSpread",
2605
- indicatorKey: "payload.additionalIndicators.baseContext.relative.execution.venueSpread",
2606
- available: false,
2607
- value: null,
2608
- zScore: null,
2609
- bps: null,
2610
- absBps: null,
2611
- bias: null,
2612
- severity: null
2613
- });
2614
- var buildSpreadContextFromSignal = (signal) => {
2615
- const baseContext = toRecord(signal.additionalIndicators?.baseContext);
2616
- const relative = toRecord(baseContext?.relative);
2617
- const execution = toRecord(relative?.execution);
2618
- const spread = toFiniteNumber(execution?.venueSpread);
2619
- const zScore = toFiniteNumber(execution?.venueSpreadZScore);
2620
- if (spread == null) {
2621
- return buildMissingSpreadContext();
2503
+ const orderId = existing.orderId;
2504
+ if (strategy && existing.strategy !== strategy) {
2505
+ return null;
2622
2506
  }
2623
- const value = roundTo(spread, 8);
2624
- const bps = roundTo(value * 1e4, 2);
2625
- const absBps = Math.abs(bps);
2626
- const bias = absBps < 1 ? "flat" : bps > 0 ? "coinbase_premium" : "binance_premium";
2627
- const severity = absBps >= 20 ? "wide" : absBps >= 5 ? "elevated" : "normal";
2628
- return {
2629
- source: "payload.additionalIndicators.baseContext.relative.execution.venueSpread",
2630
- indicatorKey: "payload.additionalIndicators.baseContext.relative.execution.venueSpread",
2631
- available: true,
2632
- value,
2633
- zScore,
2634
- bps,
2635
- absBps,
2636
- bias,
2637
- severity
2507
+ const resolvedExitPrice = typeof exitPrice === "number" && Number.isFinite(exitPrice) ? exitPrice : existing.currentPrice ?? existing.entryPrice;
2508
+ const resolvedClosedPnl = typeof closedPnl === "number" && Number.isFinite(closedPnl) ? closedPnl : typeof resolvedExitPrice === "number" && Number.isFinite(resolvedExitPrice) ? calculateClosedPnl({
2509
+ direction: existing.direction,
2510
+ entryPrice: existing.entryPrice,
2511
+ exitPrice: resolvedExitPrice,
2512
+ qty: existing.qty
2513
+ }) : existing.closedPnl ?? existing.currentPnl ?? null;
2514
+ const next = expectedOrderId && existing.status === "closed" ? existing : {
2515
+ ...existing,
2516
+ status: "closed",
2517
+ currentPrice: resolvedExitPrice,
2518
+ currentPnl: resolvedClosedPnl,
2519
+ closedPnl: resolvedClosedPnl,
2520
+ exitPrice: resolvedExitPrice,
2521
+ exitTimestamp: typeof exitTimestamp === "number" && Number.isFinite(exitTimestamp) ? exitTimestamp : now(),
2522
+ exitType: exitType ?? existing.exitType ?? null,
2523
+ lastSyncedAt: now()
2638
2524
  };
2525
+ const dayKey = (0, import_time3.getRuntimeStorageDayKey)(existing.entryTimestamp);
2526
+ const closeDayKey = (0, import_time3.getRuntimeStorageDayKey)(next.exitTimestamp);
2527
+ try {
2528
+ await Promise.all([
2529
+ (expectedOrderId ? import_redis2.setDataStrict : import_redis2.setData)(
2530
+ import_redis2.redisKeys.runtimeTrade(userName, orderId),
2531
+ next,
2532
+ {
2533
+ expire: import_constants3.TTL_1M
2534
+ }
2535
+ ),
2536
+ (expectedOrderId ? import_redis2.setHashJsonFieldStrict : import_redis2.setHashJsonField)(
2537
+ import_redis2.redisKeys.runtimeTradeBucket(userName, dayKey),
2538
+ orderId,
2539
+ next,
2540
+ { expire: import_constants3.TTL_1M }
2541
+ ),
2542
+ (expectedOrderId ? import_redis2.setHashJsonFieldStrict : import_redis2.setHashJsonField)(
2543
+ import_redis2.redisKeys.runtimeClosedTradeBucket(userName, closeDayKey),
2544
+ orderId,
2545
+ next,
2546
+ { expire: import_constants3.TTL_1M }
2547
+ ),
2548
+ expectedOrderId ? (0, import_redis2.deleteRuntimeTradeReference)(
2549
+ import_redis2.redisKeys.runtimeActiveTrade(
2550
+ userName,
2551
+ symbol,
2552
+ deploymentId ?? accountId
2553
+ ),
2554
+ expectedOrderId
2555
+ ) : (0, import_redis2.delKey)(
2556
+ import_redis2.redisKeys.runtimeActiveTrade(
2557
+ userName,
2558
+ symbol,
2559
+ deploymentId ?? accountId
2560
+ )
2561
+ )
2562
+ ]);
2563
+ } catch (error) {
2564
+ if (expectedOrderId) throw error;
2565
+ import_logger5.logger.error(
2566
+ "runtime trade close journal failed: %s %s",
2567
+ symbol,
2568
+ error?.message || String(error)
2569
+ );
2570
+ }
2571
+ return next;
2639
2572
  };
2640
- var buildTrueDeltaContextFromSignal = (signal) => {
2641
- const baseContext = toRecord(signal.additionalIndicators?.baseContext);
2642
- const participation = toRecord(baseContext?.participation);
2643
- const delta = toRecord(participation?.delta);
2644
- const source = String(delta?.source ?? "");
2645
- const isTrueDeltaSource = source === "kline_taker_volume" || source === "agg_trades" || source === "trades";
2646
- if (!delta || !isTrueDeltaSource) {
2647
- return {
2648
- source: source || null,
2649
- available: false,
2650
- buyPressurePct: null,
2651
- buyVolume: null,
2652
- sellVolume: null,
2653
- netDelta: null,
2654
- deltaPct: null,
2655
- signedVolumeZScore: null
2656
- };
2657
- }
2573
+
2574
+ // src/strategy/portfolioRuntime.ts
2575
+ var coordinators = /* @__PURE__ */ new WeakMap();
2576
+ async function observeRuntimePortfolio(connector, binding, asOf, reservations, observedNow = Date.now) {
2577
+ if (!connector.getOpenPositionPnl || !connector.getEntryExecutions)
2578
+ throw new Error(
2579
+ "Portfolio policies require position marks and entry executions"
2580
+ );
2581
+ const orders = await Promise.all(
2582
+ reservations.map(async (r) => {
2583
+ if (r.submitted && connector.getEntryOrderObservation) {
2584
+ const fact2 = await connector.getEntryOrderObservation({
2585
+ symbol: r.symbol,
2586
+ orderId: r.id
2587
+ });
2588
+ if (fact2) return fact2;
2589
+ }
2590
+ const executions = r.submitted ? await connector.getEntryExecutions({
2591
+ symbol: r.symbol,
2592
+ startTime: r.timestamp,
2593
+ endTime: asOf
2594
+ }) : [];
2595
+ const filledQty = executions.filter(
2596
+ (e) => (e.orderLinkId === r.id || e.orderId === r.id) && e.direction === r.direction
2597
+ ).reduce((sum, e) => sum + e.qty, 0);
2598
+ return {
2599
+ id: r.id,
2600
+ status: filledQty >= r.qty ? "filled" : filledQty > 0 ? "partial" : "pending",
2601
+ filledQty,
2602
+ positionId: r.id
2603
+ };
2604
+ })
2605
+ );
2606
+ const snapshots2 = await connector.getOpenPositionPnl();
2607
+ const positions = await Promise.all(
2608
+ snapshots2.map(async (p) => {
2609
+ const reserved = reservations.find(
2610
+ (r) => r.symbol === p.symbol && r.direction === p.direction && orders.some(
2611
+ (o) => o.id === r.id && o.filledQty > 0 && p.qty <= o.filledQty * (1 + 1e-8)
2612
+ )
2613
+ );
2614
+ const trade = await getActiveRuntimeTrade({
2615
+ ...binding,
2616
+ symbol: p.symbol
2617
+ });
2618
+ 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;
2619
+ return {
2620
+ id: reserved?.id ?? owner?.orderId ?? `unknown:${p.symbol}:${p.direction}`,
2621
+ strategy: reserved?.strategy ?? owner?.strategy ?? null,
2622
+ symbol: p.symbol,
2623
+ direction: p.direction,
2624
+ qty: p.qty,
2625
+ entryPrice: p.price,
2626
+ markPrice: p.currentPrice,
2627
+ markedAt: p.observedAt ?? null,
2628
+ stopLossPrice: p.slPrice ?? p.stopLossPrice ?? null
2629
+ };
2630
+ })
2631
+ );
2632
+ return { asOf: Math.max(asOf, observedNow()), positions, orders };
2633
+ }
2634
+ function persistentStore(key) {
2635
+ const raw = /* @__PURE__ */ new WeakMap();
2658
2636
  return {
2659
- source,
2660
- available: true,
2661
- buyPressurePct: toFiniteNumber(delta.buyPressurePct),
2662
- buyVolume: toFiniteNumber(delta.buyVolume),
2663
- sellVolume: toFiniteNumber(delta.sellVolume),
2664
- netDelta: toFiniteNumber(delta.netDelta),
2665
- deltaPct: toFiniteNumber(delta.deltaPct),
2666
- signedVolumeZScore: toFiniteNumber(delta.signedVolumeZScore)
2637
+ async read() {
2638
+ const value = await import_redis3.portfolioStateStorage.read(key);
2639
+ if (value === null) return null;
2640
+ const state = JSON.parse(value);
2641
+ (0, import_portfolio2.assertPortfolioState)(state);
2642
+ raw.set(state, value);
2643
+ return state;
2644
+ },
2645
+ async compare(previous, next) {
2646
+ const expected = previous === null ? null : raw.get(previous);
2647
+ if (expected === void 0)
2648
+ throw new Error("Portfolio CAS requires a storage snapshot");
2649
+ return import_redis3.portfolioStateStorage.compare(key, expected, JSON.stringify(next));
2650
+ }
2667
2651
  };
2668
- };
2669
- var buildTradeFlowContextFromSignal = (signal) => {
2670
- const baseContext = toRecord(signal.additionalIndicators?.baseContext);
2671
- const participation = toRecord(baseContext?.participation);
2672
- const tradeFlow = toRecord(participation?.tradeFlow);
2673
- if (!tradeFlow) {
2674
- return {
2675
- source: null,
2676
- available: false,
2677
- interval: null,
2678
- stale: null,
2679
- trades: null,
2680
- buyPressurePct: null,
2681
- netBaseDelta: null,
2682
- netQuoteDelta: null
2683
- };
2684
- }
2685
- return {
2686
- source: String(tradeFlow.source ?? ""),
2687
- available: true,
2688
- interval: String(tradeFlow.interval ?? ""),
2689
- stale: typeof tradeFlow.stale === "boolean" ? tradeFlow.stale : null,
2690
- trades: toFiniteNumber(tradeFlow.trades),
2691
- buyPressurePct: toFiniteNumber(tradeFlow.buyPressurePct),
2692
- netBaseDelta: toFiniteNumber(tradeFlow.netBaseDelta),
2693
- netQuoteDelta: toFiniteNumber(tradeFlow.netQuoteDelta)
2652
+ }
2653
+ function getPortfolioRuntime({
2654
+ value,
2655
+ connectorName,
2656
+ strategy,
2657
+ connector,
2658
+ env,
2659
+ userName,
2660
+ accountId,
2661
+ deploymentId,
2662
+ symbol,
2663
+ canClose
2664
+ }) {
2665
+ const policy = (0, import_portfolio2.parsePortfolioPolicy)(value);
2666
+ if (!policy) return void 0;
2667
+ const replay = (env === "BACKTEST" || env === "PARITY") && Reflect.get(connector, "__tradejsReplayConnector") === true;
2668
+ if ((env === "BACKTEST" || env === "PARITY") && (!replay || !connector.getPortfolioObservation))
2669
+ throw new Error(
2670
+ "Portfolio policies require shared portfolio replay; isolated symbol backtests are unsupported"
2671
+ );
2672
+ if (!replay && (!userName || !accountId || !deploymentId || !connector.getOpenPositionPnl || !connector.getEntryExecutions))
2673
+ throw new Error(
2674
+ "Portfolio runtime requires exact user/account/deployment binding, marks and executions"
2675
+ );
2676
+ const binding = {
2677
+ userName,
2678
+ accountId,
2679
+ deploymentId
2694
2680
  };
2695
- };
2696
- var buildMarketBreadthContextFromSignal = (signal) => {
2697
- const baseContext = toRecord(signal.additionalIndicators?.baseContext);
2698
- const relative = toRecord(baseContext?.relative);
2699
- const breadth = toRecord(relative?.marketBreadth);
2700
- if (!breadth) {
2701
- return {
2702
- source: null,
2703
- available: false,
2704
- universe: null,
2705
- interval: null,
2706
- stale: null,
2707
- symbolsCount: null,
2708
- advanceDeclineRatio: null,
2709
- pctAboveMa20: null,
2710
- pctAboveMa50: null,
2711
- equalWeightedReturn: null,
2712
- volumeWeightedReturn: null,
2713
- dispersion: null
2714
- };
2681
+ const scope = policy.scope === "strategy" ? strategy : policy.scope === "account" ? "account" : [...policy.scope.strategies].sort();
2682
+ const key = (0, import_node_crypto2.createHash)("sha256").update(JSON.stringify([binding, connectorName, connector.universe, scope])).digest("hex");
2683
+ const fingerprint3 = (0, import_portfolio2.portfolioFingerprint)(policy, strategy);
2684
+ const cacheKey = key;
2685
+ let registry = coordinators.get(connector);
2686
+ if (!registry) {
2687
+ registry = /* @__PURE__ */ new Map();
2688
+ coordinators.set(connector, registry);
2689
+ }
2690
+ const existing = registry.get(cacheKey);
2691
+ if (existing) {
2692
+ if (existing.fingerprint !== fingerprint3)
2693
+ throw new Error("Conflicting portfolio configuration for the same scope");
2694
+ if (symbol && canClose)
2695
+ existing.permissions.set(JSON.stringify([strategy, symbol]), canClose);
2696
+ return existing.coordinator;
2697
+ }
2698
+ const permissions = /* @__PURE__ */ new Map();
2699
+ if (symbol && canClose)
2700
+ permissions.set(JSON.stringify([strategy, symbol]), canClose);
2701
+ const coordinator = createPortfolioCoordinator({
2702
+ policy,
2703
+ strategy,
2704
+ store: replay ? createMemoryPortfolioStore() : persistentStore(key),
2705
+ execution: {
2706
+ async confirmClosed(intent, asOf) {
2707
+ if (replay) return;
2708
+ const position = intent.position;
2709
+ await markRuntimeTradeClosed({
2710
+ ...binding,
2711
+ strategy: position.strategy,
2712
+ expectedOrderId: position.id,
2713
+ symbol: position.symbol,
2714
+ exitPrice: position.markPrice,
2715
+ exitTimestamp: asOf,
2716
+ exitType: "exit"
2717
+ });
2718
+ },
2719
+ observe: (asOf, reservations) => replay ? connector.getPortfolioObservation(asOf, reservations) : observeRuntimePortfolio(connector, binding, asOf, reservations),
2720
+ async close(position, intent, asOf) {
2721
+ const permission = permissions.get(
2722
+ JSON.stringify([position.strategy, position.symbol])
2723
+ );
2724
+ if (!permission || !await permission(position, intent, asOf)) return;
2725
+ if (!replay) {
2726
+ const trade = await getActiveRuntimeTrade({
2727
+ ...binding,
2728
+ symbol: position.symbol
2729
+ });
2730
+ if (!trade || trade.orderId !== position.id)
2731
+ throw new Error("Portfolio close ownership changed");
2732
+ }
2733
+ await connector.closePosition({
2734
+ symbol: position.symbol,
2735
+ direction: position.direction,
2736
+ price: position.markPrice,
2737
+ timestamp: asOf,
2738
+ signal: {
2739
+ signalId: `${position.id}:${intent.reason}`,
2740
+ strategy: position.strategy,
2741
+ symbol: position.symbol,
2742
+ direction: position.direction,
2743
+ interval: "1",
2744
+ timestamp: asOf,
2745
+ prices: {
2746
+ riskRatio: 0,
2747
+ currentPrice: position.markPrice,
2748
+ takeProfitPrice: position.markPrice,
2749
+ stopLossPrice: position.markPrice
2750
+ },
2751
+ figures: {},
2752
+ indicators: {},
2753
+ additionalIndicators: { portfolioExitReason: intent.reason }
2754
+ }
2755
+ });
2756
+ }
2757
+ }
2758
+ });
2759
+ registry.set(cacheKey, { fingerprint: fingerprint3, coordinator, permissions });
2760
+ return coordinator;
2761
+ }
2762
+
2763
+ // src/strategyHelpers/tickerProfiles.ts
2764
+ var import_tickerProfiles = require("@tradejs/infra/tickerProfiles");
2765
+ var import_tickerProfiles2 = require("@tradejs/core/tickerProfiles");
2766
+
2767
+ // src/strategy/manifests.ts
2768
+ var import_indicators = require("@tradejs/core/indicators");
2769
+ var SHARED_STRATEGY_REGISTRY_KEY = "__tradejsNodeSharedStrategyRegistryV1__";
2770
+ var sharedRegistryScope = globalThis;
2771
+ var sharedStrategyRegistry = sharedRegistryScope[SHARED_STRATEGY_REGISTRY_KEY] ?? (sharedRegistryScope[SHARED_STRATEGY_REGISTRY_KEY] = {
2772
+ registryStateByProjectRoot: /* @__PURE__ */ new Map()
2773
+ });
2774
+ var createStrategyRegistryState = () => ({
2775
+ strategyCreators: /* @__PURE__ */ new Map(),
2776
+ strategyManifestsMap: /* @__PURE__ */ new Map(),
2777
+ strategyEntriesMap: /* @__PURE__ */ new Map(),
2778
+ strategySourcesMap: /* @__PURE__ */ new Map(),
2779
+ strategyModuleResolutions: /* @__PURE__ */ new Map(),
2780
+ pluginsLoadPromise: null
2781
+ });
2782
+ var registryStateByProjectRoot2 = sharedStrategyRegistry.registryStateByProjectRoot;
2783
+ var getStrategyRegistryState = (cwd = getTradejsProjectCwd()) => {
2784
+ const projectRoot = getTradejsProjectCwd(cwd);
2785
+ let state = registryStateByProjectRoot2.get(projectRoot);
2786
+ if (!state) {
2787
+ state = createStrategyRegistryState();
2788
+ registryStateByProjectRoot2.set(projectRoot, state);
2715
2789
  }
2716
2790
  return {
2717
- source: String(breadth.source ?? ""),
2718
- available: true,
2719
- universe: String(breadth.universe ?? ""),
2720
- interval: String(breadth.interval ?? ""),
2721
- stale: typeof breadth.stale === "boolean" ? breadth.stale : null,
2722
- symbolsCount: toFiniteNumber(breadth.symbolsCount),
2723
- advanceDeclineRatio: toFiniteNumber(breadth.advanceDeclineRatio),
2724
- pctAboveMa20: toFiniteNumber(breadth.pctAboveMa20),
2725
- pctAboveMa50: toFiniteNumber(breadth.pctAboveMa50),
2726
- equalWeightedReturn: toFiniteNumber(breadth.equalWeightedReturn),
2727
- volumeWeightedReturn: toFiniteNumber(breadth.volumeWeightedReturn),
2728
- dispersion: toFiniteNumber(breadth.dispersion)
2791
+ projectRoot,
2792
+ state
2729
2793
  };
2730
2794
  };
2731
- var buildMarketBreadthsContextFromSignal = (signal) => {
2732
- const baseContext = toRecord(signal.additionalIndicators?.baseContext);
2733
- const relative = toRecord(baseContext?.relative);
2734
- const breadths = toRecord(relative?.marketBreadths);
2735
- const build = (key) => {
2736
- const breadth = toRecord(breadths?.[key]);
2737
- if (!breadth) {
2738
- return {
2739
- source: null,
2740
- available: false,
2741
- universe: null,
2742
- interval: null,
2743
- stale: null,
2744
- symbolsCount: null,
2745
- advanceDeclineRatio: null,
2746
- pctAboveMa20: null,
2747
- pctAboveMa50: null,
2748
- equalWeightedReturn: null,
2749
- volumeWeightedReturn: null,
2750
- dispersion: null
2751
- };
2752
- }
2753
- return {
2754
- source: String(breadth.source ?? ""),
2755
- available: true,
2756
- universe: String(breadth.universe ?? ""),
2757
- interval: String(breadth.interval ?? ""),
2758
- stale: typeof breadth.stale === "boolean" ? breadth.stale : null,
2759
- symbolsCount: toFiniteNumber(breadth.symbolsCount),
2760
- advanceDeclineRatio: toFiniteNumber(breadth.advanceDeclineRatio),
2761
- pctAboveMa20: toFiniteNumber(breadth.pctAboveMa20),
2762
- pctAboveMa50: toFiniteNumber(breadth.pctAboveMa50),
2763
- equalWeightedReturn: toFiniteNumber(breadth.equalWeightedReturn),
2764
- volumeWeightedReturn: toFiniteNumber(breadth.volumeWeightedReturn),
2765
- dispersion: toFiniteNumber(breadth.dispersion)
2766
- };
2767
- };
2795
+ var toUniqueModules2 = (modules = []) => [
2796
+ ...new Set(modules.map((moduleName) => moduleName.trim()).filter(Boolean))
2797
+ ];
2798
+ var getConfiguredPluginModuleNames = async (cwd = getTradejsProjectCwd()) => {
2799
+ const config = await loadTradejsConfig(cwd);
2768
2800
  return {
2769
- top5: build("top5"),
2770
- top10: build("top10"),
2771
- top30: build("top30"),
2772
- top50: build("top50"),
2773
- top100: build("top100")
2801
+ strategyModules: toUniqueModules2(config.strategies),
2802
+ indicatorModules: toUniqueModules2(config.indicators)
2774
2803
  };
2775
2804
  };
2776
- var buildTargetVsBtcContextFromSignal = (signal) => {
2777
- const baseContext = toRecord(signal.additionalIndicators?.baseContext);
2778
- const relative = toRecord(baseContext?.relative);
2779
- const targetVsBtc = toRecord(relative?.targetVsBtc);
2780
- if (!targetVsBtc) {
2781
- return {
2782
- source: null,
2783
- available: false,
2784
- ratioReturn1h: null,
2785
- ratioReturn4h: null,
2786
- ratioReturn24h: null,
2787
- alphaVsBtc1h: null,
2788
- alphaVsBtc4h: null,
2789
- alphaVsBtc24h: null,
2790
- betaToBtc20: null,
2791
- correlationToBtc20: null,
2792
- ratioTrend: null
2793
- };
2805
+ var extractModuleEntries = (moduleExport, key) => {
2806
+ if (!moduleExport || typeof moduleExport !== "object") {
2807
+ return null;
2794
2808
  }
2795
- return {
2796
- source: String(targetVsBtc.source ?? ""),
2797
- available: true,
2798
- ratioReturn1h: toFiniteNumber(targetVsBtc.ratioReturn1h),
2799
- ratioReturn4h: toFiniteNumber(targetVsBtc.ratioReturn4h),
2800
- ratioReturn24h: toFiniteNumber(targetVsBtc.ratioReturn24h),
2801
- alphaVsBtc1h: toFiniteNumber(targetVsBtc.alphaVsBtc1h),
2802
- alphaVsBtc4h: toFiniteNumber(targetVsBtc.alphaVsBtc4h),
2803
- alphaVsBtc24h: toFiniteNumber(targetVsBtc.alphaVsBtc24h),
2804
- betaToBtc20: toFiniteNumber(targetVsBtc.betaToBtc20),
2805
- correlationToBtc20: toFiniteNumber(targetVsBtc.correlationToBtc20),
2806
- ratioTrend: typeof targetVsBtc.ratioTrend === "string" ? targetVsBtc.ratioTrend : null
2807
- };
2808
- };
2809
- var buildBtcAltRegimeContextFromSignal = (signal) => {
2810
- const baseContext = toRecord(signal.additionalIndicators?.baseContext);
2811
- const relative = toRecord(baseContext?.relative);
2812
- const btcAltRegime = toRecord(relative?.btcAltRegime);
2813
- if (!btcAltRegime) {
2814
- return {
2815
- source: null,
2816
- available: false,
2817
- universe: null,
2818
- interval: null,
2819
- stale: null,
2820
- regime: null,
2821
- btcReturn24h: null,
2822
- altBasketReturn24h: null,
2823
- btcVsAltReturn24h: null,
2824
- btcTurnoverShare24h: null,
2825
- btcTurnoverShareChange24h: null,
2826
- altVolToBtcVol24h: null,
2827
- altDispersion24h: null
2828
- };
2809
+ const candidate = moduleExport;
2810
+ if (Array.isArray(candidate[key])) {
2811
+ return candidate[key];
2829
2812
  }
2830
- return {
2831
- source: String(btcAltRegime.source ?? ""),
2832
- available: true,
2833
- universe: String(btcAltRegime.universe ?? ""),
2834
- interval: String(btcAltRegime.interval ?? ""),
2835
- stale: typeof btcAltRegime.stale === "boolean" ? btcAltRegime.stale : null,
2836
- regime: typeof btcAltRegime.regime === "string" ? btcAltRegime.regime : null,
2837
- btcReturn24h: toFiniteNumber(btcAltRegime.btcReturn24h),
2838
- altBasketReturn24h: toFiniteNumber(btcAltRegime.altBasketReturn24h),
2839
- btcVsAltReturn24h: toFiniteNumber(btcAltRegime.btcVsAltReturn24h),
2840
- btcTurnoverShare24h: toFiniteNumber(btcAltRegime.btcTurnoverShare24h),
2841
- btcTurnoverShareChange24h: toFiniteNumber(
2842
- btcAltRegime.btcTurnoverShareChange24h
2843
- ),
2844
- altVolToBtcVol24h: toFiniteNumber(btcAltRegime.altVolToBtcVol24h),
2845
- altDispersion24h: toFiniteNumber(btcAltRegime.altDispersion24h)
2846
- };
2813
+ const defaultExport = candidate.default;
2814
+ if (defaultExport && Array.isArray(defaultExport[key])) {
2815
+ return defaultExport[key];
2816
+ }
2817
+ return null;
2847
2818
  };
2848
- var buildCmcGlobalContextFromSignal = (signal) => {
2849
- const baseContext = toRecord(signal.additionalIndicators?.baseContext);
2850
- const relative = toRecord(baseContext?.relative);
2851
- const cmcGlobal = toRecord(relative?.cmcGlobal);
2852
- if (!cmcGlobal) {
2853
- return {
2854
- source: null,
2855
- available: false,
2856
- interval: null,
2857
- asOfTs: null,
2858
- stale: null,
2859
- totalMarketCapUsd: null,
2860
- totalVolumeUsd: null,
2861
- totalVolumeReportedUsd: null,
2862
- altMarketCapUsd: null,
2863
- altVolumeUsd: null,
2864
- altVolumeReportedUsd: null,
2865
- btcDominancePct: null,
2866
- ethDominancePct: null,
2867
- btcDominanceChange24hPct: null,
2868
- ethDominanceChange24hPct: null,
2869
- altMarketCapChange24hPct: null,
2870
- altVolumeChange24hPct: null,
2871
- activeCryptocurrencies: null,
2872
- activeExchanges: null,
2873
- activeMarketPairs: null,
2874
- altLiquidityRegime: null
2819
+ var extractStrategyPluginDefinition = (moduleExport) => {
2820
+ const strategyEntries = extractModuleEntries(
2821
+ moduleExport,
2822
+ "strategyEntries"
2823
+ );
2824
+ return strategyEntries ? { strategyEntries } : null;
2825
+ };
2826
+ var extractIndicatorPluginDefinition = (moduleExport) => {
2827
+ const indicatorEntries = extractModuleEntries(
2828
+ moduleExport,
2829
+ "indicatorEntries"
2830
+ );
2831
+ return indicatorEntries ? { indicatorEntries } : null;
2832
+ };
2833
+ var validateStrategyEntries = (moduleName, entries, state) => {
2834
+ const issues = [];
2835
+ const names = /* @__PURE__ */ new Set();
2836
+ entries.forEach((entry, index) => {
2837
+ const entryPath = `${moduleName}.strategyEntries[${index}]`;
2838
+ const strategyName = entry?.manifest?.name;
2839
+ if (typeof strategyName !== "string" || !strategyName.trim()) {
2840
+ issues.push(`${entryPath}: manifest.name is required`);
2841
+ } else if (names.has(strategyName) || state?.strategyEntriesMap.has(strategyName)) {
2842
+ issues.push(`${entryPath}: duplicate strategy ${strategyName}`);
2843
+ } else {
2844
+ names.add(strategyName);
2845
+ }
2846
+ if (!entry || typeof entry !== "object" || !entry.defaults || typeof entry.defaults !== "object" || Array.isArray(entry.defaults)) {
2847
+ issues.push(`${entryPath}: defaults must be an object`);
2848
+ }
2849
+ if (typeof entry?.parseConfig !== "function") {
2850
+ issues.push(`${entryPath}: parseConfig is required`);
2851
+ }
2852
+ if (typeof entry?.createCore !== "function") {
2853
+ issues.push(`${entryPath}: createCore is required`);
2854
+ }
2855
+ });
2856
+ return issues;
2857
+ };
2858
+ var registerEntries2 = (entries, source, state) => {
2859
+ for (const entry of entries) {
2860
+ const strategyName = entry.manifest.name;
2861
+ state.strategyManifestsMap.set(strategyName, entry.manifest);
2862
+ state.strategyEntriesMap.set(strategyName, entry);
2863
+ state.strategySourcesMap.set(strategyName, source);
2864
+ materializeStrategyCreator(strategyName, state);
2865
+ }
2866
+ };
2867
+ var materializeStrategyCreator = (strategyName, state) => {
2868
+ if (state.strategyCreators.has(strategyName) || !sharedStrategyRegistry.strategyRuntimeFactory) {
2869
+ return;
2870
+ }
2871
+ const entry = state.strategyEntriesMap.get(strategyName);
2872
+ if (!entry) return;
2873
+ state.strategyCreators.set(
2874
+ strategyName,
2875
+ sharedStrategyRegistry.strategyRuntimeFactory({
2876
+ strategyName,
2877
+ defaults: entry.defaults,
2878
+ createCore: entry.createCore,
2879
+ manifest: entry.manifest,
2880
+ detectorKey: entry.detectorKey,
2881
+ detectorNoSignalSkipReason: entry.detectorNoSignalSkipReason,
2882
+ resolveRegisteredManifest: (name) => state.strategyManifestsMap.get(name)
2883
+ })
2884
+ );
2885
+ };
2886
+ var setStrategyRuntimeFactory = (factory) => {
2887
+ sharedStrategyRegistry.strategyRuntimeFactory = factory;
2888
+ for (const state of registryStateByProjectRoot2.values()) {
2889
+ for (const strategyName of state.strategyEntriesMap.keys()) {
2890
+ materializeStrategyCreator(strategyName, state);
2891
+ }
2892
+ }
2893
+ };
2894
+ var importStrategyPluginModule = async (moduleName, cwd = getTradejsProjectCwd()) => {
2895
+ if (typeof importTradejsModule === "function") {
2896
+ return importTradejsModule(moduleName, cwd);
2897
+ }
2898
+ return import(
2899
+ /* webpackIgnore: true */
2900
+ moduleName
2901
+ );
2902
+ };
2903
+ var resolveStrategyModuleEntry = async ({
2904
+ strategyName,
2905
+ moduleName,
2906
+ cwd
2907
+ }) => {
2908
+ const { projectRoot, state } = getStrategyRegistryState(cwd);
2909
+ const source = moduleName.trim();
2910
+ const resolvedModuleName = resolvePluginModuleSpecifier(source, projectRoot);
2911
+ const cacheKey = `${resolvedModuleName}\0${strategyName}`;
2912
+ const cached = state.strategyModuleResolutions.get(cacheKey);
2913
+ if (cached) return cached;
2914
+ const resolution = (async () => {
2915
+ const moduleExport = await importStrategyPluginModule(
2916
+ resolvedModuleName,
2917
+ projectRoot
2918
+ );
2919
+ const pluginDefinition = extractStrategyPluginDefinition(moduleExport);
2920
+ if (!pluginDefinition) {
2921
+ throw new Error(`${source}: export { strategyEntries } is missing`);
2922
+ }
2923
+ const issues = validateStrategyEntries(
2924
+ source,
2925
+ pluginDefinition.strategyEntries
2926
+ );
2927
+ if (issues.length > 0) {
2928
+ throw new Error(
2929
+ ["Invalid TradeJS strategy module:", ...issues].join("\n")
2930
+ );
2931
+ }
2932
+ const entry = pluginDefinition.strategyEntries.find(
2933
+ (candidate) => candidate.manifest.name === strategyName
2934
+ );
2935
+ if (!entry) {
2936
+ throw new Error(`${source}: strategy ${strategyName} is missing`);
2937
+ }
2938
+ const factory = sharedStrategyRegistry.strategyRuntimeFactory;
2939
+ if (!factory) {
2940
+ throw new Error("Strategy runtime factory is not configured");
2941
+ }
2942
+ const manifestsByName = new Map(
2943
+ pluginDefinition.strategyEntries.map(
2944
+ (candidate) => [candidate.manifest.name, candidate.manifest]
2945
+ )
2946
+ );
2947
+ return {
2948
+ entry,
2949
+ creator: factory({
2950
+ strategyName,
2951
+ defaults: entry.defaults,
2952
+ createCore: entry.createCore,
2953
+ manifest: entry.manifest,
2954
+ detectorKey: entry.detectorKey,
2955
+ detectorNoSignalSkipReason: entry.detectorNoSignalSkipReason,
2956
+ resolveRegisteredManifest: (name) => manifestsByName.get(name) ?? state.strategyManifestsMap.get(name)
2957
+ }),
2958
+ source
2959
+ };
2960
+ })();
2961
+ state.strategyModuleResolutions.set(cacheKey, resolution);
2962
+ try {
2963
+ return await resolution;
2964
+ } catch (error) {
2965
+ state.strategyModuleResolutions.delete(cacheKey);
2966
+ throw error;
2967
+ }
2968
+ };
2969
+ var ensureStrategyPluginsLoaded = async (cwd = getTradejsProjectCwd()) => {
2970
+ const { projectRoot, state } = getStrategyRegistryState(cwd);
2971
+ if (!state.pluginsLoadPromise) {
2972
+ (0, import_indicators.resetIndicatorRegistryCache)(projectRoot);
2973
+ state.pluginsLoadPromise = (async () => {
2974
+ const { strategyModules, indicatorModules } = await getConfiguredPluginModuleNames(projectRoot);
2975
+ const strategySet = new Set(strategyModules);
2976
+ const indicatorSet = new Set(indicatorModules);
2977
+ const pluginModuleNames = [
2978
+ .../* @__PURE__ */ new Set([...strategyModules, ...indicatorModules])
2979
+ ];
2980
+ if (!pluginModuleNames.length) {
2981
+ return;
2982
+ }
2983
+ const issues = [];
2984
+ for (const moduleName of pluginModuleNames) {
2985
+ try {
2986
+ const resolvedModuleName = resolvePluginModuleSpecifier(
2987
+ moduleName,
2988
+ projectRoot
2989
+ );
2990
+ const moduleExport = await importStrategyPluginModule(
2991
+ resolvedModuleName,
2992
+ projectRoot
2993
+ );
2994
+ if (strategySet.has(moduleName)) {
2995
+ const pluginDefinition = extractStrategyPluginDefinition(moduleExport);
2996
+ if (!pluginDefinition) {
2997
+ issues.push(
2998
+ `${moduleName}: export { strategyEntries } is missing`
2999
+ );
3000
+ } else {
3001
+ const entryIssues = validateStrategyEntries(
3002
+ moduleName,
3003
+ pluginDefinition.strategyEntries,
3004
+ state
3005
+ );
3006
+ issues.push(...entryIssues);
3007
+ if (entryIssues.length === 0) {
3008
+ registerEntries2(
3009
+ pluginDefinition.strategyEntries,
3010
+ moduleName,
3011
+ state
3012
+ );
3013
+ }
3014
+ }
3015
+ }
3016
+ if (indicatorSet.has(moduleName)) {
3017
+ const indicatorPluginDefinition = extractIndicatorPluginDefinition(moduleExport);
3018
+ if (!indicatorPluginDefinition) {
3019
+ issues.push(
3020
+ `${moduleName}: export { indicatorEntries } is missing`
3021
+ );
3022
+ } else {
3023
+ (0, import_indicators.registerIndicatorEntries)(
3024
+ indicatorPluginDefinition.indicatorEntries,
3025
+ moduleName,
3026
+ projectRoot
3027
+ );
3028
+ }
3029
+ }
3030
+ if (!strategySet.has(moduleName) && !indicatorSet.has(moduleName)) {
3031
+ issues.push(`${moduleName}: plugin is not declared in config`);
3032
+ }
3033
+ } catch (error) {
3034
+ issues.push(`${moduleName}: failed to import: ${String(error)}`);
3035
+ }
3036
+ }
3037
+ if (issues.length > 0) {
3038
+ throw new Error(
3039
+ ["Invalid TradeJS plugin catalog:", ...issues].join("\n")
3040
+ );
3041
+ }
3042
+ })();
3043
+ }
3044
+ await state.pluginsLoadPromise;
3045
+ };
3046
+ var getStrategyCreator = async (name, cwd = getTradejsProjectCwd(), moduleName) => {
3047
+ if (moduleName?.trim()) {
3048
+ await ensureStrategyPluginsLoaded(cwd);
3049
+ return (await resolveStrategyModuleEntry({ strategyName: name, moduleName, cwd })).creator;
3050
+ }
3051
+ await ensureStrategyPluginsLoaded(cwd);
3052
+ const { state } = getStrategyRegistryState(cwd);
3053
+ return state.strategyCreators.get(name);
3054
+ };
3055
+ var getStrategyEntry = async (name, cwd = getTradejsProjectCwd(), moduleName) => {
3056
+ if (moduleName?.trim()) {
3057
+ await ensureStrategyPluginsLoaded(cwd);
3058
+ return (await resolveStrategyModuleEntry({ strategyName: name, moduleName, cwd })).entry;
3059
+ }
3060
+ await ensureStrategyPluginsLoaded(cwd);
3061
+ const { state } = getStrategyRegistryState(cwd);
3062
+ return state.strategyEntriesMap.get(name);
3063
+ };
3064
+ var getStrategyPluginSource = async (name, cwd = getTradejsProjectCwd(), moduleName) => {
3065
+ if (moduleName?.trim()) {
3066
+ await ensureStrategyPluginsLoaded(cwd);
3067
+ return (await resolveStrategyModuleEntry({ strategyName: name, moduleName, cwd })).source;
3068
+ }
3069
+ await ensureStrategyPluginsLoaded(cwd);
3070
+ const { state } = getStrategyRegistryState(cwd);
3071
+ return state.strategySourcesMap.get(name);
3072
+ };
3073
+ var getStrategyManifest = (name, cwd = getTradejsProjectCwd()) => {
3074
+ if (!name) {
3075
+ return void 0;
3076
+ }
3077
+ const { state } = getStrategyRegistryState(cwd);
3078
+ return state.strategyManifestsMap.get(name);
3079
+ };
3080
+ var strategies = new Proxy(
3081
+ {},
3082
+ {
3083
+ get: (_target, property) => {
3084
+ if (typeof property !== "string") {
3085
+ return void 0;
3086
+ }
3087
+ return getStrategyRegistryState().state.strategyCreators.get(property);
3088
+ },
3089
+ ownKeys: () => {
3090
+ return [...getStrategyRegistryState().state.strategyCreators.keys()];
3091
+ },
3092
+ getOwnPropertyDescriptor: () => ({
3093
+ enumerable: true,
3094
+ configurable: true
3095
+ })
3096
+ }
3097
+ );
3098
+
3099
+ // src/strategyHelpers/tickerProfiles.ts
3100
+ var snapshots = /* @__PURE__ */ new Map();
3101
+ var resolveTickerProfileSource = (strategy) => {
3102
+ const declared = getStrategyManifest(strategy)?.contextRequirements?.tickerProfiles;
3103
+ if (declared) return declared;
3104
+ const provider = process.env.TICKER_PROFILES_PROVIDER?.trim();
3105
+ const universeId = process.env.TICKER_PROFILES_UNIVERSE?.trim();
3106
+ if (!provider && !universeId) return void 0;
3107
+ if (!provider || !universeId)
3108
+ throw new Error(
3109
+ "Set both TICKER_PROFILES_PROVIDER and TICKER_PROFILES_UNIVERSE"
3110
+ );
3111
+ return { provider, universeId };
3112
+ };
3113
+ var loadTickerProfileContext = async ({
3114
+ projectRoot,
3115
+ provider,
3116
+ universeId,
3117
+ symbol,
3118
+ timestamp
3119
+ }) => {
3120
+ const asOf = (0, import_tickerProfiles2.tickerProfileMonth)(timestamp), key = JSON.stringify([projectRoot, provider, universeId, asOf]);
3121
+ let cached = snapshots.get(key);
3122
+ if (!cached || cached.expires < Date.now()) {
3123
+ const value = (0, import_tickerProfiles.readTickerProfiles)(projectRoot, provider, universeId, asOf).then((envelope) => {
3124
+ if (!envelope) return null;
3125
+ const s = envelope.snapshot;
3126
+ if (!(0, import_tickerProfiles2.isTickerProfileSnapshot)(s) || s.asOf !== asOf || s.provider !== provider || s.universeId !== universeId)
3127
+ throw new Error("Ticker profile identity/schema mismatch");
3128
+ const entry = snapshots.get(key);
3129
+ if (entry) entry.expires = Infinity;
3130
+ return new Map(
3131
+ s.profiles.map((p) => [
3132
+ p.symbol,
3133
+ Object.freeze({
3134
+ ...p,
3135
+ definition: s.definition,
3136
+ provider,
3137
+ universeId,
3138
+ snapshotId: envelope.snapshotId
3139
+ })
3140
+ ])
3141
+ );
3142
+ }).catch((error) => {
3143
+ snapshots.delete(key);
3144
+ throw error;
3145
+ });
3146
+ cached = { expires: Date.now() + 6e4, value };
3147
+ snapshots.set(key, cached);
3148
+ if (snapshots.size > 24) {
3149
+ const oldest = snapshots.keys().next().value;
3150
+ if (oldest) snapshots.delete(oldest);
3151
+ }
3152
+ }
3153
+ return (await cached.value)?.get(symbol);
3154
+ };
3155
+ var withTickerProfile = (baseContext, profile) => Object.defineProperty(
3156
+ Object.create(
3157
+ Object.getPrototypeOf(baseContext),
3158
+ Object.getOwnPropertyDescriptors(baseContext)
3159
+ ),
3160
+ "tickerProfile",
3161
+ { enumerable: true, configurable: true, value: profile }
3162
+ );
3163
+ var enrichSignalWithTickerProfile = async (signal) => {
3164
+ const source = resolveTickerProfileSource(signal.strategy);
3165
+ if (!source || signal.universe === "tradfi" || !signal.symbol.endsWith("USDT"))
3166
+ return;
3167
+ const profile = await loadTickerProfileContext({
3168
+ ...source,
3169
+ projectRoot: getTradejsProjectCwd(),
3170
+ symbol: signal.symbol,
3171
+ timestamp: signal.timestamp
3172
+ });
3173
+ if (profile) {
3174
+ signal.additionalIndicators = {
3175
+ ...signal.additionalIndicators,
3176
+ tickerProfile: profile
3177
+ };
3178
+ }
3179
+ };
3180
+
3181
+ // src/strategyRuntime.ts
3182
+ var import_jev6 = require("@tradejs/core/jev");
3183
+
3184
+ // src/jev.ts
3185
+ var import_promises = __toESM(require("fs/promises"));
3186
+ var import_node_path = __toESM(require("path"));
3187
+ var import_jev4 = require("@tradejs/core/jev");
3188
+ var import_jev5 = require("@tradejs/infra/jev");
3189
+ var import_userSettings = require("@tradejs/infra/userSettings");
3190
+ init_jevInput();
3191
+ init_jevInput();
3192
+ init_jevTraining();
3193
+ var resolveJevProvider = async (userName) => {
3194
+ const settings = await (0, import_userSettings.getUserSettings)(userName);
3195
+ const provider = {
3196
+ endpoint: settings.JEV_API_ENDPOINT || import_jev4.JEV_PROVIDERS[0].endpoint,
3197
+ model: settings.JEV_MODEL || import_jev4.JEV_PROVIDERS[0].model
3198
+ };
3199
+ (0, import_jev4.parseJevConfig)({ source: "provider", mode: "observe", provider });
3200
+ if (!settings.JEV_API_KEY)
3201
+ throw new Error("Configure the Jev API key in Account settings");
3202
+ return provider;
3203
+ };
3204
+ var emptyScores = () => Object.fromEntries(import_jev4.JEV_DIMENSIONS.map((key) => [key, null]));
3205
+ var pending = /* @__PURE__ */ new Map();
3206
+ var pendingResponses = /* @__PURE__ */ new Map();
3207
+ var evaluateJevInput = async ({
3208
+ input,
3209
+ config,
3210
+ userName,
3211
+ projectRoot
3212
+ }) => {
3213
+ config = (0, import_jev4.parseJevConfig)(config);
3214
+ const inputHash = (0, import_jev5.jevHash)(input);
3215
+ const questions = questionsForJevInput(input);
3216
+ const questionsHash = (0, import_jev5.jevHash)(questions);
3217
+ const base = {
3218
+ questionSet: import_jev4.JEV_QUESTION_SET,
3219
+ schema: "tradejs-signal-assessment/v5",
3220
+ source: config.source,
3221
+ mode: config.mode,
3222
+ inputHash,
3223
+ questionsHash,
3224
+ geometryStatus: input.geometryStatus,
3225
+ levelsValid: typeof input.features["signal.validLevels"] === "boolean" ? input.features["signal.validLevels"] : null
3226
+ };
3227
+ if (!input.questions.length)
3228
+ return {
3229
+ assessment: {
3230
+ ...base,
3231
+ status: "unavailable",
3232
+ model: "",
3233
+ scores: emptyScores(),
3234
+ confidence: emptyScores()
3235
+ }
3236
+ };
3237
+ if (config.source === "local") {
3238
+ const contents = await import_promises.default.readFile(
3239
+ import_node_path.default.resolve(projectRoot, config.modelFile),
3240
+ "utf8"
3241
+ );
3242
+ if ((0, import_jev5.jevFileHash)(contents) !== config.modelSha256)
3243
+ throw new Error("Local Jev gate checksum mismatch");
3244
+ const { validateJevGateModel: validateJevGateModel2 } = await Promise.resolve().then(() => (init_jevTraining(), jevTraining_exports));
3245
+ const model = validateJevGateModel2(JSON.parse(contents));
3246
+ if (model.strategy !== input.strategy || model.questionsHash !== (0, import_jev5.jevHash)(JEV_QUESTIONS))
3247
+ throw new Error("Local Jev gate strategy or question version mismatch");
3248
+ const scores = Object.fromEntries(
3249
+ import_jev4.JEV_DIMENSIONS.map((key) => [
3250
+ key,
3251
+ input.questions.includes(key) && model.trees[key] ? (0, import_jev4.predictJevTree)(model.trees[key], input.features) : null
3252
+ ])
3253
+ );
3254
+ return {
3255
+ assessment: {
3256
+ ...base,
3257
+ status: "available",
3258
+ model: config.modelSha256,
3259
+ scores,
3260
+ confidence: emptyScores()
3261
+ }
3262
+ };
3263
+ }
3264
+ const provider = config.provider ?? await resolveJevProvider(userName);
3265
+ const id = (0, import_jev5.jevHash)({ inputHash, questionsHash, provider });
3266
+ const dir = import_node_path.default.resolve(projectRoot, config.recordsDir ?? "data/ai/jev");
3267
+ const file = import_node_path.default.join(dir, "records", `${id}.json`);
3268
+ const getRecord = async () => {
3269
+ const existing = await (0, import_jev5.readJevArtifact)(file);
3270
+ if (existing) {
3271
+ 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))
3272
+ throw new Error("Jev recording identity mismatch");
3273
+ (0, import_jev5.validateJevResponse)(existing.response, input.questions);
3274
+ for (const key of import_jev4.JEV_DIMENSIONS) {
3275
+ const expected = input.questions.includes(key) ? existing.response.answers[key].score / (0, import_jev5.jevScoreMaximum)(key) : null;
3276
+ if (existing.scores[key] !== expected)
3277
+ throw new Error("Jev recording scores do not match the response");
3278
+ }
3279
+ return existing;
3280
+ }
3281
+ if (config.source === "recorded")
3282
+ throw new Error(
3283
+ `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.`
3284
+ );
3285
+ const settings = await (0, import_userSettings.getUserSettings)(userName);
3286
+ if (!settings.JEV_API_KEY)
3287
+ throw new Error("Configure the Jev API key in Account settings");
3288
+ const apiKey = settings.JEV_API_KEY;
3289
+ const configuredProvider = await resolveJevProvider(userName);
3290
+ if (configuredProvider.endpoint !== provider.endpoint)
3291
+ throw new Error(
3292
+ "Jev account endpoint changed; update the frozen provider configuration"
3293
+ );
3294
+ const started = Date.now();
3295
+ const state = stateForJevInput(input);
3296
+ const responseId = (0, import_jev5.jevHash)({
3297
+ state,
3298
+ questionsHash,
3299
+ provider,
3300
+ user: (0, import_jev5.jevHash)(userName)
3301
+ });
3302
+ const responseFile = import_node_path.default.join(dir, "responses", `${responseId}.json`);
3303
+ let responsePromise = pendingResponses.get(responseFile);
3304
+ if (!responsePromise) {
3305
+ responsePromise = (async () => {
3306
+ const saved = await (0, import_jev5.readJevArtifact)(responseFile);
3307
+ if (saved) return (0, import_jev5.validateJevResponse)(saved, input.questions);
3308
+ const fresh = await (0, import_jev5.requestJev)(provider, apiKey, state, questions);
3309
+ await (0, import_jev5.writeJevArtifact)(responseFile, fresh);
3310
+ return fresh;
3311
+ })();
3312
+ pendingResponses.set(responseFile, responsePromise);
3313
+ }
3314
+ let response;
3315
+ try {
3316
+ response = await responsePromise;
3317
+ } finally {
3318
+ pendingResponses.delete(responseFile);
3319
+ }
3320
+ const scores = Object.fromEntries(
3321
+ import_jev4.JEV_DIMENSIONS.map((key) => [
3322
+ key,
3323
+ input.questions.includes(key) ? response.answers[key].score / (0, import_jev5.jevScoreMaximum)(key) : null
3324
+ ])
3325
+ );
3326
+ const value = {
3327
+ schema: "tradejs-jev-record/v4",
3328
+ id,
3329
+ inputHash,
3330
+ questionsHash,
3331
+ provider,
3332
+ input,
3333
+ response,
3334
+ scores,
3335
+ createdAt: (/* @__PURE__ */ new Date()).toISOString(),
3336
+ elapsedMs: Date.now() - started
3337
+ };
3338
+ await (0, import_jev5.writeJevArtifact)(file, value);
3339
+ return await (0, import_jev5.readJevArtifact)(file);
3340
+ };
3341
+ const pendingKey = `${file}:${(0, import_jev5.jevHash)(userName)}`;
3342
+ let promise = pending.get(pendingKey);
3343
+ if (!promise) {
3344
+ promise = getRecord();
3345
+ pending.set(pendingKey, promise);
3346
+ }
3347
+ let record;
3348
+ try {
3349
+ record = await promise;
3350
+ } finally {
3351
+ pending.delete(pendingKey);
3352
+ }
3353
+ return {
3354
+ record,
3355
+ assessment: {
3356
+ ...base,
3357
+ status: "available",
3358
+ recordId: id,
3359
+ model: record.response.model,
3360
+ scores: record.scores,
3361
+ confidence: Object.fromEntries(
3362
+ import_jev4.JEV_DIMENSIONS.map((key) => [
3363
+ key,
3364
+ record.response.answers[key]?.confidence ?? null
3365
+ ])
3366
+ )
3367
+ }
3368
+ };
3369
+ };
3370
+ var assessSignalWithJev = async ({
3371
+ signal,
3372
+ config,
3373
+ userName,
3374
+ projectRoot,
3375
+ strict,
3376
+ scope = "runtime"
3377
+ }) => {
3378
+ let assessment;
3379
+ try {
3380
+ const input = buildJevInput(signal);
3381
+ ({ assessment } = await evaluateJevInput({
3382
+ input,
3383
+ config,
3384
+ userName,
3385
+ projectRoot
3386
+ }));
3387
+ } catch (error) {
3388
+ if (strict) throw error;
3389
+ assessment = {
3390
+ questionSet: import_jev4.JEV_QUESTION_SET,
3391
+ schema: "tradejs-signal-assessment/v5",
3392
+ source: config.source,
3393
+ mode: config.mode,
3394
+ status: "unavailable",
3395
+ inputHash: "",
3396
+ questionsHash: "",
3397
+ geometryStatus: "absent",
3398
+ levelsValid: null,
3399
+ model: config.provider?.model ?? config.modelSha256 ?? "",
3400
+ scores: emptyScores(),
3401
+ confidence: emptyScores()
2875
3402
  };
2876
3403
  }
2877
- return {
2878
- source: String(cmcGlobal.source ?? ""),
2879
- available: true,
2880
- interval: typeof cmcGlobal.interval === "string" ? cmcGlobal.interval : null,
2881
- asOfTs: toFiniteNumber(cmcGlobal.asOfTs),
2882
- stale: typeof cmcGlobal.stale === "boolean" ? cmcGlobal.stale : null,
2883
- totalMarketCapUsd: toFiniteNumber(cmcGlobal.totalMarketCapUsd),
2884
- totalVolumeUsd: toFiniteNumber(cmcGlobal.totalVolumeUsd),
2885
- totalVolumeReportedUsd: toFiniteNumber(cmcGlobal.totalVolumeReportedUsd),
2886
- altMarketCapUsd: toFiniteNumber(cmcGlobal.altMarketCapUsd),
2887
- altVolumeUsd: toFiniteNumber(cmcGlobal.altVolumeUsd),
2888
- altVolumeReportedUsd: toFiniteNumber(cmcGlobal.altVolumeReportedUsd),
2889
- btcDominancePct: toFiniteNumber(cmcGlobal.btcDominancePct),
2890
- ethDominancePct: toFiniteNumber(cmcGlobal.ethDominancePct),
2891
- btcDominanceChange24hPct: toFiniteNumber(
2892
- cmcGlobal.btcDominanceChange24hPct
2893
- ),
2894
- ethDominanceChange24hPct: toFiniteNumber(
2895
- cmcGlobal.ethDominanceChange24hPct
2896
- ),
2897
- altMarketCapChange24hPct: toFiniteNumber(
2898
- cmcGlobal.altMarketCapChange24hPct
3404
+ signal.assessment = assessment;
3405
+ signal.additionalIndicators = {
3406
+ ...signal.additionalIndicators,
3407
+ jev: (0, import_jev4.jevAssessmentFeatures)(assessment)
3408
+ };
3409
+ const identity = {
3410
+ signalId: signal.signalId,
3411
+ strategy: signal.strategy,
3412
+ symbol: signal.symbol,
3413
+ timestamp: signal.timestamp,
3414
+ scope,
3415
+ user: (0, import_jev5.jevHash)(userName),
3416
+ config,
3417
+ assessment
3418
+ };
3419
+ await (0, import_jev5.writeJevArtifact)(
3420
+ import_node_path.default.resolve(
3421
+ projectRoot,
3422
+ config.recordsDir ?? "data/ai/jev",
3423
+ "decisions",
3424
+ `${(0, import_jev5.jevHash)(identity)}.json`
2899
3425
  ),
2900
- altVolumeChange24hPct: toFiniteNumber(cmcGlobal.altVolumeChange24hPct),
2901
- activeCryptocurrencies: toFiniteNumber(cmcGlobal.activeCryptocurrencies),
2902
- activeExchanges: toFiniteNumber(cmcGlobal.activeExchanges),
2903
- activeMarketPairs: toFiniteNumber(cmcGlobal.activeMarketPairs),
2904
- altLiquidityRegime: typeof cmcGlobal.altLiquidityRegime === "string" ? cmcGlobal.altLiquidityRegime : null
3426
+ identity
3427
+ );
3428
+ return assessment;
3429
+ };
3430
+
3431
+ // src/strategyRuntime.ts
3432
+ var import_constants7 = require("@tradejs/core/constants");
3433
+ var import_strategies6 = require("@tradejs/core/strategies");
3434
+ var import_logger14 = require("@tradejs/infra/logger");
3435
+
3436
+ // src/strategyHelpers/runtime.ts
3437
+ var import_logger12 = require("@tradejs/infra/logger");
3438
+ var import_constants5 = require("@tradejs/core/constants");
3439
+ var import_ml2 = require("@tradejs/infra/ml");
3440
+
3441
+ // src/strategy/entrySubmissionValidation.ts
3442
+ var import_logger6 = require("@tradejs/infra/logger");
3443
+
3444
+ // src/strategy/entryExecutionValidation.ts
3445
+ var EntrySubmissionValidationError = class extends Error {
3446
+ constructor() {
3447
+ super(...arguments);
3448
+ this.name = "EntrySubmissionValidationError";
3449
+ }
3450
+ };
3451
+ var resolveEntryExecutionLimits = (config, direction) => {
3452
+ const side = config[direction];
3453
+ const minimum = side && typeof side === "object" && "minRiskRatio" in side ? side.minRiskRatio : void 0;
3454
+ const maximum = config.MAX_LOSS_VALUE;
3455
+ if (minimum == null && maximum == null) return void 0;
3456
+ const minimumNetRiskRatio = minimum == null ? void 0 : Number(minimum);
3457
+ const maxLossValue = maximum == null ? void 0 : Number(maximum);
3458
+ const feeRate = Number(config.RISK_FEE_RATE ?? 0);
3459
+ const slippage = Number(config.RISK_SLIPPAGE_BPS ?? 0);
3460
+ const impact = Number(config.RISK_MARKET_IMPACT_BPS ?? 0);
3461
+ const slippageBps = slippage + impact;
3462
+ 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)
3463
+ throw new Error("INVALID_ENTRY_EXECUTION_LIMITS");
3464
+ return { minimumNetRiskRatio, maxLossValue, feeRate, slippageBps };
3465
+ };
3466
+ var validateEntryExecution = ({
3467
+ direction,
3468
+ price,
3469
+ qty,
3470
+ stopLossPrice,
3471
+ takeProfitPrice,
3472
+ limits
3473
+ }) => {
3474
+ if (!Number.isFinite(price) || price <= 0 || !Number.isFinite(qty) || qty <= 0 || stopLossPrice == null || !Number.isFinite(stopLossPrice) || stopLossPrice <= 0 || !Number.isFinite(takeProfitPrice) || takeProfitPrice <= 0)
3475
+ throw new EntrySubmissionValidationError(
3476
+ "ENTRY_ECONOMICS_INPUTS_UNAVAILABLE"
3477
+ );
3478
+ const side = direction === "LONG" ? 1 : -1;
3479
+ if (side * (price - stopLossPrice) <= 0 || side * (takeProfitPrice - price) <= 0)
3480
+ throw new EntrySubmissionValidationError(
3481
+ "INVALID_ENTRY_PROTECTION_AT_SUBMISSION"
3482
+ );
3483
+ const slippage = limits.slippageBps / 1e4;
3484
+ const cost = (value, sign) => value * slippage + value * (1 + sign * slippage) * limits.feeRate;
3485
+ const entryCost = cost(price, side);
3486
+ const lossPerUnit = Math.abs(price - stopLossPrice) + entryCost + cost(stopLossPrice, -side);
3487
+ const rewardPerUnit = Math.max(
3488
+ 0,
3489
+ Math.abs(takeProfitPrice - price) - entryCost - cost(takeProfitPrice, -side)
3490
+ );
3491
+ const netRiskRatio = rewardPerUnit / lossPerUnit;
3492
+ const plannedLossValue = qty * lossPerUnit;
3493
+ if (!Number.isFinite(netRiskRatio) || !Number.isFinite(plannedLossValue))
3494
+ throw new EntrySubmissionValidationError(
3495
+ "ENTRY_ECONOMICS_INPUTS_UNAVAILABLE"
3496
+ );
3497
+ const belowMinimumNetRiskRatio = limits.minimumNetRiskRatio != null && netRiskRatio <= limits.minimumNetRiskRatio;
3498
+ const riskExceeded = limits.maxLossValue != null && plannedLossValue > limits.maxLossValue * (1 + 1e-9);
3499
+ return {
3500
+ netRiskRatio,
3501
+ plannedLossValue,
3502
+ riskExceeded,
3503
+ belowMinimumNetRiskRatio
3504
+ };
3505
+ };
3506
+
3507
+ // src/strategy/entrySubmissionValidation.ts
3508
+ var MAX_ENTRY_QUOTE_AGE_MS = 5e3;
3509
+ var toFiniteNumberOrNull = (value) => typeof value === "number" && Number.isFinite(value) ? value : null;
3510
+ var getOrderArrivalSnapshot = async ({
3511
+ connector,
3512
+ symbol,
3513
+ requireTimestamp = false
3514
+ }) => {
3515
+ if (typeof connector.getTopOfBookTicker !== "function") {
3516
+ return {
3517
+ arrivalSnapshotTime: Date.now(),
3518
+ arrivalSource: "unavailable",
3519
+ bid: null,
3520
+ ask: null,
3521
+ arrivalMid: null,
3522
+ spreadBps: null
3523
+ };
3524
+ }
3525
+ try {
3526
+ const ticker = await connector.getTopOfBookTicker(symbol);
3527
+ const arrivalSnapshotTime = toFiniteNumberOrNull(ticker?.timestamp);
3528
+ const timestampUnavailable = requireTimestamp && arrivalSnapshotTime == null;
3529
+ const bid = timestampUnavailable ? null : toFiniteNumberOrNull(ticker?.bidPrice);
3530
+ const ask = timestampUnavailable ? null : toFiniteNumberOrNull(ticker?.askPrice);
3531
+ const arrivalMid = bid != null && ask != null ? (bid + ask) / 2 : null;
3532
+ const spreadBps = bid != null && ask != null && arrivalMid != null && arrivalMid > 0 ? (ask - bid) / arrivalMid * 1e4 : null;
3533
+ return {
3534
+ arrivalSnapshotTime: arrivalSnapshotTime ?? Date.now(),
3535
+ arrivalSource: "top_of_book",
3536
+ bid,
3537
+ ask,
3538
+ arrivalMid,
3539
+ spreadBps
3540
+ };
3541
+ } catch (error) {
3542
+ import_logger6.logger.warn(
3543
+ "runtime order arrival snapshot failed: %s %s",
3544
+ symbol,
3545
+ error?.message || String(error)
3546
+ );
3547
+ return {
3548
+ arrivalSnapshotTime: Date.now(),
3549
+ arrivalSource: "top_of_book_error",
3550
+ bid: null,
3551
+ ask: null,
3552
+ arrivalMid: null,
3553
+ spreadBps: null
3554
+ };
3555
+ }
3556
+ };
3557
+ var validateEntryProtectionAtArrival = ({
3558
+ direction,
3559
+ signalPrice,
3560
+ bid,
3561
+ ask,
3562
+ arrivalMid,
3563
+ takeProfits,
3564
+ stopLossPrice
3565
+ }) => {
3566
+ const entryReference = direction === "LONG" ? ask ?? arrivalMid ?? signalPrice : bid ?? arrivalMid ?? signalPrice;
3567
+ const stopReference = direction === "LONG" ? bid ?? arrivalMid ?? signalPrice : ask ?? arrivalMid ?? signalPrice;
3568
+ if (!Number.isFinite(entryReference) || entryReference <= 0 || !Number.isFinite(stopReference) || stopReference <= 0) {
3569
+ throw new Error("INVALID_ENTRY_REFERENCE_AT_ARRIVAL");
3570
+ }
3571
+ for (const takeProfit of takeProfits) {
3572
+ if (!Number.isFinite(takeProfit.price) || takeProfit.price <= 0 || (direction === "LONG" ? takeProfit.price <= entryReference : takeProfit.price >= entryReference)) {
3573
+ throw new Error("TAKE_PROFIT_CROSSED_BEFORE_ENTRY");
3574
+ }
3575
+ }
3576
+ if (stopLossPrice != null && (!Number.isFinite(stopLossPrice) || stopLossPrice <= 0 || (direction === "LONG" ? stopLossPrice >= stopReference : stopLossPrice <= stopReference))) {
3577
+ throw new Error("STOP_LOSS_CROSSED_BEFORE_ENTRY");
3578
+ }
3579
+ };
3580
+ var createEntrySubmissionValidation = ({
3581
+ connector,
3582
+ symbol,
3583
+ direction,
3584
+ takeProfits,
3585
+ stopLossPrice,
3586
+ limits,
3587
+ onValidated
3588
+ }) => {
3589
+ if (!limits) return void 0;
3590
+ if (!connector.capabilities?.orderSubmissionValidation)
3591
+ throw new EntrySubmissionValidationError(
3592
+ "ENTRY_SUBMISSION_VALIDATION_UNSUPPORTED"
3593
+ );
3594
+ return async ({ qty, executionPrice }) => {
3595
+ const simulated = Reflect.get(connector, "__tradejsTestConnector") === true;
3596
+ const snapshot2 = simulated && executionPrice != null ? {
3597
+ arrivalSnapshotTime: Date.now(),
3598
+ arrivalSource: "simulation",
3599
+ bid: executionPrice,
3600
+ ask: executionPrice,
3601
+ arrivalMid: executionPrice,
3602
+ spreadBps: 0
3603
+ } : await getOrderArrivalSnapshot({
3604
+ connector,
3605
+ symbol,
3606
+ requireTimestamp: true
3607
+ });
3608
+ const age = Date.now() - snapshot2.arrivalSnapshotTime;
3609
+ if (snapshot2.bid == null || snapshot2.ask == null || !Number.isFinite(snapshot2.bid) || !Number.isFinite(snapshot2.ask) || snapshot2.bid <= 0 || snapshot2.ask < snapshot2.bid)
3610
+ throw new EntrySubmissionValidationError(
3611
+ "ENTRY_QUOTE_UNAVAILABLE_AT_SUBMISSION"
3612
+ );
3613
+ if (!Number.isFinite(age) || age < -MAX_ENTRY_QUOTE_AGE_MS || age > MAX_ENTRY_QUOTE_AGE_MS)
3614
+ throw new EntrySubmissionValidationError(
3615
+ "ENTRY_QUOTE_STALE_AT_SUBMISSION"
3616
+ );
3617
+ const price = direction === "LONG" ? snapshot2.ask : snapshot2.bid;
3618
+ validateEntryProtectionAtArrival({
3619
+ direction,
3620
+ signalPrice: price,
3621
+ bid: snapshot2.bid,
3622
+ ask: snapshot2.ask,
3623
+ arrivalMid: snapshot2.arrivalMid,
3624
+ takeProfits,
3625
+ stopLossPrice
3626
+ });
3627
+ const prices = takeProfits.map((tp) => tp.price);
3628
+ const economics2 = validateEntryExecution({
3629
+ direction,
3630
+ price,
3631
+ qty,
3632
+ stopLossPrice,
3633
+ takeProfitPrice: direction === "LONG" ? Math.max(...prices) : Math.min(...prices),
3634
+ limits
3635
+ });
3636
+ if (economics2.belowMinimumNetRiskRatio) {
3637
+ import_logger6.logger.warn(
3638
+ "entry submission warning: %s %s ENTRY_RR_BELOW_MIN_AT_SUBMISSION (RR %s, threshold %s)",
3639
+ symbol,
3640
+ direction,
3641
+ economics2.netRiskRatio,
3642
+ limits.minimumNetRiskRatio
3643
+ );
3644
+ }
3645
+ onValidated?.(snapshot2, economics2);
2905
3646
  };
2906
3647
  };
2907
- var buildCmcReferenceAssetsContextFromSignal = (signal) => {
3648
+
3649
+ // src/ai.ts
3650
+ var import_aiLanguages2 = require("@tradejs/core/aiLanguages");
3651
+ var import_redis4 = require("@tradejs/infra/redis");
3652
+
3653
+ // src/strategyAdapters/ai.ts
3654
+ var import_indicators2 = require("@tradejs/core/indicators");
3655
+ var import_aiFeatures = require("@tradejs/core/aiFeatures");
3656
+
3657
+ // src/aiShared.ts
3658
+ var MAX_AI_SERIES_POINTS = 5;
3659
+ var COMPACT_INDICATORS_SNAPSHOT_SYMBOL = /* @__PURE__ */ Symbol.for(
3660
+ "tradejs.indicators.compactSnapshot"
3661
+ );
3662
+ var COMPACT_INDICATORS_SNAPSHOT_KEY = "__tradejsCompactIndicatorsSnapshot";
3663
+ var trimSeriesDeep = (value) => {
3664
+ if (Array.isArray(value)) {
3665
+ const trimmed = value.slice(-MAX_AI_SERIES_POINTS);
3666
+ const isMatrix = trimmed.every((item) => Array.isArray(item));
3667
+ if (isMatrix) {
3668
+ return trimmed;
3669
+ }
3670
+ return trimmed.map(
3671
+ (item) => item && typeof item === "object" ? trimSeriesDeep(item) : item
3672
+ );
3673
+ }
3674
+ if (value && typeof value === "object") {
3675
+ return Object.fromEntries(
3676
+ Object.entries(value).map(([key, nested]) => [
3677
+ key,
3678
+ trimSeriesDeep(nested)
3679
+ ])
3680
+ );
3681
+ }
3682
+ return value;
3683
+ };
3684
+ var buildCompactAiIndicatorsSnapshot = (value) => {
3685
+ const compactSnapshot = value && typeof value === "object" ? value[COMPACT_INDICATORS_SNAPSHOT_SYMBOL] ?? value[COMPACT_INDICATORS_SNAPSHOT_KEY] : void 0;
3686
+ if (typeof compactSnapshot === "function") {
3687
+ return compactSnapshot({ limit: MAX_AI_SERIES_POINTS });
3688
+ }
3689
+ return trimSeriesDeep(value);
3690
+ };
3691
+
3692
+ // src/aiMarketContext.ts
3693
+ var toRecord = (value) => {
3694
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
3695
+ return null;
3696
+ }
3697
+ return value;
3698
+ };
3699
+ var toFiniteNumber = (value) => {
3700
+ const numeric = typeof value === "number" ? value : typeof value === "string" && value.trim() ? Number(value) : Number.NaN;
3701
+ return Number.isFinite(numeric) ? numeric : null;
3702
+ };
3703
+ var roundTo = (value, decimals) => {
3704
+ const factor = 10 ** decimals;
3705
+ return Math.round(value * factor) / factor;
3706
+ };
3707
+ var buildMissingSpreadContext = () => ({
3708
+ source: "payload.additionalIndicators.baseContext.relative.execution.venueSpread",
3709
+ indicatorKey: "payload.additionalIndicators.baseContext.relative.execution.venueSpread",
3710
+ available: false,
3711
+ value: null,
3712
+ zScore: null,
3713
+ bps: null,
3714
+ absBps: null,
3715
+ bias: null,
3716
+ severity: null
3717
+ });
3718
+ var buildSpreadContextFromSignal = (signal) => {
2908
3719
  const baseContext = toRecord(signal.additionalIndicators?.baseContext);
2909
3720
  const relative = toRecord(baseContext?.relative);
2910
- const cmcReferenceAssets = toRecord(relative?.cmcReferenceAssets);
2911
- if (!cmcReferenceAssets) {
2912
- return {
2913
- source: null,
2914
- available: false,
2915
- interval: null,
2916
- asOfTs: null,
2917
- stale: null,
2918
- btcMarketCapUsd: null,
2919
- ethMarketCapUsd: null,
2920
- btcVolumeUsd: null,
2921
- ethVolumeUsd: null,
2922
- btcVolumeToMarketCap: null,
2923
- ethVolumeToMarketCap: null,
2924
- ethBtcMarketCapRatio: null,
2925
- ethBtcMarketCapRatioChange24hPct: null,
2926
- ethVsBtcVolumeRatio: null,
2927
- referenceLiquidityRegime: null
2928
- };
3721
+ const execution = toRecord(relative?.execution);
3722
+ const spread = toFiniteNumber(execution?.venueSpread);
3723
+ const zScore = toFiniteNumber(execution?.venueSpreadZScore);
3724
+ if (spread == null) {
3725
+ return buildMissingSpreadContext();
2929
3726
  }
3727
+ const value = roundTo(spread, 8);
3728
+ const bps = roundTo(value * 1e4, 2);
3729
+ const absBps = Math.abs(bps);
3730
+ const bias = absBps < 1 ? "flat" : bps > 0 ? "coinbase_premium" : "binance_premium";
3731
+ const severity = absBps >= 20 ? "wide" : absBps >= 5 ? "elevated" : "normal";
2930
3732
  return {
2931
- source: String(cmcReferenceAssets.source ?? ""),
3733
+ source: "payload.additionalIndicators.baseContext.relative.execution.venueSpread",
3734
+ indicatorKey: "payload.additionalIndicators.baseContext.relative.execution.venueSpread",
2932
3735
  available: true,
2933
- interval: typeof cmcReferenceAssets.interval === "string" ? cmcReferenceAssets.interval : null,
2934
- asOfTs: toFiniteNumber(cmcReferenceAssets.asOfTs),
2935
- stale: typeof cmcReferenceAssets.stale === "boolean" ? cmcReferenceAssets.stale : null,
2936
- btcMarketCapUsd: toFiniteNumber(cmcReferenceAssets.btcMarketCapUsd),
2937
- ethMarketCapUsd: toFiniteNumber(cmcReferenceAssets.ethMarketCapUsd),
2938
- btcVolumeUsd: toFiniteNumber(cmcReferenceAssets.btcVolumeUsd),
2939
- ethVolumeUsd: toFiniteNumber(cmcReferenceAssets.ethVolumeUsd),
2940
- btcVolumeToMarketCap: toFiniteNumber(
2941
- cmcReferenceAssets.btcVolumeToMarketCap
2942
- ),
2943
- ethVolumeToMarketCap: toFiniteNumber(
2944
- cmcReferenceAssets.ethVolumeToMarketCap
2945
- ),
2946
- ethBtcMarketCapRatio: toFiniteNumber(
2947
- cmcReferenceAssets.ethBtcMarketCapRatio
2948
- ),
2949
- ethBtcMarketCapRatioChange24hPct: toFiniteNumber(
2950
- cmcReferenceAssets.ethBtcMarketCapRatioChange24hPct
2951
- ),
2952
- ethVsBtcVolumeRatio: toFiniteNumber(cmcReferenceAssets.ethVsBtcVolumeRatio),
2953
- referenceLiquidityRegime: typeof cmcReferenceAssets.referenceLiquidityRegime === "string" ? cmcReferenceAssets.referenceLiquidityRegime : null
3736
+ value,
3737
+ zScore,
3738
+ bps,
3739
+ absBps,
3740
+ bias,
3741
+ severity
2954
3742
  };
2955
3743
  };
2956
- var buildCmcExchangeLiquidityContextFromSignal = (signal) => {
3744
+ var buildTrueDeltaContextFromSignal = (signal) => {
2957
3745
  const baseContext = toRecord(signal.additionalIndicators?.baseContext);
2958
- const relative = toRecord(baseContext?.relative);
2959
- const cmcExchangeLiquidity = toRecord(relative?.cmcExchangeLiquidity);
2960
- if (!cmcExchangeLiquidity) {
3746
+ const participation = toRecord(baseContext?.participation);
3747
+ const delta = toRecord(participation?.delta);
3748
+ const source = String(delta?.source ?? "");
3749
+ const isTrueDeltaSource = source === "kline_taker_volume" || source === "agg_trades" || source === "trades";
3750
+ if (!delta || !isTrueDeltaSource) {
2961
3751
  return {
2962
- source: null,
3752
+ source: source || null,
2963
3753
  available: false,
2964
- interval: null,
2965
- asOfTs: null,
2966
- stale: null,
2967
- exchangesCount: null,
2968
- totalVolumeUsd: null,
2969
- totalVolumeChange24hPct: null,
2970
- binanceVolumeUsd: null,
2971
- binanceVolumeShare: null,
2972
- topExchangeVolumeShare: null,
2973
- liquidityRegime: null
3754
+ buyPressurePct: null,
3755
+ buyVolume: null,
3756
+ sellVolume: null,
3757
+ netDelta: null,
3758
+ deltaPct: null,
3759
+ signedVolumeZScore: null
2974
3760
  };
2975
3761
  }
2976
3762
  return {
2977
- source: String(cmcExchangeLiquidity.source ?? ""),
3763
+ source,
2978
3764
  available: true,
2979
- interval: typeof cmcExchangeLiquidity.interval === "string" ? cmcExchangeLiquidity.interval : null,
2980
- asOfTs: toFiniteNumber(cmcExchangeLiquidity.asOfTs),
2981
- stale: typeof cmcExchangeLiquidity.stale === "boolean" ? cmcExchangeLiquidity.stale : null,
2982
- exchangesCount: toFiniteNumber(cmcExchangeLiquidity.exchangesCount),
2983
- totalVolumeUsd: toFiniteNumber(cmcExchangeLiquidity.totalVolumeUsd),
2984
- totalVolumeChange24hPct: toFiniteNumber(
2985
- cmcExchangeLiquidity.totalVolumeChange24hPct
2986
- ),
2987
- binanceVolumeUsd: toFiniteNumber(cmcExchangeLiquidity.binanceVolumeUsd),
2988
- binanceVolumeShare: toFiniteNumber(cmcExchangeLiquidity.binanceVolumeShare),
2989
- topExchangeVolumeShare: toFiniteNumber(
2990
- cmcExchangeLiquidity.topExchangeVolumeShare
2991
- ),
2992
- liquidityRegime: typeof cmcExchangeLiquidity.liquidityRegime === "string" ? cmcExchangeLiquidity.liquidityRegime : null
3765
+ buyPressurePct: toFiniteNumber(delta.buyPressurePct),
3766
+ buyVolume: toFiniteNumber(delta.buyVolume),
3767
+ sellVolume: toFiniteNumber(delta.sellVolume),
3768
+ netDelta: toFiniteNumber(delta.netDelta),
3769
+ deltaPct: toFiniteNumber(delta.deltaPct),
3770
+ signedVolumeZScore: toFiniteNumber(delta.signedVolumeZScore)
2993
3771
  };
2994
3772
  };
2995
- var buildCmcFearGreedContextFromSignal = (signal) => {
3773
+ var buildTradeFlowContextFromSignal = (signal) => {
2996
3774
  const baseContext = toRecord(signal.additionalIndicators?.baseContext);
2997
- const relative = toRecord(baseContext?.relative);
2998
- const cmcFearGreed = toRecord(relative?.cmcFearGreed);
2999
- if (!cmcFearGreed) {
3775
+ const participation = toRecord(baseContext?.participation);
3776
+ const tradeFlow = toRecord(participation?.tradeFlow);
3777
+ if (!tradeFlow) {
3000
3778
  return {
3001
3779
  source: null,
3002
3780
  available: false,
3003
3781
  interval: null,
3004
- asOfTs: null,
3005
3782
  stale: null,
3006
- value: null,
3007
- valueChange24h: null,
3008
- valueChange7d: null,
3009
- classification: null,
3010
- sentimentRegime: null
3783
+ trades: null,
3784
+ buyPressurePct: null,
3785
+ netBaseDelta: null,
3786
+ netQuoteDelta: null
3011
3787
  };
3012
3788
  }
3013
3789
  return {
3014
- source: String(cmcFearGreed.source ?? ""),
3790
+ source: String(tradeFlow.source ?? ""),
3015
3791
  available: true,
3016
- interval: typeof cmcFearGreed.interval === "string" ? cmcFearGreed.interval : null,
3017
- asOfTs: toFiniteNumber(cmcFearGreed.asOfTs),
3018
- stale: typeof cmcFearGreed.stale === "boolean" ? cmcFearGreed.stale : null,
3019
- value: toFiniteNumber(cmcFearGreed.value),
3020
- valueChange24h: toFiniteNumber(cmcFearGreed.valueChange24h),
3021
- valueChange7d: toFiniteNumber(cmcFearGreed.valueChange7d),
3022
- classification: typeof cmcFearGreed.classification === "string" ? cmcFearGreed.classification : null,
3023
- sentimentRegime: typeof cmcFearGreed.sentimentRegime === "string" ? cmcFearGreed.sentimentRegime : null
3792
+ interval: String(tradeFlow.interval ?? ""),
3793
+ stale: typeof tradeFlow.stale === "boolean" ? tradeFlow.stale : null,
3794
+ trades: toFiniteNumber(tradeFlow.trades),
3795
+ buyPressurePct: toFiniteNumber(tradeFlow.buyPressurePct),
3796
+ netBaseDelta: toFiniteNumber(tradeFlow.netBaseDelta),
3797
+ netQuoteDelta: toFiniteNumber(tradeFlow.netQuoteDelta)
3024
3798
  };
3025
3799
  };
3026
- var buildCmcIndexesContextFromSignal = (signal) => {
3800
+ var buildMarketBreadthContextFromSignal = (signal) => {
3027
3801
  const baseContext = toRecord(signal.additionalIndicators?.baseContext);
3028
3802
  const relative = toRecord(baseContext?.relative);
3029
- const cmcIndexes = toRecord(relative?.cmcIndexes);
3030
- if (!cmcIndexes) {
3803
+ const breadth = toRecord(relative?.marketBreadth);
3804
+ if (!breadth) {
3031
3805
  return {
3032
3806
  source: null,
3033
3807
  available: false,
3808
+ universe: null,
3034
3809
  interval: null,
3035
- asOfTs: null,
3036
3810
  stale: null,
3037
- cmc100Value: null,
3038
- cmc100Change24hPct: null,
3039
- cmc100TopConstituentSymbol: null,
3040
- cmc100TopConstituentWeightPct: null,
3041
- cmc20Value: null,
3042
- cmc20Change24hPct: null,
3043
- cmc20TopConstituentSymbol: null,
3044
- cmc20TopConstituentWeightPct: null,
3045
- cmc20ToCmc100Ratio: null,
3046
- cmc20ToCmc100RatioChange24hPct: null,
3047
- indexRegime: null
3811
+ symbolsCount: null,
3812
+ advanceDeclineRatio: null,
3813
+ pctAboveMa20: null,
3814
+ pctAboveMa50: null,
3815
+ equalWeightedReturn: null,
3816
+ volumeWeightedReturn: null,
3817
+ dispersion: null
3048
3818
  };
3049
3819
  }
3050
3820
  return {
3051
- source: String(cmcIndexes.source ?? ""),
3821
+ source: String(breadth.source ?? ""),
3052
3822
  available: true,
3053
- interval: typeof cmcIndexes.interval === "string" ? cmcIndexes.interval : null,
3054
- asOfTs: toFiniteNumber(cmcIndexes.asOfTs),
3055
- stale: typeof cmcIndexes.stale === "boolean" ? cmcIndexes.stale : null,
3056
- cmc100Value: toFiniteNumber(cmcIndexes.cmc100Value),
3057
- cmc100Change24hPct: toFiniteNumber(cmcIndexes.cmc100Change24hPct),
3058
- cmc100TopConstituentSymbol: typeof cmcIndexes.cmc100TopConstituentSymbol === "string" ? cmcIndexes.cmc100TopConstituentSymbol : null,
3059
- cmc100TopConstituentWeightPct: toFiniteNumber(
3060
- cmcIndexes.cmc100TopConstituentWeightPct
3061
- ),
3062
- cmc20Value: toFiniteNumber(cmcIndexes.cmc20Value),
3063
- cmc20Change24hPct: toFiniteNumber(cmcIndexes.cmc20Change24hPct),
3064
- cmc20TopConstituentSymbol: typeof cmcIndexes.cmc20TopConstituentSymbol === "string" ? cmcIndexes.cmc20TopConstituentSymbol : null,
3065
- cmc20TopConstituentWeightPct: toFiniteNumber(
3066
- cmcIndexes.cmc20TopConstituentWeightPct
3067
- ),
3068
- cmc20ToCmc100Ratio: toFiniteNumber(cmcIndexes.cmc20ToCmc100Ratio),
3069
- cmc20ToCmc100RatioChange24hPct: toFiniteNumber(
3070
- cmcIndexes.cmc20ToCmc100RatioChange24hPct
3071
- ),
3072
- indexRegime: typeof cmcIndexes.indexRegime === "string" ? cmcIndexes.indexRegime : null
3823
+ universe: String(breadth.universe ?? ""),
3824
+ interval: String(breadth.interval ?? ""),
3825
+ stale: typeof breadth.stale === "boolean" ? breadth.stale : null,
3826
+ symbolsCount: toFiniteNumber(breadth.symbolsCount),
3827
+ advanceDeclineRatio: toFiniteNumber(breadth.advanceDeclineRatio),
3828
+ pctAboveMa20: toFiniteNumber(breadth.pctAboveMa20),
3829
+ pctAboveMa50: toFiniteNumber(breadth.pctAboveMa50),
3830
+ equalWeightedReturn: toFiniteNumber(breadth.equalWeightedReturn),
3831
+ volumeWeightedReturn: toFiniteNumber(breadth.volumeWeightedReturn),
3832
+ dispersion: toFiniteNumber(breadth.dispersion)
3073
3833
  };
3074
3834
  };
3075
- var buildReferenceTradeFlowContextFromSignal = (signal) => {
3835
+ var buildMarketBreadthsContextFromSignal = (signal) => {
3076
3836
  const baseContext = toRecord(signal.additionalIndicators?.baseContext);
3077
3837
  const relative = toRecord(baseContext?.relative);
3078
- const refs = toRecord(relative?.referenceTradeFlow);
3079
- const primaryReferenceSymbol = typeof refs?.primaryReferenceSymbol === "string" ? refs.primaryReferenceSymbol : null;
3080
- const tradeFlowBySymbol = toRecord(refs?.tradeFlowBySymbol);
3081
- const primaryTradeFlow = primaryReferenceSymbol != null ? toRecord(tradeFlowBySymbol?.[primaryReferenceSymbol]) : null;
3082
- if (!refs) {
3838
+ const breadths = toRecord(relative?.marketBreadths);
3839
+ const build = (key) => {
3840
+ const breadth = toRecord(breadths?.[key]);
3841
+ if (!breadth) {
3842
+ return {
3843
+ source: null,
3844
+ available: false,
3845
+ universe: null,
3846
+ interval: null,
3847
+ stale: null,
3848
+ symbolsCount: null,
3849
+ advanceDeclineRatio: null,
3850
+ pctAboveMa20: null,
3851
+ pctAboveMa50: null,
3852
+ equalWeightedReturn: null,
3853
+ volumeWeightedReturn: null,
3854
+ dispersion: null
3855
+ };
3856
+ }
3083
3857
  return {
3084
- source: null,
3085
- available: false,
3086
- primaryReferenceSymbol: null,
3087
- referenceSymbols: [],
3088
- primaryTradeFlowBuyPressurePct: null,
3089
- primaryTradeFlowStale: null
3858
+ source: String(breadth.source ?? ""),
3859
+ available: true,
3860
+ universe: String(breadth.universe ?? ""),
3861
+ interval: String(breadth.interval ?? ""),
3862
+ stale: typeof breadth.stale === "boolean" ? breadth.stale : null,
3863
+ symbolsCount: toFiniteNumber(breadth.symbolsCount),
3864
+ advanceDeclineRatio: toFiniteNumber(breadth.advanceDeclineRatio),
3865
+ pctAboveMa20: toFiniteNumber(breadth.pctAboveMa20),
3866
+ pctAboveMa50: toFiniteNumber(breadth.pctAboveMa50),
3867
+ equalWeightedReturn: toFiniteNumber(breadth.equalWeightedReturn),
3868
+ volumeWeightedReturn: toFiniteNumber(breadth.volumeWeightedReturn),
3869
+ dispersion: toFiniteNumber(breadth.dispersion)
3090
3870
  };
3091
- }
3092
- return {
3093
- source: String(refs.source ?? ""),
3094
- available: true,
3095
- primaryReferenceSymbol,
3096
- referenceSymbols: Array.isArray(refs.referenceSymbols) ? refs.referenceSymbols.map(String) : [],
3097
- primaryTradeFlowBuyPressurePct: toFiniteNumber(
3098
- primaryTradeFlow?.buyPressurePct
3099
- ),
3100
- primaryTradeFlowStale: typeof primaryTradeFlow?.stale === "boolean" ? primaryTradeFlow.stale : null
3101
3871
  };
3102
- };
3103
- var buildAiMarketContext = (signal) => ({
3104
- execution: {
3105
- binanceCoinbaseSpread: buildSpreadContextFromSignal(signal)
3106
- },
3107
- participation: {
3108
- trueDelta: buildTrueDeltaContextFromSignal(signal),
3109
- tradeFlow: buildTradeFlowContextFromSignal(signal)
3110
- },
3111
- relative: {
3112
- marketBreadth: buildMarketBreadthContextFromSignal(signal),
3113
- marketBreadths: buildMarketBreadthsContextFromSignal(signal),
3114
- targetVsBtc: buildTargetVsBtcContextFromSignal(signal),
3115
- btcAltRegime: buildBtcAltRegimeContextFromSignal(signal),
3116
- cmcGlobal: buildCmcGlobalContextFromSignal(signal),
3117
- cmcReferenceAssets: buildCmcReferenceAssetsContextFromSignal(signal),
3118
- cmcExchangeLiquidity: buildCmcExchangeLiquidityContextFromSignal(signal),
3119
- cmcFearGreed: buildCmcFearGreedContextFromSignal(signal),
3120
- cmcIndexes: buildCmcIndexesContextFromSignal(signal),
3121
- referenceTradeFlow: buildReferenceTradeFlowContextFromSignal(signal)
3122
- }
3123
- });
3124
-
3125
- // src/strategy/manifests.ts
3126
- var import_indicators = require("@tradejs/core/indicators");
3127
- var SHARED_STRATEGY_REGISTRY_KEY = "__tradejsNodeSharedStrategyRegistryV1__";
3128
- var sharedRegistryScope = globalThis;
3129
- var sharedStrategyRegistry = sharedRegistryScope[SHARED_STRATEGY_REGISTRY_KEY] ?? (sharedRegistryScope[SHARED_STRATEGY_REGISTRY_KEY] = {
3130
- registryStateByProjectRoot: /* @__PURE__ */ new Map()
3131
- });
3132
- var createStrategyRegistryState = () => ({
3133
- strategyCreators: /* @__PURE__ */ new Map(),
3134
- strategyManifestsMap: /* @__PURE__ */ new Map(),
3135
- strategyEntriesMap: /* @__PURE__ */ new Map(),
3136
- strategySourcesMap: /* @__PURE__ */ new Map(),
3137
- strategyModuleResolutions: /* @__PURE__ */ new Map(),
3138
- pluginsLoadPromise: null
3139
- });
3140
- var registryStateByProjectRoot2 = sharedStrategyRegistry.registryStateByProjectRoot;
3141
- var getStrategyRegistryState = (cwd = getTradejsProjectCwd()) => {
3142
- const projectRoot = getTradejsProjectCwd(cwd);
3143
- let state = registryStateByProjectRoot2.get(projectRoot);
3144
- if (!state) {
3145
- state = createStrategyRegistryState();
3146
- registryStateByProjectRoot2.set(projectRoot, state);
3147
- }
3148
3872
  return {
3149
- projectRoot,
3150
- state
3873
+ top5: build("top5"),
3874
+ top10: build("top10"),
3875
+ top30: build("top30"),
3876
+ top50: build("top50"),
3877
+ top100: build("top100")
3151
3878
  };
3152
3879
  };
3153
- var toUniqueModules2 = (modules = []) => [
3154
- ...new Set(modules.map((moduleName) => moduleName.trim()).filter(Boolean))
3155
- ];
3156
- var getConfiguredPluginModuleNames = async (cwd = getTradejsProjectCwd()) => {
3157
- const config = await loadTradejsConfig(cwd);
3880
+ var buildTargetVsReference = (signal, asset) => {
3881
+ const base = toRecord(signal.additionalIndicators?.baseContext);
3882
+ const relative = toRecord(base?.relative);
3883
+ const target = toRecord(relative?.[`targetVs${asset}`]);
3158
3884
  return {
3159
- strategyModules: toUniqueModules2(config.strategies),
3160
- indicatorModules: toUniqueModules2(config.indicators)
3885
+ source: target ? String(target.source ?? "") : null,
3886
+ available: Boolean(target),
3887
+ ratioReturn1h: toFiniteNumber(target?.ratioReturn1h),
3888
+ ratioReturn4h: toFiniteNumber(target?.ratioReturn4h),
3889
+ ratioReturn24h: toFiniteNumber(target?.ratioReturn24h),
3890
+ alpha1h: toFiniteNumber(target?.[`alphaVs${asset}1h`]),
3891
+ alpha4h: toFiniteNumber(target?.[`alphaVs${asset}4h`]),
3892
+ alpha24h: toFiniteNumber(target?.[`alphaVs${asset}24h`]),
3893
+ beta: toFiniteNumber(target?.[`betaTo${asset}20`]),
3894
+ correlation: toFiniteNumber(target?.[`correlationTo${asset}20`]),
3895
+ ratioTrend: typeof target?.ratioTrend === "string" ? target.ratioTrend : null
3161
3896
  };
3162
3897
  };
3163
- var extractModuleEntries = (moduleExport, key) => {
3164
- if (!moduleExport || typeof moduleExport !== "object") {
3165
- return null;
3166
- }
3167
- const candidate = moduleExport;
3168
- if (Array.isArray(candidate[key])) {
3169
- return candidate[key];
3170
- }
3171
- const defaultExport = candidate.default;
3172
- if (defaultExport && Array.isArray(defaultExport[key])) {
3173
- return defaultExport[key];
3174
- }
3175
- return null;
3176
- };
3177
- var extractStrategyPluginDefinition = (moduleExport) => {
3178
- const strategyEntries = extractModuleEntries(
3179
- moduleExport,
3180
- "strategyEntries"
3181
- );
3182
- return strategyEntries ? { strategyEntries } : null;
3183
- };
3184
- var extractIndicatorPluginDefinition = (moduleExport) => {
3185
- const indicatorEntries = extractModuleEntries(
3186
- moduleExport,
3187
- "indicatorEntries"
3188
- );
3189
- return indicatorEntries ? { indicatorEntries } : null;
3190
- };
3191
- var validateStrategyEntries = (moduleName, entries, state) => {
3192
- const issues = [];
3193
- const names = /* @__PURE__ */ new Set();
3194
- entries.forEach((entry, index) => {
3195
- const entryPath = `${moduleName}.strategyEntries[${index}]`;
3196
- const strategyName = entry?.manifest?.name;
3197
- if (typeof strategyName !== "string" || !strategyName.trim()) {
3198
- issues.push(`${entryPath}: manifest.name is required`);
3199
- } else if (names.has(strategyName) || state?.strategyEntriesMap.has(strategyName)) {
3200
- issues.push(`${entryPath}: duplicate strategy ${strategyName}`);
3201
- } else {
3202
- names.add(strategyName);
3203
- }
3204
- if (!entry || typeof entry !== "object" || !entry.defaults || typeof entry.defaults !== "object" || Array.isArray(entry.defaults)) {
3205
- issues.push(`${entryPath}: defaults must be an object`);
3206
- }
3207
- if (typeof entry?.parseConfig !== "function") {
3208
- issues.push(`${entryPath}: parseConfig is required`);
3209
- }
3210
- if (typeof entry?.createCore !== "function") {
3211
- issues.push(`${entryPath}: createCore is required`);
3212
- }
3213
- });
3214
- return issues;
3215
- };
3216
- var registerEntries2 = (entries, source, state) => {
3217
- for (const entry of entries) {
3218
- const strategyName = entry.manifest.name;
3219
- state.strategyManifestsMap.set(strategyName, entry.manifest);
3220
- state.strategyEntriesMap.set(strategyName, entry);
3221
- state.strategySourcesMap.set(strategyName, source);
3222
- materializeStrategyCreator(strategyName, state);
3223
- }
3224
- };
3225
- var materializeStrategyCreator = (strategyName, state) => {
3226
- if (state.strategyCreators.has(strategyName) || !sharedStrategyRegistry.strategyRuntimeFactory) {
3227
- return;
3228
- }
3229
- const entry = state.strategyEntriesMap.get(strategyName);
3230
- if (!entry) return;
3231
- state.strategyCreators.set(
3232
- strategyName,
3233
- sharedStrategyRegistry.strategyRuntimeFactory({
3234
- strategyName,
3235
- defaults: entry.defaults,
3236
- createCore: entry.createCore,
3237
- manifest: entry.manifest,
3238
- detectorKey: entry.detectorKey,
3239
- detectorNoSignalSkipReason: entry.detectorNoSignalSkipReason,
3240
- resolveRegisteredManifest: (name) => state.strategyManifestsMap.get(name)
3241
- })
3242
- );
3898
+ var buildTargetVsBtcContextFromSignal = (signal) => {
3899
+ const { alpha1h, alpha4h, alpha24h, beta, correlation, ...shared } = buildTargetVsReference(signal, "Btc");
3900
+ return {
3901
+ ...shared,
3902
+ alphaVsBtc1h: alpha1h,
3903
+ alphaVsBtc4h: alpha4h,
3904
+ alphaVsBtc24h: alpha24h,
3905
+ betaToBtc20: beta,
3906
+ correlationToBtc20: correlation
3907
+ };
3243
3908
  };
3244
- var setStrategyRuntimeFactory = (factory) => {
3245
- sharedStrategyRegistry.strategyRuntimeFactory = factory;
3246
- for (const state of registryStateByProjectRoot2.values()) {
3247
- for (const strategyName of state.strategyEntriesMap.keys()) {
3248
- materializeStrategyCreator(strategyName, state);
3249
- }
3250
- }
3909
+ var buildTargetVsEthContextFromSignal = (signal) => {
3910
+ const { alpha1h, alpha4h, alpha24h, beta, correlation, ...shared } = buildTargetVsReference(signal, "Eth");
3911
+ return {
3912
+ ...shared,
3913
+ alphaVsEth1h: alpha1h,
3914
+ alphaVsEth4h: alpha4h,
3915
+ alphaVsEth24h: alpha24h,
3916
+ betaToEth20: beta,
3917
+ correlationToEth20: correlation
3918
+ };
3251
3919
  };
3252
- var importStrategyPluginModule = async (moduleName, cwd = getTradejsProjectCwd()) => {
3253
- if (typeof importTradejsModule === "function") {
3254
- return importTradejsModule(moduleName, cwd);
3920
+ var buildBtcAltRegimeContextFromSignal = (signal) => {
3921
+ const baseContext = toRecord(signal.additionalIndicators?.baseContext);
3922
+ const relative = toRecord(baseContext?.relative);
3923
+ const btcAltRegime = toRecord(relative?.btcAltRegime);
3924
+ if (!btcAltRegime) {
3925
+ return {
3926
+ source: null,
3927
+ available: false,
3928
+ universe: null,
3929
+ interval: null,
3930
+ stale: null,
3931
+ regime: null,
3932
+ btcReturn24h: null,
3933
+ altBasketReturn24h: null,
3934
+ btcVsAltReturn24h: null,
3935
+ btcTurnoverShare24h: null,
3936
+ btcTurnoverShareChange24h: null,
3937
+ altVolToBtcVol24h: null,
3938
+ altDispersion24h: null
3939
+ };
3255
3940
  }
3256
- return import(
3257
- /* webpackIgnore: true */
3258
- moduleName
3259
- );
3941
+ return {
3942
+ source: String(btcAltRegime.source ?? ""),
3943
+ available: true,
3944
+ universe: String(btcAltRegime.universe ?? ""),
3945
+ interval: String(btcAltRegime.interval ?? ""),
3946
+ stale: typeof btcAltRegime.stale === "boolean" ? btcAltRegime.stale : null,
3947
+ regime: typeof btcAltRegime.regime === "string" ? btcAltRegime.regime : null,
3948
+ btcReturn24h: toFiniteNumber(btcAltRegime.btcReturn24h),
3949
+ altBasketReturn24h: toFiniteNumber(btcAltRegime.altBasketReturn24h),
3950
+ btcVsAltReturn24h: toFiniteNumber(btcAltRegime.btcVsAltReturn24h),
3951
+ btcTurnoverShare24h: toFiniteNumber(btcAltRegime.btcTurnoverShare24h),
3952
+ btcTurnoverShareChange24h: toFiniteNumber(
3953
+ btcAltRegime.btcTurnoverShareChange24h
3954
+ ),
3955
+ altVolToBtcVol24h: toFiniteNumber(btcAltRegime.altVolToBtcVol24h),
3956
+ altDispersion24h: toFiniteNumber(btcAltRegime.altDispersion24h)
3957
+ };
3260
3958
  };
3261
- var resolveStrategyModuleEntry = async ({
3262
- strategyName,
3263
- moduleName,
3264
- cwd
3265
- }) => {
3266
- const { projectRoot, state } = getStrategyRegistryState(cwd);
3267
- const source = moduleName.trim();
3268
- const resolvedModuleName = resolvePluginModuleSpecifier(source, projectRoot);
3269
- const cacheKey = `${resolvedModuleName}\0${strategyName}`;
3270
- const cached = state.strategyModuleResolutions.get(cacheKey);
3271
- if (cached) return cached;
3272
- const resolution = (async () => {
3273
- const moduleExport = await importStrategyPluginModule(
3274
- resolvedModuleName,
3275
- projectRoot
3276
- );
3277
- const pluginDefinition = extractStrategyPluginDefinition(moduleExport);
3278
- if (!pluginDefinition) {
3279
- throw new Error(`${source}: export { strategyEntries } is missing`);
3280
- }
3281
- const issues = validateStrategyEntries(
3282
- source,
3283
- pluginDefinition.strategyEntries
3284
- );
3285
- if (issues.length > 0) {
3286
- throw new Error(
3287
- ["Invalid TradeJS strategy module:", ...issues].join("\n")
3288
- );
3289
- }
3290
- const entry = pluginDefinition.strategyEntries.find(
3291
- (candidate) => candidate.manifest.name === strategyName
3292
- );
3293
- if (!entry) {
3294
- throw new Error(`${source}: strategy ${strategyName} is missing`);
3295
- }
3296
- const factory = sharedStrategyRegistry.strategyRuntimeFactory;
3297
- if (!factory) {
3298
- throw new Error("Strategy runtime factory is not configured");
3299
- }
3300
- const manifestsByName = new Map(
3301
- pluginDefinition.strategyEntries.map(
3302
- (candidate) => [candidate.manifest.name, candidate.manifest]
3303
- )
3304
- );
3959
+ var buildCmcGlobalContextFromSignal = (signal) => {
3960
+ const baseContext = toRecord(signal.additionalIndicators?.baseContext);
3961
+ const relative = toRecord(baseContext?.relative);
3962
+ const cmcGlobal = toRecord(relative?.cmcGlobal);
3963
+ if (!cmcGlobal) {
3305
3964
  return {
3306
- entry,
3307
- creator: factory({
3308
- strategyName,
3309
- defaults: entry.defaults,
3310
- createCore: entry.createCore,
3311
- manifest: entry.manifest,
3312
- detectorKey: entry.detectorKey,
3313
- detectorNoSignalSkipReason: entry.detectorNoSignalSkipReason,
3314
- resolveRegisteredManifest: (name) => manifestsByName.get(name) ?? state.strategyManifestsMap.get(name)
3315
- }),
3316
- source
3965
+ source: null,
3966
+ available: false,
3967
+ interval: null,
3968
+ asOfTs: null,
3969
+ stale: null,
3970
+ totalMarketCapUsd: null,
3971
+ totalVolumeUsd: null,
3972
+ totalVolumeReportedUsd: null,
3973
+ altMarketCapUsd: null,
3974
+ altVolumeUsd: null,
3975
+ altVolumeReportedUsd: null,
3976
+ btcDominancePct: null,
3977
+ ethDominancePct: null,
3978
+ btcDominanceChange24hPct: null,
3979
+ ethDominanceChange24hPct: null,
3980
+ altMarketCapChange24hPct: null,
3981
+ altVolumeChange24hPct: null,
3982
+ activeCryptocurrencies: null,
3983
+ activeExchanges: null,
3984
+ activeMarketPairs: null,
3985
+ altLiquidityRegime: null
3317
3986
  };
3318
- })();
3319
- state.strategyModuleResolutions.set(cacheKey, resolution);
3320
- try {
3321
- return await resolution;
3322
- } catch (error) {
3323
- state.strategyModuleResolutions.delete(cacheKey);
3324
- throw error;
3325
3987
  }
3988
+ return {
3989
+ source: String(cmcGlobal.source ?? ""),
3990
+ available: true,
3991
+ interval: typeof cmcGlobal.interval === "string" ? cmcGlobal.interval : null,
3992
+ asOfTs: toFiniteNumber(cmcGlobal.asOfTs),
3993
+ stale: typeof cmcGlobal.stale === "boolean" ? cmcGlobal.stale : null,
3994
+ totalMarketCapUsd: toFiniteNumber(cmcGlobal.totalMarketCapUsd),
3995
+ totalVolumeUsd: toFiniteNumber(cmcGlobal.totalVolumeUsd),
3996
+ totalVolumeReportedUsd: toFiniteNumber(cmcGlobal.totalVolumeReportedUsd),
3997
+ altMarketCapUsd: toFiniteNumber(cmcGlobal.altMarketCapUsd),
3998
+ altVolumeUsd: toFiniteNumber(cmcGlobal.altVolumeUsd),
3999
+ altVolumeReportedUsd: toFiniteNumber(cmcGlobal.altVolumeReportedUsd),
4000
+ btcDominancePct: toFiniteNumber(cmcGlobal.btcDominancePct),
4001
+ ethDominancePct: toFiniteNumber(cmcGlobal.ethDominancePct),
4002
+ btcDominanceChange24hPct: toFiniteNumber(
4003
+ cmcGlobal.btcDominanceChange24hPct
4004
+ ),
4005
+ ethDominanceChange24hPct: toFiniteNumber(
4006
+ cmcGlobal.ethDominanceChange24hPct
4007
+ ),
4008
+ altMarketCapChange24hPct: toFiniteNumber(
4009
+ cmcGlobal.altMarketCapChange24hPct
4010
+ ),
4011
+ altVolumeChange24hPct: toFiniteNumber(cmcGlobal.altVolumeChange24hPct),
4012
+ activeCryptocurrencies: toFiniteNumber(cmcGlobal.activeCryptocurrencies),
4013
+ activeExchanges: toFiniteNumber(cmcGlobal.activeExchanges),
4014
+ activeMarketPairs: toFiniteNumber(cmcGlobal.activeMarketPairs),
4015
+ altLiquidityRegime: typeof cmcGlobal.altLiquidityRegime === "string" ? cmcGlobal.altLiquidityRegime : null
4016
+ };
3326
4017
  };
3327
- var ensureStrategyPluginsLoaded = async (cwd = getTradejsProjectCwd()) => {
3328
- const { projectRoot, state } = getStrategyRegistryState(cwd);
3329
- if (!state.pluginsLoadPromise) {
3330
- (0, import_indicators.resetIndicatorRegistryCache)(projectRoot);
3331
- state.pluginsLoadPromise = (async () => {
3332
- const { strategyModules, indicatorModules } = await getConfiguredPluginModuleNames(projectRoot);
3333
- const strategySet = new Set(strategyModules);
3334
- const indicatorSet = new Set(indicatorModules);
3335
- const pluginModuleNames = [
3336
- .../* @__PURE__ */ new Set([...strategyModules, ...indicatorModules])
3337
- ];
3338
- if (!pluginModuleNames.length) {
3339
- return;
3340
- }
3341
- const issues = [];
3342
- for (const moduleName of pluginModuleNames) {
3343
- try {
3344
- const resolvedModuleName = resolvePluginModuleSpecifier(
3345
- moduleName,
3346
- projectRoot
3347
- );
3348
- const moduleExport = await importStrategyPluginModule(
3349
- resolvedModuleName,
3350
- projectRoot
3351
- );
3352
- if (strategySet.has(moduleName)) {
3353
- const pluginDefinition = extractStrategyPluginDefinition(moduleExport);
3354
- if (!pluginDefinition) {
3355
- issues.push(
3356
- `${moduleName}: export { strategyEntries } is missing`
3357
- );
3358
- } else {
3359
- const entryIssues = validateStrategyEntries(
3360
- moduleName,
3361
- pluginDefinition.strategyEntries,
3362
- state
3363
- );
3364
- issues.push(...entryIssues);
3365
- if (entryIssues.length === 0) {
3366
- registerEntries2(
3367
- pluginDefinition.strategyEntries,
3368
- moduleName,
3369
- state
3370
- );
3371
- }
3372
- }
3373
- }
3374
- if (indicatorSet.has(moduleName)) {
3375
- const indicatorPluginDefinition = extractIndicatorPluginDefinition(moduleExport);
3376
- if (!indicatorPluginDefinition) {
3377
- issues.push(
3378
- `${moduleName}: export { indicatorEntries } is missing`
3379
- );
3380
- } else {
3381
- (0, import_indicators.registerIndicatorEntries)(
3382
- indicatorPluginDefinition.indicatorEntries,
3383
- moduleName,
3384
- projectRoot
3385
- );
3386
- }
3387
- }
3388
- if (!strategySet.has(moduleName) && !indicatorSet.has(moduleName)) {
3389
- issues.push(`${moduleName}: plugin is not declared in config`);
3390
- }
3391
- } catch (error) {
3392
- issues.push(`${moduleName}: failed to import: ${String(error)}`);
3393
- }
3394
- }
3395
- if (issues.length > 0) {
3396
- throw new Error(
3397
- ["Invalid TradeJS plugin catalog:", ...issues].join("\n")
3398
- );
3399
- }
3400
- })();
4018
+ var buildCmcReferenceAssetsContextFromSignal = (signal) => {
4019
+ const baseContext = toRecord(signal.additionalIndicators?.baseContext);
4020
+ const relative = toRecord(baseContext?.relative);
4021
+ const cmcReferenceAssets = toRecord(relative?.cmcReferenceAssets);
4022
+ if (!cmcReferenceAssets) {
4023
+ return {
4024
+ source: null,
4025
+ available: false,
4026
+ interval: null,
4027
+ asOfTs: null,
4028
+ stale: null,
4029
+ btcMarketCapUsd: null,
4030
+ ethMarketCapUsd: null,
4031
+ btcVolumeUsd: null,
4032
+ ethVolumeUsd: null,
4033
+ btcVolumeToMarketCap: null,
4034
+ ethVolumeToMarketCap: null,
4035
+ ethBtcMarketCapRatio: null,
4036
+ ethBtcMarketCapRatioChange24hPct: null,
4037
+ ethVsBtcVolumeRatio: null,
4038
+ referenceLiquidityRegime: null
4039
+ };
3401
4040
  }
3402
- await state.pluginsLoadPromise;
4041
+ return {
4042
+ source: String(cmcReferenceAssets.source ?? ""),
4043
+ available: true,
4044
+ interval: typeof cmcReferenceAssets.interval === "string" ? cmcReferenceAssets.interval : null,
4045
+ asOfTs: toFiniteNumber(cmcReferenceAssets.asOfTs),
4046
+ stale: typeof cmcReferenceAssets.stale === "boolean" ? cmcReferenceAssets.stale : null,
4047
+ btcMarketCapUsd: toFiniteNumber(cmcReferenceAssets.btcMarketCapUsd),
4048
+ ethMarketCapUsd: toFiniteNumber(cmcReferenceAssets.ethMarketCapUsd),
4049
+ btcVolumeUsd: toFiniteNumber(cmcReferenceAssets.btcVolumeUsd),
4050
+ ethVolumeUsd: toFiniteNumber(cmcReferenceAssets.ethVolumeUsd),
4051
+ btcVolumeToMarketCap: toFiniteNumber(
4052
+ cmcReferenceAssets.btcVolumeToMarketCap
4053
+ ),
4054
+ ethVolumeToMarketCap: toFiniteNumber(
4055
+ cmcReferenceAssets.ethVolumeToMarketCap
4056
+ ),
4057
+ ethBtcMarketCapRatio: toFiniteNumber(
4058
+ cmcReferenceAssets.ethBtcMarketCapRatio
4059
+ ),
4060
+ ethBtcMarketCapRatioChange24hPct: toFiniteNumber(
4061
+ cmcReferenceAssets.ethBtcMarketCapRatioChange24hPct
4062
+ ),
4063
+ ethVsBtcVolumeRatio: toFiniteNumber(cmcReferenceAssets.ethVsBtcVolumeRatio),
4064
+ referenceLiquidityRegime: typeof cmcReferenceAssets.referenceLiquidityRegime === "string" ? cmcReferenceAssets.referenceLiquidityRegime : null
4065
+ };
3403
4066
  };
3404
- var getStrategyCreator = async (name, cwd = getTradejsProjectCwd(), moduleName) => {
3405
- if (moduleName?.trim()) {
3406
- await ensureStrategyPluginsLoaded(cwd);
3407
- return (await resolveStrategyModuleEntry({ strategyName: name, moduleName, cwd })).creator;
4067
+ var buildCmcExchangeLiquidityContextFromSignal = (signal) => {
4068
+ const baseContext = toRecord(signal.additionalIndicators?.baseContext);
4069
+ const relative = toRecord(baseContext?.relative);
4070
+ const cmcExchangeLiquidity = toRecord(relative?.cmcExchangeLiquidity);
4071
+ if (!cmcExchangeLiquidity) {
4072
+ return {
4073
+ source: null,
4074
+ available: false,
4075
+ interval: null,
4076
+ asOfTs: null,
4077
+ stale: null,
4078
+ exchangesCount: null,
4079
+ totalVolumeUsd: null,
4080
+ totalVolumeChange24hPct: null,
4081
+ binanceVolumeUsd: null,
4082
+ binanceVolumeShare: null,
4083
+ topExchangeVolumeShare: null,
4084
+ liquidityRegime: null
4085
+ };
3408
4086
  }
3409
- await ensureStrategyPluginsLoaded(cwd);
3410
- const { state } = getStrategyRegistryState(cwd);
3411
- return state.strategyCreators.get(name);
4087
+ return {
4088
+ source: String(cmcExchangeLiquidity.source ?? ""),
4089
+ available: true,
4090
+ interval: typeof cmcExchangeLiquidity.interval === "string" ? cmcExchangeLiquidity.interval : null,
4091
+ asOfTs: toFiniteNumber(cmcExchangeLiquidity.asOfTs),
4092
+ stale: typeof cmcExchangeLiquidity.stale === "boolean" ? cmcExchangeLiquidity.stale : null,
4093
+ exchangesCount: toFiniteNumber(cmcExchangeLiquidity.exchangesCount),
4094
+ totalVolumeUsd: toFiniteNumber(cmcExchangeLiquidity.totalVolumeUsd),
4095
+ totalVolumeChange24hPct: toFiniteNumber(
4096
+ cmcExchangeLiquidity.totalVolumeChange24hPct
4097
+ ),
4098
+ binanceVolumeUsd: toFiniteNumber(cmcExchangeLiquidity.binanceVolumeUsd),
4099
+ binanceVolumeShare: toFiniteNumber(cmcExchangeLiquidity.binanceVolumeShare),
4100
+ topExchangeVolumeShare: toFiniteNumber(
4101
+ cmcExchangeLiquidity.topExchangeVolumeShare
4102
+ ),
4103
+ liquidityRegime: typeof cmcExchangeLiquidity.liquidityRegime === "string" ? cmcExchangeLiquidity.liquidityRegime : null
4104
+ };
3412
4105
  };
3413
- var getStrategyEntry = async (name, cwd = getTradejsProjectCwd(), moduleName) => {
3414
- if (moduleName?.trim()) {
3415
- await ensureStrategyPluginsLoaded(cwd);
3416
- return (await resolveStrategyModuleEntry({ strategyName: name, moduleName, cwd })).entry;
4106
+ var buildCmcFearGreedContextFromSignal = (signal) => {
4107
+ const baseContext = toRecord(signal.additionalIndicators?.baseContext);
4108
+ const relative = toRecord(baseContext?.relative);
4109
+ const cmcFearGreed = toRecord(relative?.cmcFearGreed);
4110
+ if (!cmcFearGreed) {
4111
+ return {
4112
+ source: null,
4113
+ available: false,
4114
+ interval: null,
4115
+ asOfTs: null,
4116
+ stale: null,
4117
+ value: null,
4118
+ valueChange24h: null,
4119
+ valueChange7d: null,
4120
+ classification: null,
4121
+ sentimentRegime: null
4122
+ };
3417
4123
  }
3418
- await ensureStrategyPluginsLoaded(cwd);
3419
- const { state } = getStrategyRegistryState(cwd);
3420
- return state.strategyEntriesMap.get(name);
4124
+ return {
4125
+ source: String(cmcFearGreed.source ?? ""),
4126
+ available: true,
4127
+ interval: typeof cmcFearGreed.interval === "string" ? cmcFearGreed.interval : null,
4128
+ asOfTs: toFiniteNumber(cmcFearGreed.asOfTs),
4129
+ stale: typeof cmcFearGreed.stale === "boolean" ? cmcFearGreed.stale : null,
4130
+ value: toFiniteNumber(cmcFearGreed.value),
4131
+ valueChange24h: toFiniteNumber(cmcFearGreed.valueChange24h),
4132
+ valueChange7d: toFiniteNumber(cmcFearGreed.valueChange7d),
4133
+ classification: typeof cmcFearGreed.classification === "string" ? cmcFearGreed.classification : null,
4134
+ sentimentRegime: typeof cmcFearGreed.sentimentRegime === "string" ? cmcFearGreed.sentimentRegime : null
4135
+ };
3421
4136
  };
3422
- var getStrategyPluginSource = async (name, cwd = getTradejsProjectCwd(), moduleName) => {
3423
- if (moduleName?.trim()) {
3424
- await ensureStrategyPluginsLoaded(cwd);
3425
- return (await resolveStrategyModuleEntry({ strategyName: name, moduleName, cwd })).source;
4137
+ var buildCmcIndexesContextFromSignal = (signal) => {
4138
+ const baseContext = toRecord(signal.additionalIndicators?.baseContext);
4139
+ const relative = toRecord(baseContext?.relative);
4140
+ const cmcIndexes = toRecord(relative?.cmcIndexes);
4141
+ if (!cmcIndexes) {
4142
+ return {
4143
+ source: null,
4144
+ available: false,
4145
+ interval: null,
4146
+ asOfTs: null,
4147
+ stale: null,
4148
+ cmc100Value: null,
4149
+ cmc100Change24hPct: null,
4150
+ cmc100TopConstituentSymbol: null,
4151
+ cmc100TopConstituentWeightPct: null,
4152
+ cmc20Value: null,
4153
+ cmc20Change24hPct: null,
4154
+ cmc20TopConstituentSymbol: null,
4155
+ cmc20TopConstituentWeightPct: null,
4156
+ cmc20ToCmc100Ratio: null,
4157
+ cmc20ToCmc100RatioChange24hPct: null,
4158
+ indexRegime: null
4159
+ };
3426
4160
  }
3427
- await ensureStrategyPluginsLoaded(cwd);
3428
- const { state } = getStrategyRegistryState(cwd);
3429
- return state.strategySourcesMap.get(name);
4161
+ return {
4162
+ source: String(cmcIndexes.source ?? ""),
4163
+ available: true,
4164
+ interval: typeof cmcIndexes.interval === "string" ? cmcIndexes.interval : null,
4165
+ asOfTs: toFiniteNumber(cmcIndexes.asOfTs),
4166
+ stale: typeof cmcIndexes.stale === "boolean" ? cmcIndexes.stale : null,
4167
+ cmc100Value: toFiniteNumber(cmcIndexes.cmc100Value),
4168
+ cmc100Change24hPct: toFiniteNumber(cmcIndexes.cmc100Change24hPct),
4169
+ cmc100TopConstituentSymbol: typeof cmcIndexes.cmc100TopConstituentSymbol === "string" ? cmcIndexes.cmc100TopConstituentSymbol : null,
4170
+ cmc100TopConstituentWeightPct: toFiniteNumber(
4171
+ cmcIndexes.cmc100TopConstituentWeightPct
4172
+ ),
4173
+ cmc20Value: toFiniteNumber(cmcIndexes.cmc20Value),
4174
+ cmc20Change24hPct: toFiniteNumber(cmcIndexes.cmc20Change24hPct),
4175
+ cmc20TopConstituentSymbol: typeof cmcIndexes.cmc20TopConstituentSymbol === "string" ? cmcIndexes.cmc20TopConstituentSymbol : null,
4176
+ cmc20TopConstituentWeightPct: toFiniteNumber(
4177
+ cmcIndexes.cmc20TopConstituentWeightPct
4178
+ ),
4179
+ cmc20ToCmc100Ratio: toFiniteNumber(cmcIndexes.cmc20ToCmc100Ratio),
4180
+ cmc20ToCmc100RatioChange24hPct: toFiniteNumber(
4181
+ cmcIndexes.cmc20ToCmc100RatioChange24hPct
4182
+ ),
4183
+ indexRegime: typeof cmcIndexes.indexRegime === "string" ? cmcIndexes.indexRegime : null
4184
+ };
3430
4185
  };
3431
- var getStrategyManifest = (name, cwd = getTradejsProjectCwd()) => {
3432
- if (!name) {
3433
- return void 0;
4186
+ var buildReferenceTradeFlowContextFromSignal = (signal) => {
4187
+ const baseContext = toRecord(signal.additionalIndicators?.baseContext);
4188
+ const relative = toRecord(baseContext?.relative);
4189
+ const refs = toRecord(relative?.referenceTradeFlow);
4190
+ const primaryReferenceSymbol = typeof refs?.primaryReferenceSymbol === "string" ? refs.primaryReferenceSymbol : null;
4191
+ const tradeFlowBySymbol = toRecord(refs?.tradeFlowBySymbol);
4192
+ const primaryTradeFlow = primaryReferenceSymbol != null ? toRecord(tradeFlowBySymbol?.[primaryReferenceSymbol]) : null;
4193
+ if (!refs) {
4194
+ return {
4195
+ source: null,
4196
+ available: false,
4197
+ primaryReferenceSymbol: null,
4198
+ referenceSymbols: [],
4199
+ primaryTradeFlowBuyPressurePct: null,
4200
+ primaryTradeFlowStale: null
4201
+ };
3434
4202
  }
3435
- const { state } = getStrategyRegistryState(cwd);
3436
- return state.strategyManifestsMap.get(name);
4203
+ return {
4204
+ source: String(refs.source ?? ""),
4205
+ available: true,
4206
+ primaryReferenceSymbol,
4207
+ referenceSymbols: Array.isArray(refs.referenceSymbols) ? refs.referenceSymbols.map(String) : [],
4208
+ primaryTradeFlowBuyPressurePct: toFiniteNumber(
4209
+ primaryTradeFlow?.buyPressurePct
4210
+ ),
4211
+ primaryTradeFlowStale: typeof primaryTradeFlow?.stale === "boolean" ? primaryTradeFlow.stale : null
4212
+ };
3437
4213
  };
3438
- var strategies = new Proxy(
3439
- {},
3440
- {
3441
- get: (_target, property) => {
3442
- if (typeof property !== "string") {
3443
- return void 0;
3444
- }
3445
- return getStrategyRegistryState().state.strategyCreators.get(property);
3446
- },
3447
- ownKeys: () => {
3448
- return [...getStrategyRegistryState().state.strategyCreators.keys()];
3449
- },
3450
- getOwnPropertyDescriptor: () => ({
3451
- enumerable: true,
3452
- configurable: true
3453
- })
4214
+ var buildAiMarketContext = (signal) => ({
4215
+ execution: {
4216
+ binanceCoinbaseSpread: buildSpreadContextFromSignal(signal)
4217
+ },
4218
+ participation: {
4219
+ trueDelta: buildTrueDeltaContextFromSignal(signal),
4220
+ tradeFlow: buildTradeFlowContextFromSignal(signal)
4221
+ },
4222
+ relative: {
4223
+ marketBreadth: buildMarketBreadthContextFromSignal(signal),
4224
+ marketBreadths: buildMarketBreadthsContextFromSignal(signal),
4225
+ targetVsBtc: buildTargetVsBtcContextFromSignal(signal),
4226
+ targetVsEth: buildTargetVsEthContextFromSignal(signal),
4227
+ btcAltRegime: buildBtcAltRegimeContextFromSignal(signal),
4228
+ cmcGlobal: buildCmcGlobalContextFromSignal(signal),
4229
+ cmcReferenceAssets: buildCmcReferenceAssetsContextFromSignal(signal),
4230
+ cmcExchangeLiquidity: buildCmcExchangeLiquidityContextFromSignal(signal),
4231
+ cmcFearGreed: buildCmcFearGreedContextFromSignal(signal),
4232
+ cmcIndexes: buildCmcIndexesContextFromSignal(signal),
4233
+ referenceTradeFlow: buildReferenceTradeFlowContextFromSignal(signal)
3454
4234
  }
3455
- );
4235
+ });
3456
4236
 
3457
4237
  // src/strategy/policyProfiles.ts
3458
4238
  var profileMatches = (profile, universe, assetClass) => {
@@ -3554,12 +4334,61 @@ var buildBaseAiPayload = (signal) => {
3554
4334
  };
3555
4335
  };
3556
4336
  var defaultAiAdapter = {};
4337
+ var tickerProfileFeatureIds = new Set(
4338
+ import_aiFeatures.TICKER_PROFILE_AI_FEATURES.map(({ id }) => id)
4339
+ );
3557
4340
  var getStrategyAiAdapter = (strategy, profileId) => getStrategyProfileAiAdapter(getStrategyManifest(strategy), profileId) ?? defaultAiAdapter;
3558
4341
  var getSignalAiAdapter = (signal) => getStrategyAiAdapter(signal.strategy, signal.policyProfileId);
3559
4342
  var buildAiPayloadByStrategy = (signal) => {
3560
4343
  const basePayload = buildBaseAiPayload(signal);
3561
4344
  const adapter = getSignalAiAdapter(signal);
3562
- return adapter.buildPayload?.({ signal, basePayload }) ?? basePayload;
4345
+ const payload = adapter.buildPayload?.({ signal, basePayload }) ?? basePayload;
4346
+ const custom = getStrategyManifest(signal.strategy)?.aiFeatures ?? [];
4347
+ const frozen = signal.aiFeatureReplay?.snapshot;
4348
+ if (frozen) (0, import_aiFeatures.validateAiFeatureSnapshot)(frozen);
4349
+ const tickerProfile = signal.additionalIndicators?.tickerProfile ?? signal.additionalIndicators?.baseContext?.tickerProfile;
4350
+ const hasProfile = Boolean(
4351
+ tickerProfile || frozen?.definitions.some(({ id }) => tickerProfileFeatureIds.has(id))
4352
+ );
4353
+ const indicatorFeatures = (0, import_indicators2.getRegisteredIndicatorEntries)(
4354
+ getTradejsProjectCwd()
4355
+ ).flatMap((entry) => entry.indicator.enabled ? entry.aiFeatures ?? [] : []);
4356
+ const hasPortfolio = Boolean(
4357
+ signal.additionalIndicators?.portfolioContext || frozen?.definitions.some(
4358
+ ({ id }) => import_aiFeatures.PORTFOLIO_AI_FEATURES.some((d) => d.id === id)
4359
+ )
4360
+ );
4361
+ const definitions = [
4362
+ ...hasPortfolio ? import_aiFeatures.PORTFOLIO_AI_FEATURES : [],
4363
+ ...hasProfile ? import_aiFeatures.TICKER_PROFILE_AI_FEATURES : [],
4364
+ ...indicatorFeatures,
4365
+ ...custom
4366
+ ];
4367
+ if (!definitions.length && !signal.aiFeatureReplay?.snapshot) return payload;
4368
+ (0, import_aiFeatures.validateAiFeatureDefinitions)(definitions);
4369
+ if (signal.aiFeatureReplay && !frozen) {
4370
+ throw new Error(
4371
+ "Dataset has no frozen AI features; regenerate the decision-time export"
4372
+ );
4373
+ }
4374
+ if (frozen && JSON.stringify(frozen.definitions) !== JSON.stringify(definitions)) {
4375
+ throw new Error(
4376
+ "AI feature definition changed; regenerate the decision-time export"
4377
+ );
4378
+ }
4379
+ return {
4380
+ ...payload,
4381
+ featureSnapshot: frozen ?? (0, import_aiFeatures.projectAiFeatures)(definitions, {
4382
+ signal: tickerProfile ? {
4383
+ ...signal,
4384
+ additionalIndicators: {
4385
+ ...signal.additionalIndicators,
4386
+ tickerProfile
4387
+ }
4388
+ } : signal,
4389
+ payload
4390
+ })
4391
+ };
3563
4392
  };
3564
4393
  var buildAiSystemPromptAddonByStrategy = (signal) => getSignalAiAdapter(signal).buildSystemPromptAddon?.({ signal }) ?? "";
3565
4394
  var buildAiHumanPromptAddonByStrategy = (signal, payload) => getSignalAiAdapter(signal).buildHumanPromptAddon?.({
@@ -3945,380 +4774,132 @@ var buildAiPayload = (signal) => buildAiPayloadByStrategy(signal);
3945
4774
  var getDeterministicAiGateContext = (payload) => {
3946
4775
  const additionalIndicators = asRecord(payload.additionalIndicators);
3947
4776
  const candidates = [
3948
- additionalIndicators,
3949
- ...Object.values(additionalIndicators ?? {}).map(asRecord)
3950
- ].filter((value) => Boolean(value));
3951
- return candidates.find(
3952
- (candidate) => Array.isArray(candidate.approvalBlockReasons) || Array.isArray(candidate.riskAnnotations) || Array.isArray(candidate.structuralHardBlockReasons) || typeof candidate.approvalAllowedNow === "boolean"
3953
- ) ?? null;
3954
- };
3955
- var buildAiHumanPrompt = (signal, payload = buildAiPayload(signal)) => `
3956
- Analyze the already computed internal signal for ${signal.symbol}. The original signal direction is ${signal.direction}.
3957
- This is a structure-classification and audit task, not execution advice. Determine whether the current structure confirms the existing signal, how structurally coherent it is right now, whether an extra confirmation level is needed, and whether the already supplied levels in \`payload.signal.prices\` still look internally coherent. Do not replace the original thesis with a new one and do not invent new levels; return only the requested JSON.
3958
-
3959
- Trade payload:
3960
- ${JSON.stringify(payload)}
3961
- ${buildAiHumanPromptAddonByStrategy(signal, payload)}
3962
- `;
3963
- var runAiPrompt = async ({ systemPrompt, humanPrompt }, options = {}) => {
3964
- const settings = await getAiUserSettings(options.userName);
3965
- const responseLanguage = (0, import_aiLanguages2.getAiResponseLanguagePromptName)(
3966
- settings.AI_RESPONSE_LANGUAGE || import_aiLanguages2.DEFAULT_AI_RESPONSE_LANGUAGE
3967
- );
3968
- const response = await invokeAiPromptChat({
3969
- userName: options.userName,
3970
- model: options.model,
3971
- messages: [
3972
- { role: "system", content: systemPrompt },
3973
- {
3974
- role: "system",
3975
- content: `Write all user-visible text fields in ${responseLanguage}. Keep field names and JSON syntax unchanged.`
3976
- },
3977
- { role: "user", content: humanPrompt }
3978
- ]
3979
- });
3980
- const parsed = parseAIResponse(response.content);
3981
- const normalized = normalizeAnalysis(parsed);
3982
- if (!options.signal) {
3983
- return normalized;
3984
- }
3985
- return postProcessAiAnalysisByStrategy(
3986
- options.signal,
3987
- normalized,
3988
- options.payload
3989
- );
3990
- };
3991
- var runAiPromptLocal = async (signal, options = {}) => {
3992
- const payload = options.payload ?? buildAiPayload(signal);
3993
- const gateContext = getDeterministicAiGateContext(payload);
3994
- const signalDirection = getSignalDirection(signal);
3995
- const deterministicQuality = getDeterministicQuality(gateContext);
3996
- const approvalAllowedNow = typeof gateContext?.approvalAllowedNow === "boolean" ? gateContext.approvalAllowedNow : deterministicQuality >= 4;
3997
- return postProcessLocalAiAnalysisByStrategy(
3998
- signal,
3999
- {
4000
- direction: approvalAllowedNow ? signalDirection : null,
4001
- quality: deterministicQuality,
4002
- needRetest: !approvalAllowedNow,
4003
- retestPrice: null,
4004
- takeProfitPrice: approvalAllowedNow ? signal.prices?.takeProfitPrice ?? null : null,
4005
- stopLossPrice: approvalAllowedNow ? signal.prices?.stopLossPrice ?? null : null
4006
- },
4007
- payload
4008
- );
4009
- };
4010
- var askAI = async (signal, options = {}) => {
4011
- const { symbol } = signal;
4012
- const payload = buildAiPayload(signal);
4013
- const content = await runAiPrompt(
4014
- {
4015
- systemPrompt: buildAiSystemPrompt(signal),
4016
- humanPrompt: buildAiHumanPrompt(signal, payload)
4017
- },
4018
- {
4019
- ...options,
4020
- signal,
4021
- payload
4022
- }
4023
- );
4024
- await (0, import_redis2.setData)(import_redis2.redisKeys.analysis(symbol, signal.signalId), content);
4025
- return content;
4026
- };
4027
-
4028
- // src/strategyAdapters/ml.ts
4029
- var defaultMlAdapter = {
4030
- normalizeStrategyConfig: (strategyConfig) => strategyConfig
4031
- };
4032
- var getStrategyMlAdapter = (strategy, profileId) => {
4033
- const strategyAdapter = getStrategyProfileMlAdapter(
4034
- getStrategyManifest(strategy),
4035
- profileId
4036
- );
4037
- if (!strategyAdapter) return defaultMlAdapter;
4038
- return {
4039
- ...defaultMlAdapter,
4040
- ...strategyAdapter
4041
- };
4042
- };
4043
-
4044
- // src/mlPayload.ts
4045
- var normalizeStrategyConfig = (strategyConfig, strategyName, profileId) => {
4046
- return getStrategyMlAdapter(
4047
- strategyName,
4048
- profileId
4049
- ).normalizeStrategyConfig?.(strategyConfig);
4050
- };
4051
- var buildMlPayload = (payload) => {
4052
- const strategyName = payload.signal?.strategy ?? payload.context?.strategyName;
4053
- const profileId = payload.signal?.policyProfileId;
4054
- const mlAdapter = getStrategyMlAdapter(strategyName, profileId);
4055
- const normalizedSignal = mlAdapter.normalizeSignal?.(payload.signal) ?? payload.signal;
4056
- const nextSignal = {
4057
- ...normalizedSignal,
4058
- indicators: {
4059
- ...normalizedSignal?.indicators ?? {}
4060
- }
4061
- };
4062
- const nextContext = payload.context ? {
4063
- ...payload.context,
4064
- strategyConfig: normalizeStrategyConfig(
4065
- payload.context.strategyConfig,
4066
- strategyName,
4067
- profileId
4068
- )
4069
- } : void 0;
4070
- return {
4071
- signal: nextSignal,
4072
- context: nextContext
4073
- };
4074
- };
4075
-
4076
- // src/runtimeJournal.ts
4077
- var import_node_crypto = require("crypto");
4078
- var import_constants3 = require("@tradejs/core/constants");
4079
- var import_time3 = require("@tradejs/core/time");
4080
- var import_trade = require("@tradejs/core/trade");
4081
- var import_logger6 = require("@tradejs/infra/logger");
4082
- var import_redis3 = require("@tradejs/infra/redis");
4083
- var now = () => Date.now();
4084
- var toRandomOrderSuffix = () => (0, import_node_crypto.randomUUID)().replace(/-/g, "").slice(0, 12).toLowerCase();
4085
- var calculateClosedPnl = ({
4086
- direction,
4087
- entryPrice,
4088
- exitPrice,
4089
- qty
4090
- }) => {
4091
- const pnl = direction === "LONG" ? (exitPrice - entryPrice) * qty : (entryPrice - exitPrice) * qty;
4092
- return Number.isFinite(pnl) ? pnl : null;
4093
- };
4094
- var createRuntimeOrderId = (strategy) => {
4095
- const prefix = (0, import_trade.createRuntimeOrderLinkPrefix)(strategy);
4096
- if (prefix === "tjs-") {
4097
- return `tjs-${(0, import_node_crypto.randomUUID)().replace(/-/g, "").slice(0, 24).toLowerCase()}`;
4098
- }
4099
- return `${prefix}${toRandomOrderSuffix()}`;
4100
- };
4101
- var recordRuntimeTradeOpen = async (params) => {
4102
- const { userName } = params;
4103
- if (!userName) {
4104
- return null;
4105
- }
4106
- const record = {
4107
- ...params,
4108
- entryCount: 1,
4109
- lastEntryPrice: params.entryPrice,
4110
- lastEntryQty: params.qty,
4111
- lastEntryTimestamp: params.entryTimestamp,
4112
- status: "active",
4113
- currentPrice: params.entryPrice,
4114
- currentPnl: 0,
4115
- closedPnl: null,
4116
- exitPrice: null,
4117
- exitTimestamp: null,
4118
- lastSyncedAt: now()
4119
- };
4120
- const dayKey = (0, import_time3.getRuntimeStorageDayKey)(record.entryTimestamp);
4121
- const runtimeScopeId = record.deploymentId ?? record.accountId;
4122
- try {
4123
- await Promise.all([
4124
- (0, import_redis3.setData)(import_redis3.redisKeys.runtimeTrade(userName, record.orderId), record, {
4125
- expire: 0
4126
- }),
4127
- (0, import_redis3.setHashJsonField)(
4128
- import_redis3.redisKeys.runtimeTradeBucket(userName, dayKey),
4129
- record.orderId,
4130
- record,
4131
- { expire: 0 }
4132
- ),
4133
- (0, import_redis3.setData)(
4134
- import_redis3.redisKeys.runtimeActiveTrade(userName, record.symbol, runtimeScopeId),
4135
- { orderId: record.orderId },
4136
- { expire: 0 }
4137
- )
4138
- ]);
4139
- } catch (error) {
4140
- import_logger6.logger.error(
4141
- "runtime trade open journal failed: %s %s",
4142
- record.symbol,
4143
- error?.message || String(error)
4144
- );
4145
- }
4146
- return record;
4147
- };
4148
- var recordRuntimeTradeIncrease = async (params) => {
4149
- const {
4150
- userName,
4151
- strategy,
4152
- symbol,
4153
- direction,
4154
- resultingQty,
4155
- resultingEntryPrice,
4156
- addedQty,
4157
- addedEntryPrice,
4158
- entryTimestamp,
4159
- fee,
4160
- accountId,
4161
- deploymentId
4162
- } = params;
4163
- if (!userName) {
4164
- return null;
4165
- }
4166
- const existing = await getActiveRuntimeTrade({
4167
- userName,
4168
- symbol,
4169
- accountId,
4170
- deploymentId
4171
- });
4172
- if (!existing || existing.strategy !== strategy || existing.direction !== direction) {
4173
- return null;
4174
- }
4175
- const addedFee = typeof fee === "number" && Number.isFinite(fee) ? fee : 0;
4176
- const openFee = (existing.openFee ?? existing.fee ?? 0) + addedFee;
4177
- const totalFee = (existing.totalFee ?? existing.fee ?? 0) + addedFee;
4178
- const next = {
4179
- ...existing,
4180
- ...params.orderLeverage ? { orderLeverage: params.orderLeverage } : {},
4181
- qty: resultingQty,
4182
- entryPrice: resultingEntryPrice,
4183
- entryCount: Math.max(1, existing.entryCount ?? 1) + 1,
4184
- lastEntryPrice: addedEntryPrice,
4185
- lastEntryQty: addedQty,
4186
- lastEntryTimestamp: entryTimestamp,
4187
- currentPrice: resultingEntryPrice,
4188
- currentPnl: 0,
4189
- fee: openFee,
4190
- openFee,
4191
- totalFee,
4192
- lastSyncedAt: now()
4193
- };
4194
- const dayKey = (0, import_time3.getRuntimeStorageDayKey)(existing.entryTimestamp);
4195
- try {
4196
- await Promise.all([
4197
- (0, import_redis3.setData)(import_redis3.redisKeys.runtimeTrade(userName, existing.orderId), next, {
4198
- expire: 0
4199
- }),
4200
- (0, import_redis3.setHashJsonField)(
4201
- import_redis3.redisKeys.runtimeTradeBucket(userName, dayKey),
4202
- existing.orderId,
4203
- next,
4204
- { expire: 0 }
4205
- )
4206
- ]);
4207
- } catch (error) {
4208
- import_logger6.logger.error(
4209
- "runtime trade increase journal failed: %s %s",
4210
- symbol,
4211
- error?.message || String(error)
4212
- );
4213
- }
4214
- return next;
4777
+ additionalIndicators,
4778
+ ...Object.values(additionalIndicators ?? {}).map(asRecord)
4779
+ ].filter((value) => Boolean(value));
4780
+ return candidates.find(
4781
+ (candidate) => Array.isArray(candidate.approvalBlockReasons) || Array.isArray(candidate.riskAnnotations) || Array.isArray(candidate.structuralHardBlockReasons) || typeof candidate.approvalAllowedNow === "boolean"
4782
+ ) ?? null;
4215
4783
  };
4216
- var getActiveRuntimeTrade = async (params) => {
4217
- const { userName, symbol, accountId, deploymentId } = params;
4218
- if (!userName) {
4219
- return null;
4220
- }
4221
- const activeRef = await (0, import_redis3.getData)(
4222
- import_redis3.redisKeys.runtimeActiveTrade(userName, symbol, deploymentId ?? accountId),
4223
- null
4784
+ var buildAiHumanPrompt = (signal, payload = buildAiPayload(signal)) => `
4785
+ Analyze the already computed internal signal for ${signal.symbol}. The original signal direction is ${signal.direction}.
4786
+ This is a structure-classification and audit task, not execution advice. Determine whether the current structure confirms the existing signal, how structurally coherent it is right now, whether an extra confirmation level is needed, and whether the already supplied levels in \`payload.signal.prices\` still look internally coherent. Do not replace the original thesis with a new one and do not invent new levels; return only the requested JSON.
4787
+
4788
+ Trade payload:
4789
+ ${JSON.stringify(payload)}
4790
+ ${buildAiHumanPromptAddonByStrategy(signal, payload)}
4791
+ `;
4792
+ var runAiPrompt = async ({ systemPrompt, humanPrompt }, options = {}) => {
4793
+ const settings = await getAiUserSettings(options.userName);
4794
+ const responseLanguage = (0, import_aiLanguages2.getAiResponseLanguagePromptName)(
4795
+ settings.AI_RESPONSE_LANGUAGE || import_aiLanguages2.DEFAULT_AI_RESPONSE_LANGUAGE
4224
4796
  );
4225
- const orderId = String(activeRef?.orderId || "").trim();
4226
- if (!orderId) {
4227
- return null;
4797
+ const response = await invokeAiPromptChat({
4798
+ userName: options.userName,
4799
+ model: options.model,
4800
+ messages: [
4801
+ { role: "system", content: systemPrompt },
4802
+ {
4803
+ role: "system",
4804
+ content: `Write all user-visible text fields in ${responseLanguage}. Keep field names and JSON syntax unchanged.`
4805
+ },
4806
+ { role: "user", content: humanPrompt }
4807
+ ]
4808
+ });
4809
+ const parsed = parseAIResponse(response.content);
4810
+ const normalized = normalizeAnalysis(parsed);
4811
+ if (!options.signal) {
4812
+ return normalized;
4228
4813
  }
4229
- const existing = await (0, import_redis3.getData)(
4230
- import_redis3.redisKeys.runtimeTrade(userName, orderId),
4231
- null
4814
+ return postProcessAiAnalysisByStrategy(
4815
+ options.signal,
4816
+ normalized,
4817
+ options.payload
4232
4818
  );
4233
- if (!existing || existing.status !== "active") {
4234
- await (0, import_redis3.delKey)(
4235
- import_redis3.redisKeys.runtimeActiveTrade(userName, symbol, deploymentId ?? accountId)
4236
- );
4237
- return null;
4238
- }
4239
- return existing;
4240
4819
  };
4241
- var markRuntimeTradeClosed = async (params) => {
4242
- const {
4243
- userName,
4244
- symbol,
4245
- strategy,
4246
- exitPrice,
4247
- exitTimestamp,
4248
- closedPnl,
4249
- exitType,
4250
- accountId,
4251
- deploymentId
4252
- } = params;
4253
- if (!userName) {
4254
- return null;
4255
- }
4256
- const existing = await getActiveRuntimeTrade({
4257
- userName,
4258
- symbol,
4259
- accountId,
4260
- deploymentId
4261
- });
4262
- if (!existing) {
4263
- return null;
4264
- }
4265
- const orderId = existing.orderId;
4266
- if (strategy && existing.strategy !== strategy) {
4267
- return null;
4268
- }
4269
- const resolvedExitPrice = typeof exitPrice === "number" && Number.isFinite(exitPrice) ? exitPrice : existing.currentPrice ?? existing.entryPrice;
4270
- const resolvedClosedPnl = typeof closedPnl === "number" && Number.isFinite(closedPnl) ? closedPnl : typeof resolvedExitPrice === "number" && Number.isFinite(resolvedExitPrice) ? calculateClosedPnl({
4271
- direction: existing.direction,
4272
- entryPrice: existing.entryPrice,
4273
- exitPrice: resolvedExitPrice,
4274
- qty: existing.qty
4275
- }) : existing.closedPnl ?? existing.currentPnl ?? null;
4276
- const next = {
4277
- ...existing,
4278
- status: "closed",
4279
- currentPrice: resolvedExitPrice,
4280
- currentPnl: resolvedClosedPnl,
4281
- closedPnl: resolvedClosedPnl,
4282
- exitPrice: resolvedExitPrice,
4283
- exitTimestamp: typeof exitTimestamp === "number" && Number.isFinite(exitTimestamp) ? exitTimestamp : now(),
4284
- exitType: exitType ?? existing.exitType ?? null,
4285
- lastSyncedAt: now()
4820
+ var runAiPromptLocal = async (signal, options = {}) => {
4821
+ const payload = options.payload ?? buildAiPayload(signal);
4822
+ const gateContext = getDeterministicAiGateContext(payload);
4823
+ const signalDirection = getSignalDirection(signal);
4824
+ const deterministicQuality = getDeterministicQuality(gateContext);
4825
+ const approvalAllowedNow = typeof gateContext?.approvalAllowedNow === "boolean" ? gateContext.approvalAllowedNow : deterministicQuality >= 4;
4826
+ return postProcessLocalAiAnalysisByStrategy(
4827
+ signal,
4828
+ {
4829
+ direction: approvalAllowedNow ? signalDirection : null,
4830
+ quality: deterministicQuality,
4831
+ needRetest: !approvalAllowedNow,
4832
+ retestPrice: null,
4833
+ takeProfitPrice: approvalAllowedNow ? signal.prices?.takeProfitPrice ?? null : null,
4834
+ stopLossPrice: approvalAllowedNow ? signal.prices?.stopLossPrice ?? null : null
4835
+ },
4836
+ payload
4837
+ );
4838
+ };
4839
+ var askAI = async (signal, options = {}) => {
4840
+ const { symbol } = signal;
4841
+ const payload = buildAiPayload(signal);
4842
+ const content = await runAiPrompt(
4843
+ {
4844
+ systemPrompt: buildAiSystemPrompt(signal),
4845
+ humanPrompt: buildAiHumanPrompt(signal, payload)
4846
+ },
4847
+ {
4848
+ ...options,
4849
+ signal,
4850
+ payload
4851
+ }
4852
+ );
4853
+ await (0, import_redis4.setData)(import_redis4.redisKeys.analysis(symbol, signal.signalId), content);
4854
+ return content;
4855
+ };
4856
+
4857
+ // src/strategyAdapters/ml.ts
4858
+ var defaultMlAdapter = {
4859
+ normalizeStrategyConfig: (strategyConfig) => strategyConfig
4860
+ };
4861
+ var getStrategyMlAdapter = (strategy, profileId) => {
4862
+ const strategyAdapter = getStrategyProfileMlAdapter(
4863
+ getStrategyManifest(strategy),
4864
+ profileId
4865
+ );
4866
+ if (!strategyAdapter) return defaultMlAdapter;
4867
+ return {
4868
+ ...defaultMlAdapter,
4869
+ ...strategyAdapter
4870
+ };
4871
+ };
4872
+
4873
+ // src/mlPayload.ts
4874
+ var normalizeStrategyConfig = (strategyConfig, strategyName, profileId) => {
4875
+ return getStrategyMlAdapter(
4876
+ strategyName,
4877
+ profileId
4878
+ ).normalizeStrategyConfig?.(strategyConfig);
4879
+ };
4880
+ var buildMlPayload = (payload) => {
4881
+ const strategyName = payload.signal?.strategy ?? payload.context?.strategyName;
4882
+ const profileId = payload.signal?.policyProfileId;
4883
+ const mlAdapter = getStrategyMlAdapter(strategyName, profileId);
4884
+ const normalizedSignal = mlAdapter.normalizeSignal?.(payload.signal) ?? payload.signal;
4885
+ const nextSignal = {
4886
+ ...normalizedSignal,
4887
+ indicators: {
4888
+ ...normalizedSignal?.indicators ?? {}
4889
+ }
4890
+ };
4891
+ const nextContext = payload.context ? {
4892
+ ...payload.context,
4893
+ strategyConfig: normalizeStrategyConfig(
4894
+ payload.context.strategyConfig,
4895
+ strategyName,
4896
+ profileId
4897
+ )
4898
+ } : void 0;
4899
+ return {
4900
+ signal: nextSignal,
4901
+ context: nextContext
4286
4902
  };
4287
- const dayKey = (0, import_time3.getRuntimeStorageDayKey)(existing.entryTimestamp);
4288
- const closeDayKey = (0, import_time3.getRuntimeStorageDayKey)(next.exitTimestamp);
4289
- try {
4290
- await Promise.all([
4291
- (0, import_redis3.setData)(import_redis3.redisKeys.runtimeTrade(userName, orderId), next, {
4292
- expire: import_constants3.TTL_1M
4293
- }),
4294
- (0, import_redis3.setHashJsonField)(
4295
- import_redis3.redisKeys.runtimeTradeBucket(userName, dayKey),
4296
- orderId,
4297
- next,
4298
- { expire: import_constants3.TTL_1M }
4299
- ),
4300
- (0, import_redis3.setHashJsonField)(
4301
- import_redis3.redisKeys.runtimeClosedTradeBucket(userName, closeDayKey),
4302
- orderId,
4303
- next,
4304
- { expire: import_constants3.TTL_1M }
4305
- ),
4306
- (0, import_redis3.delKey)(
4307
- import_redis3.redisKeys.runtimeActiveTrade(
4308
- userName,
4309
- symbol,
4310
- deploymentId ?? accountId
4311
- )
4312
- )
4313
- ]);
4314
- } catch (error) {
4315
- import_logger6.logger.error(
4316
- "runtime trade close journal failed: %s %s",
4317
- symbol,
4318
- error?.message || String(error)
4319
- );
4320
- }
4321
- return next;
4322
4903
  };
4323
4904
 
4324
4905
  // src/strategyHelpers/marketContextStages.ts
@@ -4330,7 +4911,7 @@ var import_logger7 = require("@tradejs/infra/logger");
4330
4911
  var import_strategies = require("@tradejs/core/strategies");
4331
4912
 
4332
4913
  // src/binanceBreadthUniverses.ts
4333
- var import_node_crypto2 = require("crypto");
4914
+ var import_node_crypto3 = require("crypto");
4334
4915
 
4335
4916
  // src/config/binanceBreadthUniverses.json
4336
4917
  var binanceBreadthUniverses_default = {
@@ -4569,7 +5150,7 @@ var BINANCE_BREADTH_UNIVERSE_KEYS = [
4569
5150
  "top50",
4570
5151
  "top100"
4571
5152
  ];
4572
- var fingerprint = (value, length) => (0, import_node_crypto2.createHash)("sha256").update(JSON.stringify(value)).digest("hex").slice(0, length);
5153
+ var fingerprint = (value, length) => (0, import_node_crypto3.createHash)("sha256").update(JSON.stringify(value)).digest("hex").slice(0, length);
4573
5154
  var normalizeSymbols = (symbols) => symbols.map((symbol) => symbol.trim().toUpperCase()).filter(Boolean);
4574
5155
  var buildBinanceBreadthUniverseSnapshot = ({
4575
5156
  rankedSymbols,
@@ -5593,7 +6174,7 @@ var enrichSignalWithCoinMarketCapContext = async (params) => {
5593
6174
  };
5594
6175
 
5595
6176
  // src/strategyHelpers/derivativesContext.ts
5596
- var import_indicators2 = require("@tradejs/core/indicators");
6177
+ var import_indicators3 = require("@tradejs/core/indicators");
5597
6178
  var import_data3 = require("@tradejs/core/data");
5598
6179
  var import_strategies3 = require("@tradejs/core/strategies");
5599
6180
  var import_constants4 = require("@tradejs/core/constants");
@@ -5627,7 +6208,7 @@ var parseLookbackMs = () => {
5627
6208
  };
5628
6209
  var withHourlyFallbackRows = (rowsByInterval) => ({
5629
6210
  "15m": rowsByInterval["15m"] ?? [],
5630
- "1h": (0, import_indicators2.buildCoinalyzeHourlyRowsWithFallback)({
6211
+ "1h": (0, import_indicators3.buildCoinalyzeHourlyRowsWithFallback)({
5631
6212
  rows15m: rowsByInterval["15m"],
5632
6213
  fallbackRows1h: rowsByInterval["1h"]
5633
6214
  })
@@ -5755,7 +6336,7 @@ var enrichSignalWithDerivativesContext = async (params) => {
5755
6336
  const targetSymbol = normalizeSymbol(signal.symbol);
5756
6337
  const lookbackMs = parseLookbackMs();
5757
6338
  const decisionTimeMs = signal.timestamp + (0, import_data3.intervalToMs)(signal.interval);
5758
- const derivativesEndMs = (0, import_indicators2.getLastClosedDerivativesBarStartMs)(
6339
+ const derivativesEndMs = (0, import_indicators3.getLastClosedDerivativesBarStartMs)(
5759
6340
  decisionTimeMs,
5760
6341
  "15m"
5761
6342
  );
@@ -5770,7 +6351,7 @@ var enrichSignalWithDerivativesContext = async (params) => {
5770
6351
  });
5771
6352
  return [
5772
6353
  symbol,
5773
- (0, import_indicators2.buildDerivativesContext)({
6354
+ (0, import_indicators3.buildDerivativesContext)({
5774
6355
  symbol,
5775
6356
  direction: signal.direction,
5776
6357
  timestamp: derivativesEndMs,
@@ -5797,7 +6378,7 @@ var enrichSignalWithDerivativesContext = async (params) => {
5797
6378
  lookbackMs,
5798
6379
  ...params.abortSignal ? { signal: params.abortSignal } : {}
5799
6380
  });
5800
- const context = (0, import_indicators2.buildDerivativesContext)({
6381
+ const context = (0, import_indicators3.buildDerivativesContext)({
5801
6382
  symbol: targetSymbol,
5802
6383
  direction: signal.direction,
5803
6384
  timestamp: derivativesEndMs,
@@ -5845,7 +6426,7 @@ var import_hyperliquidWhales2 = require("@tradejs/infra/timescale/hyperliquidWha
5845
6426
  var import_logger10 = require("@tradejs/infra/logger");
5846
6427
 
5847
6428
  // src/hyperliquidWhaleUniverse.ts
5848
- var import_node_crypto3 = require("crypto");
6429
+ var import_node_crypto4 = require("crypto");
5849
6430
 
5850
6431
  // src/config/hyperliquidPerpUniverse.json
5851
6432
  var hyperliquidPerpUniverse_default = {
@@ -6017,7 +6598,7 @@ var hyperliquidWhales_default = {
6017
6598
  };
6018
6599
 
6019
6600
  // src/hyperliquidWhaleUniverse.ts
6020
- var fingerprint2 = (value) => (0, import_node_crypto3.createHash)("sha256").update(JSON.stringify(value)).digest("hex").slice(0, 16);
6601
+ var fingerprint2 = (value) => (0, import_node_crypto4.createHash)("sha256").update(JSON.stringify(value)).digest("hex").slice(0, 16);
6021
6602
  var validateUnique = (values, label) => {
6022
6603
  if (new Set(values).size !== values.length) {
6023
6604
  throw new Error(`Hyperliquid ${label} snapshot contains duplicates`);
@@ -6504,6 +7085,7 @@ var enrichSignalWithMarketContextStages = async ({
6504
7085
  })
6505
7086
  });
6506
7087
  }
7088
+ await enrichSignalWithTickerProfile(signal);
6507
7089
  const results = [];
6508
7090
  for (const stage of stages) {
6509
7091
  if (abortSignal?.aborted) break;
@@ -6551,12 +7133,12 @@ var findReplayAiAnalysis = ({
6551
7133
  direction,
6552
7134
  ai
6553
7135
  }) => {
6554
- const snapshots = ai?.replayAnalyses;
6555
- if (!Array.isArray(snapshots) || !snapshots.length) {
7136
+ const snapshots2 = ai?.replayAnalyses;
7137
+ if (!Array.isArray(snapshots2) || !snapshots2.length) {
6556
7138
  return void 0;
6557
7139
  }
6558
7140
  let best = null;
6559
- for (const snapshot2 of snapshots) {
7141
+ for (const snapshot2 of snapshots2) {
6560
7142
  if (snapshot2.symbol !== signal.symbol || snapshot2.direction !== direction || snapshot2.strategy && snapshot2.strategy !== signal.strategy) {
6561
7143
  continue;
6562
7144
  }
@@ -6716,11 +7298,13 @@ var executeEntryOrder = async ({
6716
7298
  beforePlaceOrder,
6717
7299
  recordRuntimeTrade = true,
6718
7300
  leverage,
6719
- executionLimits
7301
+ executionLimits,
7302
+ markPortfolioSubmitted,
7303
+ rejectPortfolioSubmission
6720
7304
  }) => {
6721
7305
  await beforePlaceOrder?.();
6722
7306
  const previousPosition = positionIntent === "increase" ? await connector.getPosition(symbol) : null;
6723
- const orderId = signal.orderId || createRuntimeOrderId(signal.strategy);
7307
+ let orderId = signal.orderId || createRuntimeOrderId(signal.strategy);
6724
7308
  const signalTimestamp = signal.timestamp;
6725
7309
  const signalClosePrice = currentPrice;
6726
7310
  signal.orderId = orderId;
@@ -6789,9 +7373,16 @@ var executeEntryOrder = async ({
6789
7373
  ...typeof leverage === "number" && Number.isFinite(leverage) ? { leverage } : {},
6790
7374
  orderId,
6791
7375
  signal,
6792
- ...beforeSubmit ? { beforeSubmit } : {}
7376
+ ...rejectPortfolioSubmission ? { onRejected: rejectPortfolioSubmission } : {},
7377
+ ...beforeSubmit || markPortfolioSubmitted ? {
7378
+ beforeSubmit: async (context) => {
7379
+ await beforeSubmit?.(context);
7380
+ return markPortfolioSubmitted?.(context.qty);
7381
+ }
7382
+ } : {}
6793
7383
  });
6794
7384
  const orderAckTime = Date.now();
7385
+ if (markPortfolioSubmitted && signal.orderId) orderId = signal.orderId;
6795
7386
  const placedQty = typeof signal.orderQty === "number" && Number.isFinite(signal.orderQty) && signal.orderQty > 0 ? signal.orderQty : qty;
6796
7387
  const currentPosition = await connector.getPosition(symbol);
6797
7388
  const fillTime = Date.now();
@@ -7777,7 +8368,7 @@ var handleProtectDecision = async ({
7777
8368
  }
7778
8369
  return decision.code;
7779
8370
  };
7780
- var executeEntryDecision = async ({
8371
+ var executeAdmittedEntryDecision = async ({
7781
8372
  connector,
7782
8373
  symbol,
7783
8374
  decision,
@@ -7791,7 +8382,9 @@ var executeEntryDecision = async ({
7791
8382
  ai,
7792
8383
  recordRuntimeJournal,
7793
8384
  invokeStageHooks,
7794
- notifyRuntimeError
8385
+ notifyRuntimeError,
8386
+ markPortfolioSubmitted,
8387
+ rejectPortfolioSubmission
7795
8388
  }) => {
7796
8389
  const signal = decision.signal;
7797
8390
  const beforePlaceOrder = async () => {
@@ -7838,6 +8431,8 @@ var executeEntryDecision = async ({
7838
8431
  ...Number.isFinite(Number(hookCtx.strategyConfig.LEVERAGE)) ? { leverage: Number(hookCtx.strategyConfig.LEVERAGE) } : {},
7839
8432
  signal,
7840
8433
  beforePlaceOrder,
8434
+ markPortfolioSubmitted,
8435
+ rejectPortfolioSubmission,
7841
8436
  recordRuntimeTrade: recordRuntimeJournal,
7842
8437
  executionLimits: resolveEntryExecutionLimits(
7843
8438
  hookCtx.strategyConfig,
@@ -7978,6 +8573,37 @@ var executeEntryDecision = async ({
7978
8573
  }
7979
8574
  return signal ?? decision.code;
7980
8575
  };
8576
+ async function executeEntryDecision(params) {
8577
+ if (!params.portfolio) return executeAdmittedEntryDecision(params);
8578
+ const { decision, portfolio } = params;
8579
+ const signal = decision.signal;
8580
+ if (!signal || decision.orderPlan.positionIntent === "increase")
8581
+ return "PORTFOLIO_REQUIRES_NEW_SIGNAL_POSITION";
8582
+ signal.orderId ??= createRuntimeOrderId(signal.strategy);
8583
+ const result = await portfolio.submit(
8584
+ params.portfolioTime ?? decision.entryContext.timestamp,
8585
+ {
8586
+ id: signal.orderId,
8587
+ signalId: signal.signalId,
8588
+ strategy: signal.strategy,
8589
+ symbol: params.symbol,
8590
+ direction: decision.entryContext.direction,
8591
+ qty: decision.orderPlan.qty,
8592
+ timestamp: decision.entryContext.timestamp
8593
+ },
8594
+ (markPortfolioSubmitted, rejectPortfolioSubmission) => executeAdmittedEntryDecision({
8595
+ ...params,
8596
+ markPortfolioSubmitted,
8597
+ rejectPortfolioSubmission
8598
+ })
8599
+ );
8600
+ if (result.reason) {
8601
+ signal.orderStatus = "failed";
8602
+ signal.orderFailureReason = result.reason;
8603
+ return signal;
8604
+ }
8605
+ return result.value;
8606
+ }
7981
8607
 
7982
8608
  // src/strategyRuntime.ts
7983
8609
  var cloneWithPropertyDescriptors = (value) => Object.create(
@@ -8005,6 +8631,7 @@ var createStrategyRuntime = ({
8005
8631
  };
8006
8632
  const creator = async ({
8007
8633
  userName,
8634
+ connectorName,
8008
8635
  config: baseConfig,
8009
8636
  symbol,
8010
8637
  universe: requestedUniverse,
@@ -8043,6 +8670,37 @@ var createStrategyRuntime = ({
8043
8670
  const projectConfig = await loadTradejsConfig(projectRoot);
8044
8671
  const projectHooks = projectConfig.hooks;
8045
8672
  const env = String(config.ENV ?? "BACKTEST");
8673
+ let portfolioMarket;
8674
+ const portfolio = getPortfolioRuntime({
8675
+ symbol,
8676
+ canClose: async (position, intent, asOf) => {
8677
+ if (!portfolioMarket || portfolioMarket.candle.timestamp > asOf || !isRuntimeOrderExecutionEnabled({ config, env, connector }))
8678
+ return false;
8679
+ const gate = await authorizeClose(
8680
+ {
8681
+ kind: "exit",
8682
+ code: intent.reason,
8683
+ closePlan: {
8684
+ direction: position.direction,
8685
+ price: position.markPrice,
8686
+ timestamp: asOf
8687
+ }
8688
+ },
8689
+ portfolioMarket,
8690
+ position.strategy
8691
+ );
8692
+ return gate?.allow !== false;
8693
+ },
8694
+ connectorName,
8695
+ value: baseConfig?.PORTFOLIO_POLICY ?? config.PORTFOLIO_POLICY,
8696
+ strategy: strategyName,
8697
+ connector,
8698
+ env,
8699
+ userName,
8700
+ accountId,
8701
+ deploymentId
8702
+ });
8703
+ const portfolioContext = portfolio ? (now2) => isRuntimeOrderExecutionEnabled({ config, env, connector }) ? portfolio.tick(now2) : portfolio.context(now2) : void 0;
8046
8704
  const researchPolicyValue = baseConfig?.RESEARCH_ENTRY_POLICY ?? config.RESEARCH_ENTRY_POLICY;
8047
8705
  const researchEntryPolicy = researchPolicyValue == null ? void 0 : parseResearchEntryPolicy(researchPolicyValue);
8048
8706
  if (researchEntryPolicy && (env !== "BACKTEST" || Reflect.get(connector, "__tradejsTestConnector") !== true))
@@ -8207,7 +8865,10 @@ var createStrategyRuntime = ({
8207
8865
  return invokeHook(stage, hook, params, errorContext);
8208
8866
  };
8209
8867
  const invokeGateHooks = async (stage, hook, params, errorContext = {}) => {
8210
- const failureResult = stage === "beforeEntryGate" ? { allow: false, reason: "ERROR" } : void 0;
8868
+ const failureResult = {
8869
+ allow: false,
8870
+ reason: "ERROR"
8871
+ };
8211
8872
  const projectResults = await invokeProjectHooks(
8212
8873
  stage,
8213
8874
  params,
@@ -8228,6 +8889,12 @@ var createStrategyRuntime = ({
8228
8889
  failureResult
8229
8890
  );
8230
8891
  };
8892
+ const authorizeClose = (decision, market, owner = strategyName) => invokeGateHooks(
8893
+ "beforeClosePosition",
8894
+ resolveManifest(owner)?.hooks?.beforeClosePosition,
8895
+ { ctx: getHookCtx(owner), market, decision },
8896
+ { decision, market }
8897
+ );
8231
8898
  const applyProjectAfterCoreDecisionHooks = async ({
8232
8899
  hookCtx,
8233
8900
  market,
@@ -8341,6 +9008,20 @@ var createStrategyRuntime = ({
8341
9008
  interval: decisionInterval
8342
9009
  }) => {
8343
9010
  if (!baseContext) return void 0;
9011
+ const profileSource = strategyManifest?.contextRequirements?.tickerProfiles;
9012
+ if (profileSource) {
9013
+ const profile = await loadTickerProfileContext({
9014
+ ...profileSource,
9015
+ projectRoot,
9016
+ symbol: decisionSymbol,
9017
+ timestamp: candle.timestamp
9018
+ });
9019
+ if (!profile)
9020
+ throw new Error(
9021
+ "Required ticker profile is unavailable at decision time"
9022
+ );
9023
+ baseContext = withTickerProfile(baseContext, profile);
9024
+ }
8344
9025
  if (!coreContextRequirements.has("hyperliquidWhales")) {
8345
9026
  return baseContext;
8346
9027
  }
@@ -8508,6 +9189,8 @@ var createStrategyRuntime = ({
8508
9189
  runtime: pending2.runtime
8509
9190
  });
8510
9191
  return executeEntryDecision({
9192
+ portfolio,
9193
+ portfolioTime: env === "BACKTEST" || env === "PARITY" ? candle.timestamp : Date.now(),
8511
9194
  connector,
8512
9195
  symbol,
8513
9196
  decision: pending2.decision,
@@ -8525,6 +9208,11 @@ var createStrategyRuntime = ({
8525
9208
  });
8526
9209
  };
8527
9210
  const runWithDecisionOverride = async (candle, btcCandle, options = {}) => {
9211
+ portfolioMarket = { candle, btcCandle };
9212
+ if (portfolioContext)
9213
+ await portfolioContext(
9214
+ env === "BACKTEST" || env === "PARITY" ? candle.timestamp : Date.now()
9215
+ );
8528
9216
  appendCurrentMarketData(candle, btcCandle, options.ethCandle);
8529
9217
  const ethCandle = resolveEthCandle(candle, options.ethCandle);
8530
9218
  indicatorsState.setCurrentBar(candle, btcCandle, ethCandle);
@@ -8627,15 +9315,10 @@ var createStrategyRuntime = ({
8627
9315
  if (!makeOrdersEnabled) {
8628
9316
  return decision.code;
8629
9317
  }
8630
- const closeGate = await invokeGateHooks(
8631
- "beforeClosePosition",
8632
- decisionManifest?.hooks?.beforeClosePosition,
8633
- {
8634
- ctx: decisionHookCtx,
8635
- market,
8636
- decision
8637
- },
8638
- { decision, market }
9318
+ const closeGate = await authorizeClose(
9319
+ decision,
9320
+ market,
9321
+ decisionHookCtx.strategyName
8639
9322
  );
8640
9323
  if (closeGate?.allow === false) {
8641
9324
  return closeGate.reason ? `CLOSE_BLOCKED_BY_HOOK:${closeGate.reason}` : "CLOSE_BLOCKED_BY_HOOK";
@@ -8695,6 +9378,13 @@ var createStrategyRuntime = ({
8695
9378
  });
8696
9379
  const signal = decision.signal;
8697
9380
  if (signal) {
9381
+ if (portfolio)
9382
+ signal.additionalIndicators = {
9383
+ ...signal.additionalIndicators,
9384
+ portfolioContext: await portfolioContext(
9385
+ env === "BACKTEST" || env === "PARITY" ? decision.entryContext.timestamp : Date.now()
9386
+ )
9387
+ };
8698
9388
  if (runtimeLineage) signal.runtimeLineage = runtimeLineage;
8699
9389
  if (universe) signal.universe = universe;
8700
9390
  if (assetClass) signal.assetClass = assetClass;
@@ -8934,6 +9624,8 @@ var createStrategyRuntime = ({
8934
9624
  });
8935
9625
  }
8936
9626
  return executeEntryDecision({
9627
+ portfolio,
9628
+ portfolioTime: env === "BACKTEST" || env === "PARITY" ? candle.timestamp : Date.now(),
8937
9629
  connector,
8938
9630
  symbol,
8939
9631
  decision,
@@ -9045,7 +9737,7 @@ var normalizeForCanonicalJson = (value) => {
9045
9737
  }
9046
9738
  return value;
9047
9739
  };
9048
- var revision = (prefix, value) => `${prefix}:${(0, import_node_crypto4.createHash)("sha256").update(JSON.stringify(normalizeForCanonicalJson(value))).digest("hex").slice(0, 16)}`;
9740
+ var revision = (prefix, value) => `${prefix}:${(0, import_node_crypto5.createHash)("sha256").update(JSON.stringify(normalizeForCanonicalJson(value))).digest("hex").slice(0, 16)}`;
9049
9741
  var computeStrategyRevision = ({
9050
9742
  strategyName,
9051
9743
  strategyPackage,
@@ -9630,18 +10322,18 @@ var loadRuntimeTrades = async (userName, {
9630
10322
  const [bucketTradeGroups, legacyKeys] = await Promise.all([
9631
10323
  Promise.all(
9632
10324
  dayKeys.map(
9633
- (dayKey) => (0, import_redis4.getHashJsonValues)(
9634
- import_redis4.redisKeys.runtimeTradeBucket(userName, dayKey)
10325
+ (dayKey) => (0, import_redis5.getHashJsonValues)(
10326
+ import_redis5.redisKeys.runtimeTradeBucket(userName, dayKey)
9635
10327
  )
9636
10328
  )
9637
10329
  ),
9638
- (0, import_redis4.getKeys)(import_redis4.redisKeys.runtimeTrades(userName))
10330
+ (0, import_redis5.getKeys)(import_redis5.redisKeys.runtimeTrades(userName))
9639
10331
  ]);
9640
10332
  const legacyTradeKeys = legacyKeys.filter(
9641
- (key) => !key.startsWith(import_redis4.redisKeys.runtimeTradeBuckets(userName))
10333
+ (key) => !key.startsWith(import_redis5.redisKeys.runtimeTradeBuckets(userName))
9642
10334
  );
9643
10335
  const legacyTrades = await Promise.all(
9644
- legacyTradeKeys.map((key) => (0, import_redis4.getData)(key, null))
10336
+ legacyTradeKeys.map((key) => (0, import_redis5.getData)(key, null))
9645
10337
  );
9646
10338
  const dedupedTrades = /* @__PURE__ */ new Map();
9647
10339
  for (const trade of [...legacyTrades, ...bucketTradeGroups.flat()]) {
@@ -9728,8 +10420,8 @@ var loadExchangeRange = async ({
9728
10420
  }
9729
10421
  };
9730
10422
  var loadActiveRuntimeOrderIds = async (userName) => {
9731
- const keys = await (0, import_redis4.getKeys)(import_redis4.redisKeys.runtimeActiveTrades(userName));
9732
- const refs = await Promise.all(keys.map((key) => (0, import_redis4.getData)(key, null)));
10423
+ const keys = await (0, import_redis5.getKeys)(import_redis5.redisKeys.runtimeActiveTrades(userName));
10424
+ const refs = await Promise.all(keys.map((key) => (0, import_redis5.getData)(key, null)));
9733
10425
  return new Set(
9734
10426
  refs.map(
9735
10427
  (ref) => typeof ref?.orderId === "string" && ref.orderId.trim() ? ref.orderId.trim() : null