@tradejs/cli 2.0.21 → 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 +242 -69
- package/dist/lib/aiTrainOptions.js +36 -0
- package/dist/lib/coreResearch.js +73 -8
- package/dist/lib/strategyRelease.js +71 -14
- 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/runtimeScorecard.js +35 -2
- package/dist/scripts/strategyRelease.js +71 -14
- 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(
|
|
@@ -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) {
|
|
@@ -115,7 +117,7 @@ var deriveStrategyReleaseResearchDecision = async (input) => {
|
|
|
115
117
|
maxLossValue: null,
|
|
116
118
|
requiresRuntimeBinding: false,
|
|
117
119
|
blockers,
|
|
118
|
-
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.`
|
|
119
121
|
};
|
|
120
122
|
}
|
|
121
123
|
const repairAllowed = Boolean(
|
|
@@ -242,6 +244,7 @@ var deriveReleaseGates = (evidence) => {
|
|
|
242
244
|
};
|
|
243
245
|
};
|
|
244
246
|
var verifyEvidenceComposition = (strategy, composition, evidence) => {
|
|
247
|
+
const directionPolicy = composition.directionPolicy ?? "both";
|
|
245
248
|
const requireEqual = (actual, expected, label, artifactId) => assert(
|
|
246
249
|
actual === expected,
|
|
247
250
|
`evidence ${artifactId} ${label} does not match the frozen composition`
|
|
@@ -312,6 +315,14 @@ var verifyEvidenceComposition = (strategy, composition, evidence) => {
|
|
|
312
315
|
"gateContextFingerprint",
|
|
313
316
|
reference.artifactId
|
|
314
317
|
);
|
|
318
|
+
if (directionPolicy !== "both" || lineage?.directionPolicy != null) {
|
|
319
|
+
requireEqual(
|
|
320
|
+
lineage?.directionPolicy,
|
|
321
|
+
directionPolicy,
|
|
322
|
+
"directionPolicy",
|
|
323
|
+
reference.artifactId
|
|
324
|
+
);
|
|
325
|
+
}
|
|
315
326
|
assert(
|
|
316
327
|
lineage?.sourceSha256s.includes(composition.coreExportSha256),
|
|
317
328
|
`evidence ${reference.artifactId} does not use the frozen core export`
|
|
@@ -356,6 +367,14 @@ var verifyEvidenceComposition = (strategy, composition, evidence) => {
|
|
|
356
367
|
"MAX_LOSS_VALUE",
|
|
357
368
|
reference.artifactId
|
|
358
369
|
);
|
|
370
|
+
if (directionPolicy !== "both" || lineage?.directionPolicy != null) {
|
|
371
|
+
requireEqual(
|
|
372
|
+
lineage?.directionPolicy,
|
|
373
|
+
directionPolicy,
|
|
374
|
+
"directionPolicy",
|
|
375
|
+
reference.artifactId
|
|
376
|
+
);
|
|
377
|
+
}
|
|
359
378
|
continue;
|
|
360
379
|
}
|
|
361
380
|
if (reference.kind === "execution_calibration") {
|
|
@@ -396,6 +415,14 @@ var verifyEvidenceComposition = (strategy, composition, evidence) => {
|
|
|
396
415
|
"MAX_LOSS_VALUE",
|
|
397
416
|
reference.artifactId
|
|
398
417
|
);
|
|
418
|
+
if (directionPolicy !== "both" || lineage?.directionPolicy != null) {
|
|
419
|
+
requireEqual(
|
|
420
|
+
lineage?.directionPolicy,
|
|
421
|
+
directionPolicy,
|
|
422
|
+
"directionPolicy",
|
|
423
|
+
reference.artifactId
|
|
424
|
+
);
|
|
425
|
+
}
|
|
399
426
|
}
|
|
400
427
|
}
|
|
401
428
|
};
|
|
@@ -431,6 +458,12 @@ var createStrategyReleaseManifest = (input) => {
|
|
|
431
458
|
input.composition.longEnabled && input.composition.shortEnabled,
|
|
432
459
|
"LONG and SHORT must both remain enabled"
|
|
433
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
|
+
);
|
|
434
467
|
assert(
|
|
435
468
|
Number.isInteger(input.monitoring.minimumProspectiveClosedTrades) && input.monitoring.minimumProspectiveClosedTrades > 0,
|
|
436
469
|
"monitoring.minimumProspectiveClosedTrades must be positive"
|
|
@@ -495,7 +528,8 @@ var createStrategyReleaseManifest = (input) => {
|
|
|
495
528
|
gateFingerprint: input.composition.gateFingerprint,
|
|
496
529
|
gateContextFingerprint: input.composition.gateContextFingerprint,
|
|
497
530
|
runtimeContextFingerprint: input.composition.runtimeContextFingerprint,
|
|
498
|
-
maxLossValue: input.composition.maxLossValue
|
|
531
|
+
maxLossValue: input.composition.maxLossValue,
|
|
532
|
+
...input.composition.directionPolicy == null ? {} : { directionPolicy: input.composition.directionPolicy }
|
|
499
533
|
};
|
|
500
534
|
const compositionId = `${safeSegment(input.strategy, "strategy")}_${strategyReleaseSha256(compositionIdentity).slice(0, 16)}`;
|
|
501
535
|
const reasons = releaseReasons({ ...input, gates: evidenceGates });
|
|
@@ -585,7 +619,8 @@ var verifyStrategyReleaseEnvelope = async (valueOrPath) => {
|
|
|
585
619
|
runtimeContextFingerprint: manifest.composition.runtimeContextFingerprint,
|
|
586
620
|
maxLossValue: manifest.composition.maxLossValue,
|
|
587
621
|
longEnabled: manifest.composition.longEnabled,
|
|
588
|
-
shortEnabled: manifest.composition.shortEnabled
|
|
622
|
+
shortEnabled: manifest.composition.shortEnabled,
|
|
623
|
+
directionPolicy: manifest.composition.directionPolicy
|
|
589
624
|
},
|
|
590
625
|
marketWindow: manifest.marketWindow,
|
|
591
626
|
researchBudget: manifest.researchBudget,
|
|
@@ -1009,6 +1044,7 @@ var finiteNumber = (value) => {
|
|
|
1009
1044
|
const number = Number(value);
|
|
1010
1045
|
return Number.isFinite(number) ? number : null;
|
|
1011
1046
|
};
|
|
1047
|
+
var finiteDirectionPolicy = (value) => ["both", "long_only", "short_only", "direction_aware"].includes(String(value)) ? value : null;
|
|
1012
1048
|
var uniqueStrings = (values) => [
|
|
1013
1049
|
...new Set(
|
|
1014
1050
|
values.map(finiteString).filter((value) => value != null)
|
|
@@ -1025,6 +1061,7 @@ var incompleteEvidenceLineage = () => ({
|
|
|
1025
1061
|
gateContextFingerprint: null,
|
|
1026
1062
|
runtimeContextFingerprint: null,
|
|
1027
1063
|
maxLossValue: null,
|
|
1064
|
+
directionPolicy: null,
|
|
1028
1065
|
sourceSha256s: []
|
|
1029
1066
|
});
|
|
1030
1067
|
var extractEvidenceLineage = ({
|
|
@@ -1087,6 +1124,7 @@ var extractEvidenceLineage = ({
|
|
|
1087
1124
|
gateConfigIdsFingerprint: finiteString(lineage?.configIdsFingerprint),
|
|
1088
1125
|
gateFingerprint: finiteString(lineage?.gateFingerprint),
|
|
1089
1126
|
gateContextFingerprint: finiteString(lineage?.contextFingerprint),
|
|
1127
|
+
directionPolicy: finiteDirectionPolicy(lineage?.directionPolicy),
|
|
1090
1128
|
sourceSha256s: uniqueStrings(array(lineage?.sourceSha256s))
|
|
1091
1129
|
};
|
|
1092
1130
|
}
|
|
@@ -1122,7 +1160,8 @@ var extractEvidenceLineage = ({
|
|
|
1122
1160
|
gateFingerprint: one("gateFingerprint"),
|
|
1123
1161
|
runtimeConfigFingerprint: one("configFingerprint"),
|
|
1124
1162
|
runtimeContextFingerprint: one("contextFingerprint"),
|
|
1125
|
-
maxLossValue: maxLossValues.length === 1 ? maxLossValues[0] : null
|
|
1163
|
+
maxLossValue: maxLossValues.length === 1 ? maxLossValues[0] : null,
|
|
1164
|
+
directionPolicy: finiteDirectionPolicy(one("directionPolicy"))
|
|
1126
1165
|
};
|
|
1127
1166
|
}
|
|
1128
1167
|
if (reference.kind === "execution_calibration") {
|
|
@@ -1159,6 +1198,7 @@ var extractEvidenceLineage = ({
|
|
|
1159
1198
|
runtimeConfigFingerprint: one("configFingerprint"),
|
|
1160
1199
|
runtimeContextFingerprint: one("contextFingerprint"),
|
|
1161
1200
|
maxLossValue: maxLossValues.length === 1 ? maxLossValues[0] : null,
|
|
1201
|
+
directionPolicy: finiteDirectionPolicy(one("directionPolicy")),
|
|
1162
1202
|
sourceSha256s: uniqueStrings([
|
|
1163
1203
|
...array(nested(artifact.sources)?.sha256s),
|
|
1164
1204
|
...array(artifact.sourceSha256s)
|
|
@@ -1295,9 +1335,10 @@ async function collectReleaseEvidenceReferences(references) {
|
|
|
1295
1335
|
if (reference.kind === "ai_gate") {
|
|
1296
1336
|
const outcome = nested(artifact.outcome);
|
|
1297
1337
|
const approvedRisk = nested(outcome?.approvedRisk);
|
|
1298
|
-
const
|
|
1299
|
-
|
|
1300
|
-
|
|
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);
|
|
1301
1342
|
const hasLongShortDirections = (value) => {
|
|
1302
1343
|
const directions = new Map(
|
|
1303
1344
|
array(value).map(nested).filter(
|
|
@@ -1311,6 +1352,22 @@ async function collectReleaseEvidenceReferences(references) {
|
|
|
1311
1352
|
const directionalEvidenceComplete = hasLongShortDirections(artifact.byDirection) && terminalWindows.every(
|
|
1312
1353
|
(window) => hasLongShortDirections(window.byDirection)
|
|
1313
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
|
+
));
|
|
1314
1371
|
const terminalsPass = terminalWindows.length > 0 && terminalWindows.every((window) => {
|
|
1315
1372
|
const terminalOutcome = nested(window.outcome);
|
|
1316
1373
|
return window.complete === true && Number(terminalOutcome?.approved ?? 0) > 0 && Number(
|
|
@@ -1318,7 +1375,7 @@ async function collectReleaseEvidenceReferences(references) {
|
|
|
1318
1375
|
) > 0;
|
|
1319
1376
|
});
|
|
1320
1377
|
return {
|
|
1321
|
-
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
|
|
1322
1379
|
};
|
|
1323
1380
|
}
|
|
1324
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,
|
package/dist/scripts/aiTrain.js
CHANGED
|
@@ -43,6 +43,7 @@ var import_ai = require("@tradejs/infra/ai");
|
|
|
43
43
|
var import_strategyReleaseEvidence = require("@tradejs/infra/strategyReleaseEvidence");
|
|
44
44
|
var import_redis = require("@tradejs/infra/redis");
|
|
45
45
|
var import_ai2 = require("@tradejs/node/ai");
|
|
46
|
+
var import_registry = require("@tradejs/node/registry");
|
|
46
47
|
var import_constants2 = require("@tradejs/node/constants");
|
|
47
48
|
|
|
48
49
|
// src/lib/aiTrainMetrics.ts
|
|
@@ -920,6 +921,38 @@ var parseTerminalWindowDays = (value) => {
|
|
|
920
921
|
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);
|
|
921
922
|
return [...new Set(raw)].sort((left, right) => right - left);
|
|
922
923
|
};
|
|
924
|
+
var parseAiTrainDirectionPolicy = (value) => {
|
|
925
|
+
const normalized = String(value ?? "both").trim().toLowerCase().replaceAll("-", "_");
|
|
926
|
+
if (normalized === "both" || normalized === "long_only" || normalized === "short_only" || normalized === "direction_aware") {
|
|
927
|
+
return normalized;
|
|
928
|
+
}
|
|
929
|
+
throw new Error(
|
|
930
|
+
`Invalid --directionPolicy value "${String(value)}". Use both, long_only, short_only, or direction_aware.`
|
|
931
|
+
);
|
|
932
|
+
};
|
|
933
|
+
var assertAiTrainDirectionPolicy = ({
|
|
934
|
+
directionPolicy,
|
|
935
|
+
byDirection,
|
|
936
|
+
terminalWindows
|
|
937
|
+
}) => {
|
|
938
|
+
const suppressedDirection = directionPolicy === "long_only" ? "SHORT" : directionPolicy === "short_only" ? "LONG" : null;
|
|
939
|
+
if (!suppressedDirection) return;
|
|
940
|
+
for (const scope of [{ label: "full", byDirection }, ...terminalWindows]) {
|
|
941
|
+
const summary = scope.byDirection.find(
|
|
942
|
+
(entry) => entry.direction === suppressedDirection
|
|
943
|
+
);
|
|
944
|
+
if (!summary) {
|
|
945
|
+
throw new Error(
|
|
946
|
+
`${scope.label} ${suppressedDirection} direction summary is required for ${directionPolicy}`
|
|
947
|
+
);
|
|
948
|
+
}
|
|
949
|
+
if (summary.summary.approved !== 0) {
|
|
950
|
+
throw new Error(
|
|
951
|
+
`${scope.label} has ${summary.summary.approved} ${suppressedDirection} approved rows under ${directionPolicy}`
|
|
952
|
+
);
|
|
953
|
+
}
|
|
954
|
+
}
|
|
955
|
+
};
|
|
923
956
|
var parseDumpFeatureMode = (value) => {
|
|
924
957
|
const text = String(value ?? "none").trim().toLowerCase();
|
|
925
958
|
if (!text || text === "none" || text === "false" || text === "0") {
|
|
@@ -1416,6 +1449,11 @@ import_args.default.option(
|
|
|
1416
1449
|
"Minimum AI quality required to approve entry",
|
|
1417
1450
|
4
|
|
1418
1451
|
);
|
|
1452
|
+
import_args.default.option(
|
|
1453
|
+
["Y", "directionPolicy"],
|
|
1454
|
+
"Evidence label for the enforced gate policy: both, long_only, short_only, or direction_aware",
|
|
1455
|
+
"both"
|
|
1456
|
+
);
|
|
1419
1457
|
import_args.default.option(
|
|
1420
1458
|
["l", "localOnly"],
|
|
1421
1459
|
"Replay deterministic adapter gate without AI provider calls",
|
|
@@ -1806,6 +1844,7 @@ var main = async () => {
|
|
|
1806
1844
|
});
|
|
1807
1845
|
const qualityThresholds = parseQualityThresholds(flags.qualityThresholds);
|
|
1808
1846
|
const terminalWindowDays = parseTerminalWindowDays(flags.terminalWindows);
|
|
1847
|
+
const directionPolicy = parseAiTrainDirectionPolicy(flags.directionPolicy);
|
|
1809
1848
|
const dumpEvaluationsPath = String(flags.dumpEvaluations || "").trim();
|
|
1810
1849
|
const dumpFeatureMode = parseDumpFeatureMode(flags.dumpFeatures);
|
|
1811
1850
|
if (dumpFeatureMode !== "none" && !dumpEvaluationsPath) {
|
|
@@ -1827,7 +1866,7 @@ var main = async () => {
|
|
|
1827
1866
|
const userName = String(flags.user || "root").trim() || "root";
|
|
1828
1867
|
const model = String(flags.model || import_ai2.DEFAULT_AI_MODEL).trim() || import_ai2.DEFAULT_AI_MODEL;
|
|
1829
1868
|
const parallel = normalizePositiveInt(flags.parallel, import_constants2.AI_CONCURRENCY_LIMIT);
|
|
1830
|
-
await (0,
|
|
1869
|
+
await (0, import_registry.ensureStrategyPluginsLoaded)();
|
|
1831
1870
|
const filePaths = await resolveDatasetFiles();
|
|
1832
1871
|
const datasetId = getMergedGroupId(filePaths[0] || "")?.mergeId;
|
|
1833
1872
|
const { totalRows, selectedRows } = await (0, import_ai.countAiDatasetRows)({
|
|
@@ -2020,18 +2059,29 @@ var main = async () => {
|
|
|
2020
2059
|
finalEvaluations,
|
|
2021
2060
|
terminalWindowDays
|
|
2022
2061
|
);
|
|
2023
|
-
|
|
2024
|
-
|
|
2025
|
-
|
|
2026
|
-
|
|
2027
|
-
configIds: finalEvaluations.map((evaluation) => evaluation.configId),
|
|
2028
|
-
sourceSha256s: await Promise.all(filePaths.map(import_strategyReleaseEvidence.strategyEvidenceFileSha256)),
|
|
2029
|
-
runContext: {
|
|
2030
|
-
mode: localOnly ? "local-deterministic" : "llm",
|
|
2031
|
-
model: localOnly ? "local-deterministic" : model,
|
|
2032
|
-
minQuality
|
|
2033
|
-
}
|
|
2062
|
+
assertAiTrainDirectionPolicy({
|
|
2063
|
+
directionPolicy,
|
|
2064
|
+
byDirection: directionSummaries,
|
|
2065
|
+
terminalWindows
|
|
2034
2066
|
});
|
|
2067
|
+
const topRejectReasons = summarizeAiTrainRejectReasons(finalEvaluations);
|
|
2068
|
+
const lineage = {
|
|
2069
|
+
...await buildAiTrainLineage({
|
|
2070
|
+
projectRoot: String(process.env.PROJECT_CWD || process.cwd()).trim() || process.cwd(),
|
|
2071
|
+
strategyName,
|
|
2072
|
+
configIds: finalEvaluations.map((evaluation) => evaluation.configId),
|
|
2073
|
+
sourceSha256s: await Promise.all(
|
|
2074
|
+
filePaths.map(import_strategyReleaseEvidence.strategyEvidenceFileSha256)
|
|
2075
|
+
),
|
|
2076
|
+
runContext: {
|
|
2077
|
+
mode: localOnly ? "local-deterministic" : "llm",
|
|
2078
|
+
model: localOnly ? "local-deterministic" : model,
|
|
2079
|
+
minQuality,
|
|
2080
|
+
directionPolicy
|
|
2081
|
+
}
|
|
2082
|
+
}),
|
|
2083
|
+
directionPolicy
|
|
2084
|
+
};
|
|
2035
2085
|
const evaluated = summary.correct + summary.incorrect;
|
|
2036
2086
|
const result = {
|
|
2037
2087
|
run: {
|
|
@@ -2053,7 +2103,8 @@ var main = async () => {
|
|
|
2053
2103
|
model: localOnly ? "local-deterministic" : model,
|
|
2054
2104
|
parallel: concurrency,
|
|
2055
2105
|
dumpFeatures: dumpFeatureMode,
|
|
2056
|
-
terminalWindows: terminalWindowDays
|
|
2106
|
+
terminalWindows: terminalWindowDays,
|
|
2107
|
+
directionPolicy
|
|
2057
2108
|
},
|
|
2058
2109
|
outcome: summary,
|
|
2059
2110
|
byDirection: directionSummaries,
|