@tradejs/cli 2.0.20 → 3.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli.js +267 -98
- package/dist/lib/aiTrainOptions.js +36 -0
- package/dist/lib/coreResearch.js +73 -8
- package/dist/lib/runtimeLineage.js +2 -14
- package/dist/lib/strategyRelease.js +82 -17
- package/dist/scripts/agentRun.js +22 -23
- package/dist/scripts/aiPocketSearch.js +2 -1
- package/dist/scripts/aiTrain.js +64 -13
- package/dist/scripts/coreResearch.js +73 -8
- package/dist/scripts/replay.js +2 -14
- package/dist/scripts/replayRunner.js +2 -14
- package/dist/scripts/runtimeEvidence.js +1 -0
- package/dist/scripts/runtimeScorecard.js +35 -2
- package/dist/scripts/signals.js +2 -14
- package/dist/scripts/signalsDaemon.js +2 -14
- package/dist/scripts/strategyRelease.js +82 -17
- package/package.json +9 -9
|
@@ -20,7 +20,9 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
|
|
|
20
20
|
// src/lib/aiTrainOptions.ts
|
|
21
21
|
var aiTrainOptions_exports = {};
|
|
22
22
|
__export(aiTrainOptions_exports, {
|
|
23
|
+
assertAiTrainDirectionPolicy: () => assertAiTrainDirectionPolicy,
|
|
23
24
|
hasCliOption: () => hasCliOption,
|
|
25
|
+
parseAiTrainDirectionPolicy: () => parseAiTrainDirectionPolicy,
|
|
24
26
|
parseDumpFeatureMode: () => parseDumpFeatureMode,
|
|
25
27
|
parseQualityThresholds: () => parseQualityThresholds,
|
|
26
28
|
parseTerminalWindowDays: () => parseTerminalWindowDays,
|
|
@@ -81,6 +83,38 @@ var parseTerminalWindowDays = (value) => {
|
|
|
81
83
|
const raw = String(value ?? "90,30,7").split(",").map((part) => Number(part.trim())).filter((part) => Number.isFinite(part)).map((part) => Math.trunc(part)).filter((part) => part > 0);
|
|
82
84
|
return [...new Set(raw)].sort((left, right) => right - left);
|
|
83
85
|
};
|
|
86
|
+
var parseAiTrainDirectionPolicy = (value) => {
|
|
87
|
+
const normalized = String(value ?? "both").trim().toLowerCase().replaceAll("-", "_");
|
|
88
|
+
if (normalized === "both" || normalized === "long_only" || normalized === "short_only" || normalized === "direction_aware") {
|
|
89
|
+
return normalized;
|
|
90
|
+
}
|
|
91
|
+
throw new Error(
|
|
92
|
+
`Invalid --directionPolicy value "${String(value)}". Use both, long_only, short_only, or direction_aware.`
|
|
93
|
+
);
|
|
94
|
+
};
|
|
95
|
+
var assertAiTrainDirectionPolicy = ({
|
|
96
|
+
directionPolicy,
|
|
97
|
+
byDirection,
|
|
98
|
+
terminalWindows
|
|
99
|
+
}) => {
|
|
100
|
+
const suppressedDirection = directionPolicy === "long_only" ? "SHORT" : directionPolicy === "short_only" ? "LONG" : null;
|
|
101
|
+
if (!suppressedDirection) return;
|
|
102
|
+
for (const scope of [{ label: "full", byDirection }, ...terminalWindows]) {
|
|
103
|
+
const summary = scope.byDirection.find(
|
|
104
|
+
(entry) => entry.direction === suppressedDirection
|
|
105
|
+
);
|
|
106
|
+
if (!summary) {
|
|
107
|
+
throw new Error(
|
|
108
|
+
`${scope.label} ${suppressedDirection} direction summary is required for ${directionPolicy}`
|
|
109
|
+
);
|
|
110
|
+
}
|
|
111
|
+
if (summary.summary.approved !== 0) {
|
|
112
|
+
throw new Error(
|
|
113
|
+
`${scope.label} has ${summary.summary.approved} ${suppressedDirection} approved rows under ${directionPolicy}`
|
|
114
|
+
);
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
};
|
|
84
118
|
var parseDumpFeatureMode = (value) => {
|
|
85
119
|
const text = String(value ?? "none").trim().toLowerCase();
|
|
86
120
|
if (!text || text === "none" || text === "false" || text === "0") {
|
|
@@ -120,7 +154,9 @@ var resolveAiTrainRecentLimit = ({
|
|
|
120
154
|
};
|
|
121
155
|
// Annotate the CommonJS export names for ESM import in node:
|
|
122
156
|
0 && (module.exports = {
|
|
157
|
+
assertAiTrainDirectionPolicy,
|
|
123
158
|
hasCliOption,
|
|
159
|
+
parseAiTrainDirectionPolicy,
|
|
124
160
|
parseDumpFeatureMode,
|
|
125
161
|
parseQualityThresholds,
|
|
126
162
|
parseTerminalWindowDays,
|
package/dist/lib/coreResearch.js
CHANGED
|
@@ -1513,6 +1513,7 @@ var normalizeCompletedTrade = (params) => {
|
|
|
1513
1513
|
runId: row.backtestRunId ?? null,
|
|
1514
1514
|
configId: row.configId?.trim() || "<missing-config-id>",
|
|
1515
1515
|
signalId,
|
|
1516
|
+
positionCycleId: typeof result.positionCycleId === "string" && result.positionCycleId.trim() ? result.positionCycleId.trim() : null,
|
|
1516
1517
|
setupIdentity,
|
|
1517
1518
|
setupIdentitySource: explicitIdentity ? "research.setupIdentity" : strategyIdentity ? "strategy-context" : "signal-time-fallback",
|
|
1518
1519
|
strategy: row.strategyName,
|
|
@@ -1536,6 +1537,69 @@ var normalizeCompletedTrade = (params) => {
|
|
|
1536
1537
|
};
|
|
1537
1538
|
};
|
|
1538
1539
|
var completedTradeIdentity = (trade) => [trade.runId ?? "", trade.configId, trade.signalId, trade.symbol].join("|");
|
|
1540
|
+
var positionCycleIdentity = (trade) => trade.positionCycleId ? [
|
|
1541
|
+
trade.runId ?? "",
|
|
1542
|
+
trade.configId,
|
|
1543
|
+
trade.strategy,
|
|
1544
|
+
trade.symbol,
|
|
1545
|
+
trade.direction,
|
|
1546
|
+
trade.positionCycleId
|
|
1547
|
+
].join("|") : null;
|
|
1548
|
+
var sumBy = (trades, select) => trades.reduce((sum, trade) => sum + select(trade), 0);
|
|
1549
|
+
var weightedPrice = (trades, select) => {
|
|
1550
|
+
const withPrice = trades.filter((trade) => select(trade) != null);
|
|
1551
|
+
const qty = sumBy(withPrice, (trade) => trade.qty);
|
|
1552
|
+
if (qty <= 0) return null;
|
|
1553
|
+
return sumBy(withPrice, (trade) => Number(select(trade)) * trade.qty) / qty;
|
|
1554
|
+
};
|
|
1555
|
+
var aggregatePositionCycles = (trades) => {
|
|
1556
|
+
const standalone = [];
|
|
1557
|
+
const cycles = /* @__PURE__ */ new Map();
|
|
1558
|
+
for (const trade of trades) {
|
|
1559
|
+
const identity = positionCycleIdentity(trade);
|
|
1560
|
+
if (!identity) {
|
|
1561
|
+
standalone.push(trade);
|
|
1562
|
+
continue;
|
|
1563
|
+
}
|
|
1564
|
+
const cycle = cycles.get(identity) ?? [];
|
|
1565
|
+
cycle.push(trade);
|
|
1566
|
+
cycles.set(identity, cycle);
|
|
1567
|
+
}
|
|
1568
|
+
for (const [identity, cycle] of cycles) {
|
|
1569
|
+
const primaryRows = cycle.filter(
|
|
1570
|
+
(trade) => trade.signalId === trade.positionCycleId
|
|
1571
|
+
);
|
|
1572
|
+
if (primaryRows.length !== 1) {
|
|
1573
|
+
throw new Error(
|
|
1574
|
+
`Position cycle ${identity} must contain exactly one opening row; found ${primaryRows.length}`
|
|
1575
|
+
);
|
|
1576
|
+
}
|
|
1577
|
+
const primary = primaryRows[0];
|
|
1578
|
+
if (cycle.some(
|
|
1579
|
+
(trade) => trade.exitTimestamp !== primary.exitTimestamp || trade.exitReason !== primary.exitReason
|
|
1580
|
+
)) {
|
|
1581
|
+
throw new Error(
|
|
1582
|
+
`Position cycle ${identity} contains inconsistent final exits`
|
|
1583
|
+
);
|
|
1584
|
+
}
|
|
1585
|
+
if (cycle.length === 1) {
|
|
1586
|
+
standalone.push(primary);
|
|
1587
|
+
continue;
|
|
1588
|
+
}
|
|
1589
|
+
standalone.push({
|
|
1590
|
+
...primary,
|
|
1591
|
+
entryTimestamp: Math.min(...cycle.map((trade) => trade.entryTimestamp)),
|
|
1592
|
+
entryPrice: weightedPrice(cycle, (trade) => trade.entryPrice) ?? 0,
|
|
1593
|
+
exitPrice: weightedPrice(cycle, (trade) => trade.exitPrice),
|
|
1594
|
+
qty: sumBy(cycle, (trade) => trade.qty),
|
|
1595
|
+
netProfit: sumBy(cycle, (trade) => trade.netProfit),
|
|
1596
|
+
grossProfit: sumBy(cycle, (trade) => trade.grossProfit),
|
|
1597
|
+
totalFee: sumBy(cycle, (trade) => trade.totalFee),
|
|
1598
|
+
totalSlippageCost: sumBy(cycle, (trade) => trade.totalSlippageCost)
|
|
1599
|
+
});
|
|
1600
|
+
}
|
|
1601
|
+
return standalone;
|
|
1602
|
+
};
|
|
1539
1603
|
var compareText = (left, right) => left < right ? -1 : left > right ? 1 : 0;
|
|
1540
1604
|
var compareTrades = (left, right) => left.exitTimestamp - right.exitTimestamp || compareText(left.symbol, right.symbol) || compareText(left.direction, right.direction) || compareText(left.signalId, right.signalId) || compareText(left.sourceFile, right.sourceFile) || left.sourceLine - right.sourceLine;
|
|
1541
1605
|
var readCoreResearchVariant = async (variant) => {
|
|
@@ -1612,7 +1676,7 @@ var readCoreResearchVariant = async (variant) => {
|
|
|
1612
1676
|
variant,
|
|
1613
1677
|
files: sources,
|
|
1614
1678
|
duplicateRowsDropped,
|
|
1615
|
-
trades: trades.sort(compareTrades)
|
|
1679
|
+
trades: aggregatePositionCycles(trades).sort(compareTrades)
|
|
1616
1680
|
};
|
|
1617
1681
|
};
|
|
1618
1682
|
|
|
@@ -2093,13 +2157,17 @@ var analyzeCoreResearchPrepared = async (spec, options = {}) => {
|
|
|
2093
2157
|
const loaded = await Promise.all(
|
|
2094
2158
|
analysisSpec.variants.map(readCoreResearchVariant)
|
|
2095
2159
|
);
|
|
2160
|
+
const periodDays = (spec.window.end - spec.window.start) / DAY_MS;
|
|
2096
2161
|
const windowed = loaded.map((entry) => ({
|
|
2097
2162
|
...entry,
|
|
2163
|
+
reconciliationMetrics: summarizeCoreResearchCohorts(
|
|
2164
|
+
entry.trades,
|
|
2165
|
+
periodDays
|
|
2166
|
+
).ALL,
|
|
2098
2167
|
trades: entry.trades.filter(
|
|
2099
2168
|
(trade) => trade.exitTimestamp >= spec.window.start && trade.exitTimestamp < spec.window.end
|
|
2100
2169
|
)
|
|
2101
2170
|
}));
|
|
2102
|
-
const periodDays = (spec.window.end - spec.window.start) / DAY_MS;
|
|
2103
2171
|
const analyses = await Promise.all(
|
|
2104
2172
|
windowed.map(async (entry) => ({
|
|
2105
2173
|
variant: entry.variant,
|
|
@@ -2157,12 +2225,9 @@ var analyzeCoreResearchPrepared = async (spec, options = {}) => {
|
|
|
2157
2225
|
reconciliation: await reconcileCoreResearchVariant({
|
|
2158
2226
|
variant: entry.variant,
|
|
2159
2227
|
spec,
|
|
2160
|
-
|
|
2161
|
-
|
|
2162
|
-
|
|
2163
|
-
start: spec.window.start,
|
|
2164
|
-
end: spec.window.end
|
|
2165
|
-
}).cohorts.ALL
|
|
2228
|
+
// Redis result.stat includes the runner's terminal close at window.end.
|
|
2229
|
+
// Keep half-open research metrics, but reconcile the complete run export.
|
|
2230
|
+
exportMetrics: entry.reconciliationMetrics
|
|
2166
2231
|
}),
|
|
2167
2232
|
supplemental: {
|
|
2168
2233
|
coldStart: Object.fromEntries(
|
|
@@ -43,6 +43,7 @@ var import_promises2 = __toESM(require("fs/promises"));
|
|
|
43
43
|
var import_node_path2 = __toESM(require("path"));
|
|
44
44
|
var import_types = require("@tradejs/types");
|
|
45
45
|
var import_strategies = require("@tradejs/node/strategies");
|
|
46
|
+
var import_strategyReleaseEvidence = require("@tradejs/infra/strategyReleaseEvidence");
|
|
46
47
|
|
|
47
48
|
// src/lib/strategyGateFingerprint.ts
|
|
48
49
|
var import_node_crypto = require("crypto");
|
|
@@ -137,17 +138,6 @@ var normalizeForStableJson = (value) => {
|
|
|
137
138
|
}
|
|
138
139
|
return value;
|
|
139
140
|
};
|
|
140
|
-
var normalizeRuntimeLogicConfig = (value) => {
|
|
141
|
-
if (Array.isArray(value)) {
|
|
142
|
-
return value.map(normalizeRuntimeLogicConfig);
|
|
143
|
-
}
|
|
144
|
-
if (value && typeof value === "object") {
|
|
145
|
-
return Object.fromEntries(
|
|
146
|
-
Object.entries(value).filter(([key]) => key !== "MAX_LOSS_VALUE").sort(([left], [right]) => left.localeCompare(right)).map(([key, entry]) => [key, normalizeRuntimeLogicConfig(entry)])
|
|
147
|
-
);
|
|
148
|
-
}
|
|
149
|
-
return value;
|
|
150
|
-
};
|
|
151
141
|
var resolveRuntimeMaxLossValue = (config) => {
|
|
152
142
|
if (!config || typeof config !== "object" || Array.isArray(config)) {
|
|
153
143
|
return null;
|
|
@@ -311,9 +301,7 @@ var buildRuntimeLineage = async ({
|
|
|
311
301
|
})).gateFingerprint,
|
|
312
302
|
// Position risk is tracked separately so changing trade size does not
|
|
313
303
|
// masquerade as a change to the core + gate decision logic.
|
|
314
|
-
configFingerprint:
|
|
315
|
-
normalizeRuntimeLogicConfig(config)
|
|
316
|
-
),
|
|
304
|
+
configFingerprint: (0, import_strategyReleaseEvidence.strategyLogicConfigFingerprint)(config),
|
|
317
305
|
contextFingerprint: fingerprintRuntimeValue(context),
|
|
318
306
|
maxLossValue: resolveRuntimeMaxLossValue(config)
|
|
319
307
|
};
|
|
@@ -81,6 +81,12 @@ var hasExactRuntimeTarget = (target) => Boolean(
|
|
|
81
81
|
);
|
|
82
82
|
var deriveStrategyReleaseResearchDecision = async (input) => {
|
|
83
83
|
const blockers = [];
|
|
84
|
+
const directionPolicy = input.directionPolicy ?? "both";
|
|
85
|
+
const activeDirections = directionPolicy === "long_only" ? ["long"] : directionPolicy === "short_only" ? ["short"] : ["long", "short"];
|
|
86
|
+
const suppressedDirection = directionPolicy === "long_only" ? "short" : directionPolicy === "short_only" ? "long" : null;
|
|
87
|
+
const hasHistoricalEdge = (entry) => entry.pnl > 0 && entry.profitFactor >= 1 && activeDirections.every(
|
|
88
|
+
(direction) => entry[direction].pnl > 0 && entry[direction].profitFactor >= 1
|
|
89
|
+
) && (suppressedDirection == null || entry[suppressedDirection].trades === 0 && entry[suppressedDirection].pnl === 0);
|
|
84
90
|
const byDays = new Map(
|
|
85
91
|
input.historicalWindows.map((entry) => [entry.days, entry])
|
|
86
92
|
);
|
|
@@ -93,17 +99,13 @@ var deriveStrategyReleaseResearchDecision = async (input) => {
|
|
|
93
99
|
const longWindows = REQUIRED_LONG_WINDOWS.map(
|
|
94
100
|
(days) => byDays.get(days)
|
|
95
101
|
).filter((entry) => Boolean(entry));
|
|
96
|
-
if (longWindows.some(
|
|
97
|
-
(entry) => entry.pnl <= 0 || entry.profitFactor < 1 || entry.long.pnl <= 0 || entry.long.profitFactor < 1 || entry.short.pnl <= 0 || entry.short.profitFactor < 1
|
|
98
|
-
)) {
|
|
102
|
+
if (longWindows.some((entry) => !hasHistoricalEdge(entry))) {
|
|
99
103
|
blockers.push("HISTORICAL_EDGE_FAILED");
|
|
100
104
|
}
|
|
101
105
|
const stableTerminalWindows = REQUIRED_STABLE_TERMINAL_WINDOWS.map(
|
|
102
106
|
(days) => byDays.get(days)
|
|
103
107
|
).filter((entry) => Boolean(entry));
|
|
104
|
-
if (stableTerminalWindows.some(
|
|
105
|
-
(entry) => entry.pnl <= 0 || entry.profitFactor < 1 || entry.long.pnl <= 0 || entry.long.profitFactor < 1 || entry.short.pnl <= 0 || entry.short.profitFactor < 1
|
|
106
|
-
)) {
|
|
108
|
+
if (stableTerminalWindows.some((entry) => !hasHistoricalEdge(entry))) {
|
|
107
109
|
blockers.push("HISTORICAL_EDGE_FAILED");
|
|
108
110
|
}
|
|
109
111
|
if (blockers.length) {
|
|
@@ -113,8 +115,9 @@ var deriveStrategyReleaseResearchDecision = async (input) => {
|
|
|
113
115
|
repairAllowed: false,
|
|
114
116
|
targetDirection: input.recentFailure?.direction ?? null,
|
|
115
117
|
maxLossValue: null,
|
|
118
|
+
requiresRuntimeBinding: false,
|
|
116
119
|
blockers,
|
|
117
|
-
summary:
|
|
120
|
+
summary: `The final ${directionPolicy} composition does not have complete positive evidence for ALL and every active direction on the frozen 3y/4y/max-window matrix.`
|
|
118
121
|
};
|
|
119
122
|
}
|
|
120
123
|
const repairAllowed = Boolean(
|
|
@@ -127,6 +130,7 @@ var deriveStrategyReleaseResearchDecision = async (input) => {
|
|
|
127
130
|
repairAllowed: true,
|
|
128
131
|
targetDirection: input.recentFailure.direction,
|
|
129
132
|
maxLossValue: null,
|
|
133
|
+
requiresRuntimeBinding: false,
|
|
130
134
|
blockers: [],
|
|
131
135
|
summary: "Spend the single terminal-direction repair round on the preregistered causal mechanism, then rebuild the full evidence matrix."
|
|
132
136
|
};
|
|
@@ -145,6 +149,7 @@ var deriveStrategyReleaseResearchDecision = async (input) => {
|
|
|
145
149
|
repairAllowed: false,
|
|
146
150
|
targetDirection: input.recentFailure?.direction ?? null,
|
|
147
151
|
maxLossValue: null,
|
|
152
|
+
requiresRuntimeBinding: false,
|
|
148
153
|
blockers,
|
|
149
154
|
summary: "Complete the frozen candidate implementation and full-period chart before any micro-forward execution."
|
|
150
155
|
};
|
|
@@ -156,6 +161,9 @@ var deriveStrategyReleaseResearchDecision = async (input) => {
|
|
|
156
161
|
repairAllowed: false,
|
|
157
162
|
targetDirection: input.recentFailure?.direction ?? null,
|
|
158
163
|
maxLossValue: 1,
|
|
164
|
+
requiresRuntimeBinding: !hasExactRuntimeTarget(
|
|
165
|
+
input.forwardTest.runtimeTarget
|
|
166
|
+
),
|
|
159
167
|
blockers: ["FORWARD_NOT_AUTHORIZED"],
|
|
160
168
|
summary: "The candidate is ready for a separately authorized micro-forward test at MAX_LOSS_VALUE=1."
|
|
161
169
|
};
|
|
@@ -163,12 +171,13 @@ var deriveStrategyReleaseResearchDecision = async (input) => {
|
|
|
163
171
|
if (!hasExactRuntimeTarget(input.forwardTest.runtimeTarget)) {
|
|
164
172
|
return {
|
|
165
173
|
strategy: input.strategy,
|
|
166
|
-
action: "
|
|
174
|
+
action: "MICRO_FORWARD_READY",
|
|
167
175
|
repairAllowed: false,
|
|
168
176
|
targetDirection: input.recentFailure?.direction ?? null,
|
|
169
177
|
maxLossValue: 1,
|
|
170
|
-
|
|
171
|
-
|
|
178
|
+
requiresRuntimeBinding: true,
|
|
179
|
+
blockers: [],
|
|
180
|
+
summary: "The portable micro-forward handoff is ready. Bind it on the runtime server to that server's deployment/account ids and verify canonical composition fingerprints there."
|
|
172
181
|
};
|
|
173
182
|
}
|
|
174
183
|
return {
|
|
@@ -177,6 +186,7 @@ var deriveStrategyReleaseResearchDecision = async (input) => {
|
|
|
177
186
|
repairAllowed: false,
|
|
178
187
|
targetDirection: input.recentFailure?.direction ?? null,
|
|
179
188
|
maxLossValue: 1,
|
|
189
|
+
requiresRuntimeBinding: false,
|
|
180
190
|
blockers: [],
|
|
181
191
|
summary: "Start the exact frozen composition as a micro-forward test at MAX_LOSS_VALUE=1; do not retune an underpowered or exposed recent tail."
|
|
182
192
|
};
|
|
@@ -234,6 +244,7 @@ var deriveReleaseGates = (evidence) => {
|
|
|
234
244
|
};
|
|
235
245
|
};
|
|
236
246
|
var verifyEvidenceComposition = (strategy, composition, evidence) => {
|
|
247
|
+
const directionPolicy = composition.directionPolicy ?? "both";
|
|
237
248
|
const requireEqual = (actual, expected, label, artifactId) => assert(
|
|
238
249
|
actual === expected,
|
|
239
250
|
`evidence ${artifactId} ${label} does not match the frozen composition`
|
|
@@ -304,6 +315,14 @@ var verifyEvidenceComposition = (strategy, composition, evidence) => {
|
|
|
304
315
|
"gateContextFingerprint",
|
|
305
316
|
reference.artifactId
|
|
306
317
|
);
|
|
318
|
+
if (directionPolicy !== "both" || lineage?.directionPolicy != null) {
|
|
319
|
+
requireEqual(
|
|
320
|
+
lineage?.directionPolicy,
|
|
321
|
+
directionPolicy,
|
|
322
|
+
"directionPolicy",
|
|
323
|
+
reference.artifactId
|
|
324
|
+
);
|
|
325
|
+
}
|
|
307
326
|
assert(
|
|
308
327
|
lineage?.sourceSha256s.includes(composition.coreExportSha256),
|
|
309
328
|
`evidence ${reference.artifactId} does not use the frozen core export`
|
|
@@ -348,6 +367,14 @@ var verifyEvidenceComposition = (strategy, composition, evidence) => {
|
|
|
348
367
|
"MAX_LOSS_VALUE",
|
|
349
368
|
reference.artifactId
|
|
350
369
|
);
|
|
370
|
+
if (directionPolicy !== "both" || lineage?.directionPolicy != null) {
|
|
371
|
+
requireEqual(
|
|
372
|
+
lineage?.directionPolicy,
|
|
373
|
+
directionPolicy,
|
|
374
|
+
"directionPolicy",
|
|
375
|
+
reference.artifactId
|
|
376
|
+
);
|
|
377
|
+
}
|
|
351
378
|
continue;
|
|
352
379
|
}
|
|
353
380
|
if (reference.kind === "execution_calibration") {
|
|
@@ -388,6 +415,14 @@ var verifyEvidenceComposition = (strategy, composition, evidence) => {
|
|
|
388
415
|
"MAX_LOSS_VALUE",
|
|
389
416
|
reference.artifactId
|
|
390
417
|
);
|
|
418
|
+
if (directionPolicy !== "both" || lineage?.directionPolicy != null) {
|
|
419
|
+
requireEqual(
|
|
420
|
+
lineage?.directionPolicy,
|
|
421
|
+
directionPolicy,
|
|
422
|
+
"directionPolicy",
|
|
423
|
+
reference.artifactId
|
|
424
|
+
);
|
|
425
|
+
}
|
|
391
426
|
}
|
|
392
427
|
}
|
|
393
428
|
};
|
|
@@ -423,6 +458,12 @@ var createStrategyReleaseManifest = (input) => {
|
|
|
423
458
|
input.composition.longEnabled && input.composition.shortEnabled,
|
|
424
459
|
"LONG and SHORT must both remain enabled"
|
|
425
460
|
);
|
|
461
|
+
assert(
|
|
462
|
+
input.composition.directionPolicy == null || ["both", "long_only", "short_only", "direction_aware"].includes(
|
|
463
|
+
input.composition.directionPolicy
|
|
464
|
+
),
|
|
465
|
+
"composition.directionPolicy is invalid"
|
|
466
|
+
);
|
|
426
467
|
assert(
|
|
427
468
|
Number.isInteger(input.monitoring.minimumProspectiveClosedTrades) && input.monitoring.minimumProspectiveClosedTrades > 0,
|
|
428
469
|
"monitoring.minimumProspectiveClosedTrades must be positive"
|
|
@@ -487,7 +528,8 @@ var createStrategyReleaseManifest = (input) => {
|
|
|
487
528
|
gateFingerprint: input.composition.gateFingerprint,
|
|
488
529
|
gateContextFingerprint: input.composition.gateContextFingerprint,
|
|
489
530
|
runtimeContextFingerprint: input.composition.runtimeContextFingerprint,
|
|
490
|
-
maxLossValue: input.composition.maxLossValue
|
|
531
|
+
maxLossValue: input.composition.maxLossValue,
|
|
532
|
+
...input.composition.directionPolicy == null ? {} : { directionPolicy: input.composition.directionPolicy }
|
|
491
533
|
};
|
|
492
534
|
const compositionId = `${safeSegment(input.strategy, "strategy")}_${strategyReleaseSha256(compositionIdentity).slice(0, 16)}`;
|
|
493
535
|
const reasons = releaseReasons({ ...input, gates: evidenceGates });
|
|
@@ -577,7 +619,8 @@ var verifyStrategyReleaseEnvelope = async (valueOrPath) => {
|
|
|
577
619
|
runtimeContextFingerprint: manifest.composition.runtimeContextFingerprint,
|
|
578
620
|
maxLossValue: manifest.composition.maxLossValue,
|
|
579
621
|
longEnabled: manifest.composition.longEnabled,
|
|
580
|
-
shortEnabled: manifest.composition.shortEnabled
|
|
622
|
+
shortEnabled: manifest.composition.shortEnabled,
|
|
623
|
+
directionPolicy: manifest.composition.directionPolicy
|
|
581
624
|
},
|
|
582
625
|
marketWindow: manifest.marketWindow,
|
|
583
626
|
researchBudget: manifest.researchBudget,
|
|
@@ -1001,6 +1044,7 @@ var finiteNumber = (value) => {
|
|
|
1001
1044
|
const number = Number(value);
|
|
1002
1045
|
return Number.isFinite(number) ? number : null;
|
|
1003
1046
|
};
|
|
1047
|
+
var finiteDirectionPolicy = (value) => ["both", "long_only", "short_only", "direction_aware"].includes(String(value)) ? value : null;
|
|
1004
1048
|
var uniqueStrings = (values) => [
|
|
1005
1049
|
...new Set(
|
|
1006
1050
|
values.map(finiteString).filter((value) => value != null)
|
|
@@ -1017,6 +1061,7 @@ var incompleteEvidenceLineage = () => ({
|
|
|
1017
1061
|
gateContextFingerprint: null,
|
|
1018
1062
|
runtimeContextFingerprint: null,
|
|
1019
1063
|
maxLossValue: null,
|
|
1064
|
+
directionPolicy: null,
|
|
1020
1065
|
sourceSha256s: []
|
|
1021
1066
|
});
|
|
1022
1067
|
var extractEvidenceLineage = ({
|
|
@@ -1079,6 +1124,7 @@ var extractEvidenceLineage = ({
|
|
|
1079
1124
|
gateConfigIdsFingerprint: finiteString(lineage?.configIdsFingerprint),
|
|
1080
1125
|
gateFingerprint: finiteString(lineage?.gateFingerprint),
|
|
1081
1126
|
gateContextFingerprint: finiteString(lineage?.contextFingerprint),
|
|
1127
|
+
directionPolicy: finiteDirectionPolicy(lineage?.directionPolicy),
|
|
1082
1128
|
sourceSha256s: uniqueStrings(array(lineage?.sourceSha256s))
|
|
1083
1129
|
};
|
|
1084
1130
|
}
|
|
@@ -1114,7 +1160,8 @@ var extractEvidenceLineage = ({
|
|
|
1114
1160
|
gateFingerprint: one("gateFingerprint"),
|
|
1115
1161
|
runtimeConfigFingerprint: one("configFingerprint"),
|
|
1116
1162
|
runtimeContextFingerprint: one("contextFingerprint"),
|
|
1117
|
-
maxLossValue: maxLossValues.length === 1 ? maxLossValues[0] : null
|
|
1163
|
+
maxLossValue: maxLossValues.length === 1 ? maxLossValues[0] : null,
|
|
1164
|
+
directionPolicy: finiteDirectionPolicy(one("directionPolicy"))
|
|
1118
1165
|
};
|
|
1119
1166
|
}
|
|
1120
1167
|
if (reference.kind === "execution_calibration") {
|
|
@@ -1151,6 +1198,7 @@ var extractEvidenceLineage = ({
|
|
|
1151
1198
|
runtimeConfigFingerprint: one("configFingerprint"),
|
|
1152
1199
|
runtimeContextFingerprint: one("contextFingerprint"),
|
|
1153
1200
|
maxLossValue: maxLossValues.length === 1 ? maxLossValues[0] : null,
|
|
1201
|
+
directionPolicy: finiteDirectionPolicy(one("directionPolicy")),
|
|
1154
1202
|
sourceSha256s: uniqueStrings([
|
|
1155
1203
|
...array(nested(artifact.sources)?.sha256s),
|
|
1156
1204
|
...array(artifact.sourceSha256s)
|
|
@@ -1287,9 +1335,10 @@ async function collectReleaseEvidenceReferences(references) {
|
|
|
1287
1335
|
if (reference.kind === "ai_gate") {
|
|
1288
1336
|
const outcome = nested(artifact.outcome);
|
|
1289
1337
|
const approvedRisk = nested(outcome?.approvedRisk);
|
|
1290
|
-
const
|
|
1291
|
-
|
|
1292
|
-
|
|
1338
|
+
const run = nested(artifact.run);
|
|
1339
|
+
const research = nested(artifact.research);
|
|
1340
|
+
const researchLineage = nested(research?.lineage);
|
|
1341
|
+
const terminalWindows = array(research?.terminalWindows).map(nested).filter((entry) => entry != null);
|
|
1293
1342
|
const hasLongShortDirections = (value) => {
|
|
1294
1343
|
const directions = new Map(
|
|
1295
1344
|
array(value).map(nested).filter(
|
|
@@ -1303,6 +1352,22 @@ async function collectReleaseEvidenceReferences(references) {
|
|
|
1303
1352
|
const directionalEvidenceComplete = hasLongShortDirections(artifact.byDirection) && terminalWindows.every(
|
|
1304
1353
|
(window) => hasLongShortDirections(window.byDirection)
|
|
1305
1354
|
);
|
|
1355
|
+
const directionPolicy = finiteDirectionPolicy(researchLineage?.directionPolicy) ?? "both";
|
|
1356
|
+
const runDirectionPolicy = finiteDirectionPolicy(run?.directionPolicy) ?? "both";
|
|
1357
|
+
const suppressedDirection = directionPolicy === "long_only" ? "SHORT" : directionPolicy === "short_only" ? "LONG" : null;
|
|
1358
|
+
const approvedForDirection = (value, direction) => {
|
|
1359
|
+
const entry = array(value).map(nested).find((candidate) => candidate?.direction === direction);
|
|
1360
|
+
return Number(nested(entry?.summary)?.approved ?? Number.NaN);
|
|
1361
|
+
};
|
|
1362
|
+
const directionPolicyValid = runDirectionPolicy === directionPolicy && (suppressedDirection == null || approvedForDirection(
|
|
1363
|
+
artifact.byDirection,
|
|
1364
|
+
suppressedDirection
|
|
1365
|
+
) === 0 && terminalWindows.every(
|
|
1366
|
+
(window) => approvedForDirection(
|
|
1367
|
+
window.byDirection,
|
|
1368
|
+
suppressedDirection
|
|
1369
|
+
) === 0
|
|
1370
|
+
));
|
|
1306
1371
|
const terminalsPass = terminalWindows.length > 0 && terminalWindows.every((window) => {
|
|
1307
1372
|
const terminalOutcome = nested(window.outcome);
|
|
1308
1373
|
return window.complete === true && Number(terminalOutcome?.approved ?? 0) > 0 && Number(
|
|
@@ -1310,7 +1375,7 @@ async function collectReleaseEvidenceReferences(references) {
|
|
|
1310
1375
|
) > 0;
|
|
1311
1376
|
});
|
|
1312
1377
|
return {
|
|
1313
|
-
aiGateAddsValue: Number(outcome?.expectancyDelta ?? Number.NEGATIVE_INFINITY) > 0 && Number(approvedRisk?.totalProfit ?? Number.NEGATIVE_INFINITY) > 0 && Number(approvedRisk?.profitFactor ?? Number.NEGATIVE_INFINITY) > 1 && directionalEvidenceComplete && terminalsPass
|
|
1378
|
+
aiGateAddsValue: Number(outcome?.expectancyDelta ?? Number.NEGATIVE_INFINITY) > 0 && Number(approvedRisk?.totalProfit ?? Number.NEGATIVE_INFINITY) > 0 && Number(approvedRisk?.profitFactor ?? Number.NEGATIVE_INFINITY) > 1 && directionalEvidenceComplete && directionPolicyValid && terminalsPass
|
|
1314
1379
|
};
|
|
1315
1380
|
}
|
|
1316
1381
|
if (reference.kind === "runtime_parity") {
|
package/dist/scripts/agentRun.js
CHANGED
|
@@ -40,6 +40,7 @@ var import_node_os = __toESM(require("os"));
|
|
|
40
40
|
var import_node_path = __toESM(require("path"));
|
|
41
41
|
var import_node_child_process = require("child_process");
|
|
42
42
|
var import_constants = require("@tradejs/core/constants");
|
|
43
|
+
var import_aiEndpoints = require("@tradejs/core/aiEndpoints");
|
|
43
44
|
var import_redis = require("@tradejs/infra/redis");
|
|
44
45
|
var import_userSettings = require("@tradejs/infra/userSettings");
|
|
45
46
|
|
|
@@ -415,35 +416,33 @@ var extractAssistantContent = (value) => {
|
|
|
415
416
|
};
|
|
416
417
|
var requestAgentDiff = async (userName, messages) => {
|
|
417
418
|
const settings = await (0, import_userSettings.getUserSettings)(userName);
|
|
418
|
-
|
|
419
|
+
const aiApiEndpoint = (0, import_aiEndpoints.normalizeAiEndpoint)(settings.AI_API_ENDPOINT);
|
|
420
|
+
if (!settings.AI_API_KEY || !aiApiEndpoint) {
|
|
419
421
|
throw new Error(`AI settings are incomplete for user ${userName}`);
|
|
420
422
|
}
|
|
421
|
-
if (!isOpenRouterEndpoint(
|
|
423
|
+
if (!isOpenRouterEndpoint(aiApiEndpoint)) {
|
|
422
424
|
throw new Error(
|
|
423
|
-
`Research agent requires OpenRouter endpoint, got: ${
|
|
425
|
+
`Research agent requires OpenRouter endpoint, got: ${aiApiEndpoint}`
|
|
424
426
|
);
|
|
425
427
|
}
|
|
426
|
-
const response = await fetch(
|
|
427
|
-
|
|
428
|
-
{
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
428
|
+
const response = await fetch(resolveChatCompletionsUrl(aiApiEndpoint), {
|
|
429
|
+
method: "POST",
|
|
430
|
+
headers: {
|
|
431
|
+
Authorization: `Bearer ${settings.AI_API_KEY}`,
|
|
432
|
+
"Content-Type": "application/json",
|
|
433
|
+
"HTTP-Referer": "https://tradejs.dev",
|
|
434
|
+
"X-Title": "TradeJS Research Agent"
|
|
435
|
+
},
|
|
436
|
+
body: JSON.stringify({
|
|
437
|
+
model: RESEARCH_AGENT_MODEL,
|
|
438
|
+
messages,
|
|
439
|
+
reasoning: {
|
|
440
|
+
effort: RESEARCH_AGENT_REASONING_EFFORT,
|
|
441
|
+
exclude: true
|
|
435
442
|
},
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
reasoning: {
|
|
440
|
-
effort: RESEARCH_AGENT_REASONING_EFFORT,
|
|
441
|
-
exclude: true
|
|
442
|
-
},
|
|
443
|
-
temperature: 0.2
|
|
444
|
-
})
|
|
445
|
-
}
|
|
446
|
-
);
|
|
443
|
+
temperature: 0.2
|
|
444
|
+
})
|
|
445
|
+
});
|
|
447
446
|
if (!response.ok) {
|
|
448
447
|
throw new Error(
|
|
449
448
|
`OpenRouter request failed (${response.status}): ${await response.text()}`
|
|
@@ -40,6 +40,7 @@ var import_path = __toESM(require("path"));
|
|
|
40
40
|
var import_progress = __toESM(require("progress"));
|
|
41
41
|
var import_ai = require("@tradejs/infra/ai");
|
|
42
42
|
var import_ai2 = require("@tradejs/node/ai");
|
|
43
|
+
var import_registry = require("@tradejs/node/registry");
|
|
43
44
|
|
|
44
45
|
// src/lib/aiTrainDataset.ts
|
|
45
46
|
var extractSignalFromAiDatasetRow = (row) => {
|
|
@@ -3482,7 +3483,7 @@ var main = async () => {
|
|
|
3482
3483
|
const explicitReportFile = String(flags.reportFile || "").trim();
|
|
3483
3484
|
const generatedAt = Date.now();
|
|
3484
3485
|
const excludedFeaturePaths = /* @__PURE__ */ new Map();
|
|
3485
|
-
await (0,
|
|
3486
|
+
await (0, import_registry.ensureStrategyPluginsLoaded)();
|
|
3486
3487
|
const filePaths = await resolveMergedDatasetFiles({
|
|
3487
3488
|
outDir,
|
|
3488
3489
|
strategyName,
|