create-tradejs 3.1.27 → 3.1.28-beta.252
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/skill-bundle/.codex/skills/ai-train-local-research/references/gate-ablation.md +55 -0
- package/dist/skill-bundle/.codex/skills/ai-train-local-research/scripts/ai-gate-ablation.mjs +276 -51
- package/dist/skill-bundle/.codex/skills/ai-train-local-research/scripts/ai-gate-ablation.test.mjs +189 -6
- package/dist/skill-bundle/.codex/skills/strategy-improvement-research/scripts/final-composition-board.mjs +10 -9
- package/dist/skill-bundle/.codex/skills/strategy-improvement-research/scripts/final-composition-board.test.mjs +16 -2
- package/dist/skill-bundle/.codex/tradejs-skill-bundle.json +6 -6
- package/package.json +1 -1
|
@@ -120,6 +120,55 @@ JSON reports include timestamp-grouped cumulative `equity` arrays for the
|
|
|
120
120
|
current-gate baseline and every variant. Use these checksum-bound arrays for
|
|
121
121
|
final-composition charts instead of reconstructing curves by hand.
|
|
122
122
|
|
|
123
|
+
For a deterministic timestamp-local portfolio limit, a JSON spec variant may
|
|
124
|
+
also define `selection`. The tool first evaluates the gate, then keeps the
|
|
125
|
+
highest-ranked rows independently inside each decision timestamp. Missing rank
|
|
126
|
+
values sort last; symbol and source sequence are stable tie-breakers.
|
|
127
|
+
|
|
128
|
+
```json
|
|
129
|
+
{
|
|
130
|
+
"name": "capacity-five",
|
|
131
|
+
"mode": "replace",
|
|
132
|
+
"quality": 4,
|
|
133
|
+
"expression": "derived.direction == SHORT && feature.margin >= 0",
|
|
134
|
+
"selection": {
|
|
135
|
+
"capacity": 5,
|
|
136
|
+
"rankBy": [
|
|
137
|
+
{ "path": "feature.margin", "order": "desc" },
|
|
138
|
+
{
|
|
139
|
+
"path": "additionalIndicators.volumeDivergenceSetup.reclaimPct",
|
|
140
|
+
"order": "desc"
|
|
141
|
+
}
|
|
142
|
+
]
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
Treat every rank path like an approval feature: it must be causal, available at
|
|
148
|
+
decision time, stationary enough for the intended use, and documented with its
|
|
149
|
+
scope and environment dependencies. Capacity ranking is not permission to use
|
|
150
|
+
outcome, current-gate output, or data-availability fields.
|
|
151
|
+
|
|
152
|
+
A JSON spec can also define an event-count-preserving timestamp-rotation
|
|
153
|
+
placebo. Its own expression defines eligible rows. Inside each train, tuning,
|
|
154
|
+
and test partition, the referenced variant's approved event timestamps are
|
|
155
|
+
shifted through the eligible timestamp sequence. Trade outcomes are never read
|
|
156
|
+
while constructing the shift.
|
|
157
|
+
|
|
158
|
+
```json
|
|
159
|
+
{
|
|
160
|
+
"name": "rotated-placebo",
|
|
161
|
+
"mode": "replace",
|
|
162
|
+
"quality": 4,
|
|
163
|
+
"expression": "derived.direction == SHORT",
|
|
164
|
+
"placebo": {
|
|
165
|
+
"type": "timestamp-rotation",
|
|
166
|
+
"referenceVariant": "frozen-gate",
|
|
167
|
+
"offsetEvents": 37
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
```
|
|
171
|
+
|
|
123
172
|
## Expression Grammar
|
|
124
173
|
|
|
125
174
|
Expressions support parentheses, `&&`, `||`, and comparisons:
|
|
@@ -160,6 +209,12 @@ When several core candidates must be compared, pass the same exact UTC
|
|
|
160
209
|
Exact boundaries take precedence over ratio splits and keep sparse candidates
|
|
161
210
|
on one calendar partition contract.
|
|
162
211
|
|
|
212
|
+
Use `--windowStart <UTC> --windowEnd <UTC>` to compare candidates over the same
|
|
213
|
+
calendar window. The start is inclusive, and the end is exclusive. Full-period
|
|
214
|
+
cadence and terminal windows use these bounds instead of each export's first
|
|
215
|
+
and last trade. The report includes zero-trade terminal windows. Without these
|
|
216
|
+
options, the existing export-based window remains unchanged.
|
|
217
|
+
|
|
163
218
|
## Cross-Strategy Feasibility
|
|
164
219
|
|
|
165
220
|
Use `--crossStrategy` to test whether the latest merged export for every
|
package/dist/skill-bundle/.codex/skills/ai-train-local-research/scripts/ai-gate-ablation.mjs
CHANGED
|
@@ -36,6 +36,8 @@ Options:
|
|
|
36
36
|
--testSplit <ratio> Later timestamp-grouped test share (default: 0)
|
|
37
37
|
--tuningSince <timestamp> Exact UTC boundary where tuning starts
|
|
38
38
|
--testSince <timestamp> Exact UTC boundary where test starts
|
|
39
|
+
--windowStart <timestamp> Common comparison start, inclusive
|
|
40
|
+
--windowEnd <timestamp> Common comparison end, exclusive
|
|
39
41
|
--capacities <list> Capacity stress limits (default: 1,3,5)
|
|
40
42
|
--maxLossValue <n> Per-order loss budget for capacity stress
|
|
41
43
|
--featurePattern <regex> Inventory matching causal feature paths
|
|
@@ -165,7 +167,9 @@ export const parseCliArgs = (argv) => {
|
|
|
165
167
|
options.testSplit = Number.isFinite(parsed)
|
|
166
168
|
? Math.max(0, Math.min(0.9, parsed))
|
|
167
169
|
: 0;
|
|
168
|
-
} else if (
|
|
170
|
+
} else if (
|
|
171
|
+
['tuningSince', 'testSince', 'windowStart', 'windowEnd'].includes(name)
|
|
172
|
+
) {
|
|
169
173
|
const numeric = Number(value);
|
|
170
174
|
const parsed = Number.isFinite(numeric) ? numeric : Date.parse(value);
|
|
171
175
|
if (!Number.isFinite(parsed)) {
|
|
@@ -668,11 +672,55 @@ const loadVariants = async (inlineVariants, specPath) => {
|
|
|
668
672
|
entry.quality == null ? '' : `@${Math.trunc(Number(entry.quality))}`;
|
|
669
673
|
const directionSuffix =
|
|
670
674
|
entry.direction == null ? '' : `[${String(entry.direction)}]`;
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
`${entry.name}::${entry.mode}${qualitySuffix}${directionSuffix}::${entry.expression}`,
|
|
674
|
-
),
|
|
675
|
+
const variant = parseVariant(
|
|
676
|
+
`${entry.name}::${entry.mode}${qualitySuffix}${directionSuffix}::${entry.expression}`,
|
|
675
677
|
);
|
|
678
|
+
if (entry.selection != null) {
|
|
679
|
+
const capacity = Math.trunc(Number(entry.selection.capacity));
|
|
680
|
+
const rankBy = entry.selection.rankBy;
|
|
681
|
+
if (!Number.isFinite(capacity) || capacity < 1) {
|
|
682
|
+
throw new Error(
|
|
683
|
+
`Variant ${entry.name} selection.capacity must be a positive integer`,
|
|
684
|
+
);
|
|
685
|
+
}
|
|
686
|
+
if (!Array.isArray(rankBy) || rankBy.length === 0) {
|
|
687
|
+
throw new Error(
|
|
688
|
+
`Variant ${entry.name} selection.rankBy must be a non-empty array`,
|
|
689
|
+
);
|
|
690
|
+
}
|
|
691
|
+
variant.selection = {
|
|
692
|
+
capacity,
|
|
693
|
+
rankBy: rankBy.map((rank, index) => {
|
|
694
|
+
const pathValue = String(rank?.path ?? '').trim();
|
|
695
|
+
const order = String(rank?.order ?? 'desc').toLowerCase();
|
|
696
|
+
if (!pathValue || !['asc', 'desc'].includes(order)) {
|
|
697
|
+
throw new Error(
|
|
698
|
+
`Variant ${entry.name} selection.rankBy[${index}] requires path and asc|desc order`,
|
|
699
|
+
);
|
|
700
|
+
}
|
|
701
|
+
return { path: pathValue, order };
|
|
702
|
+
}),
|
|
703
|
+
};
|
|
704
|
+
}
|
|
705
|
+
if (entry.placebo != null) {
|
|
706
|
+
const type = String(entry.placebo.type ?? '').trim();
|
|
707
|
+
const referenceVariant = String(
|
|
708
|
+
entry.placebo.referenceVariant ?? '',
|
|
709
|
+
).trim();
|
|
710
|
+
const offsetEvents = Math.trunc(Number(entry.placebo.offsetEvents));
|
|
711
|
+
if (
|
|
712
|
+
type !== 'timestamp-rotation' ||
|
|
713
|
+
!referenceVariant ||
|
|
714
|
+
!Number.isFinite(offsetEvents) ||
|
|
715
|
+
offsetEvents < 1
|
|
716
|
+
) {
|
|
717
|
+
throw new Error(
|
|
718
|
+
`Variant ${entry.name} placebo requires type=timestamp-rotation, referenceVariant, and positive offsetEvents`,
|
|
719
|
+
);
|
|
720
|
+
}
|
|
721
|
+
variant.placebo = { type, referenceVariant, offsetEvents };
|
|
722
|
+
}
|
|
723
|
+
variants.push(variant);
|
|
676
724
|
}
|
|
677
725
|
const names = new Set();
|
|
678
726
|
for (const variant of variants) {
|
|
@@ -681,6 +729,16 @@ const loadVariants = async (inlineVariants, specPath) => {
|
|
|
681
729
|
}
|
|
682
730
|
names.add(variant.name);
|
|
683
731
|
}
|
|
732
|
+
for (const variant of variants) {
|
|
733
|
+
if (
|
|
734
|
+
variant.placebo != null &&
|
|
735
|
+
!names.has(variant.placebo.referenceVariant)
|
|
736
|
+
) {
|
|
737
|
+
throw new Error(
|
|
738
|
+
`Variant ${variant.name} references unknown placebo variant ${variant.placebo.referenceVariant}`,
|
|
739
|
+
);
|
|
740
|
+
}
|
|
741
|
+
}
|
|
684
742
|
return variants;
|
|
685
743
|
};
|
|
686
744
|
|
|
@@ -962,6 +1020,111 @@ const candidateSelectedAt = (
|
|
|
962
1020
|
defaultQuality: minQuality,
|
|
963
1021
|
});
|
|
964
1022
|
|
|
1023
|
+
const compareRankValue = (left, right, order) => {
|
|
1024
|
+
const leftMissing = left == null || Number.isNaN(left);
|
|
1025
|
+
const rightMissing = right == null || Number.isNaN(right);
|
|
1026
|
+
if (leftMissing || rightMissing) {
|
|
1027
|
+
if (leftMissing && rightMissing) return 0;
|
|
1028
|
+
return leftMissing ? 1 : -1;
|
|
1029
|
+
}
|
|
1030
|
+
let comparison;
|
|
1031
|
+
if (typeof left === 'number' && typeof right === 'number') {
|
|
1032
|
+
comparison = left - right;
|
|
1033
|
+
} else {
|
|
1034
|
+
comparison = String(left).localeCompare(String(right));
|
|
1035
|
+
}
|
|
1036
|
+
return order === 'asc' ? comparison : -comparison;
|
|
1037
|
+
};
|
|
1038
|
+
|
|
1039
|
+
export const selectRowsWithinCapacity = ({ rows, selector, selection }) => {
|
|
1040
|
+
const selected = rows.filter(selector);
|
|
1041
|
+
if (selection == null) return new Set(selected);
|
|
1042
|
+
const byTimestamp = new Map();
|
|
1043
|
+
for (const row of selected) {
|
|
1044
|
+
const eventRows = byTimestamp.get(row.timestamp) ?? [];
|
|
1045
|
+
eventRows.push(row);
|
|
1046
|
+
byTimestamp.set(row.timestamp, eventRows);
|
|
1047
|
+
}
|
|
1048
|
+
const accepted = new Set();
|
|
1049
|
+
for (const eventRows of byTimestamp.values()) {
|
|
1050
|
+
eventRows.sort((left, right) => {
|
|
1051
|
+
for (const rank of selection.rankBy) {
|
|
1052
|
+
const comparison = compareRankValue(
|
|
1053
|
+
left.features?.[rank.path],
|
|
1054
|
+
right.features?.[rank.path],
|
|
1055
|
+
rank.order,
|
|
1056
|
+
);
|
|
1057
|
+
if (comparison !== 0) return comparison;
|
|
1058
|
+
}
|
|
1059
|
+
return (
|
|
1060
|
+
String(left.symbol ?? '').localeCompare(String(right.symbol ?? '')) ||
|
|
1061
|
+
Number(left.sequence ?? 0) - Number(right.sequence ?? 0)
|
|
1062
|
+
);
|
|
1063
|
+
});
|
|
1064
|
+
for (const row of eventRows.slice(0, selection.capacity)) {
|
|
1065
|
+
accepted.add(row);
|
|
1066
|
+
}
|
|
1067
|
+
}
|
|
1068
|
+
return accepted;
|
|
1069
|
+
};
|
|
1070
|
+
|
|
1071
|
+
export const applyTimestampRotationPlacebos = (
|
|
1072
|
+
rows,
|
|
1073
|
+
variants,
|
|
1074
|
+
partitions = [rows],
|
|
1075
|
+
) => {
|
|
1076
|
+
const variantIndexes = new Map(
|
|
1077
|
+
variants.map((variant, index) => [variant.name, index]),
|
|
1078
|
+
);
|
|
1079
|
+
for (
|
|
1080
|
+
let placeboIndex = 0;
|
|
1081
|
+
placeboIndex < variants.length;
|
|
1082
|
+
placeboIndex += 1
|
|
1083
|
+
) {
|
|
1084
|
+
const placebo = variants[placeboIndex].placebo;
|
|
1085
|
+
if (placebo?.type !== 'timestamp-rotation') continue;
|
|
1086
|
+
const referenceIndex = variantIndexes.get(placebo.referenceVariant);
|
|
1087
|
+
if (referenceIndex == null) {
|
|
1088
|
+
throw new Error(
|
|
1089
|
+
`Unknown timestamp-rotation reference ${placebo.referenceVariant}`,
|
|
1090
|
+
);
|
|
1091
|
+
}
|
|
1092
|
+
for (const partitionRows of partitions) {
|
|
1093
|
+
const eligibleTimestamps = [
|
|
1094
|
+
...new Set(
|
|
1095
|
+
partitionRows
|
|
1096
|
+
.filter((row) => row.variantMatches[placeboIndex])
|
|
1097
|
+
.map((row) => row.timestamp),
|
|
1098
|
+
),
|
|
1099
|
+
].sort((left, right) => left - right);
|
|
1100
|
+
if (eligibleTimestamps.length === 0) continue;
|
|
1101
|
+
const eligibleIndex = new Map(
|
|
1102
|
+
eligibleTimestamps.map((timestamp, index) => [timestamp, index]),
|
|
1103
|
+
);
|
|
1104
|
+
const referenceTimestamps = new Set(
|
|
1105
|
+
partitionRows
|
|
1106
|
+
.filter((row) => row.variantMatches[referenceIndex])
|
|
1107
|
+
.map((row) => row.timestamp),
|
|
1108
|
+
);
|
|
1109
|
+
const rotatedTimestamps = new Set();
|
|
1110
|
+
for (const timestamp of referenceTimestamps) {
|
|
1111
|
+
const index = eligibleIndex.get(timestamp);
|
|
1112
|
+
if (index == null) continue;
|
|
1113
|
+
rotatedTimestamps.add(
|
|
1114
|
+
eligibleTimestamps[
|
|
1115
|
+
(index + placebo.offsetEvents) % eligibleTimestamps.length
|
|
1116
|
+
],
|
|
1117
|
+
);
|
|
1118
|
+
}
|
|
1119
|
+
for (const row of partitionRows) {
|
|
1120
|
+
row.variantMatches[placeboIndex] =
|
|
1121
|
+
row.variantMatches[placeboIndex] &&
|
|
1122
|
+
rotatedTimestamps.has(row.timestamp);
|
|
1123
|
+
}
|
|
1124
|
+
}
|
|
1125
|
+
}
|
|
1126
|
+
};
|
|
1127
|
+
|
|
965
1128
|
const selectRows = (rows, predicate) => rows.filter(predicate);
|
|
966
1129
|
|
|
967
1130
|
const buildPeriodSummaries = ({
|
|
@@ -972,15 +1135,17 @@ const buildPeriodSummaries = ({
|
|
|
972
1135
|
maxTimestamp,
|
|
973
1136
|
summaryOptions,
|
|
974
1137
|
}) => {
|
|
975
|
-
const withCalendarDays = (periodRows) => ({
|
|
1138
|
+
const withCalendarDays = (periodRows, start, end) => ({
|
|
976
1139
|
...summaryOptions,
|
|
977
|
-
calendarDays:
|
|
1140
|
+
calendarDays: summaryOptions.comparisonWindow
|
|
1141
|
+
? getCalendarDays([{ timestamp: start }, { timestamp: end - 1 }])
|
|
1142
|
+
: getCalendarDays(periodRows),
|
|
978
1143
|
});
|
|
979
1144
|
const result = {
|
|
980
1145
|
full: summarizeRows(
|
|
981
1146
|
selectRows(rows, selector),
|
|
982
1147
|
Math.max((maxTimestamp - minTimestamp) / DAY_MS, 1),
|
|
983
|
-
withCalendarDays(rows),
|
|
1148
|
+
withCalendarDays(rows, minTimestamp, maxTimestamp),
|
|
984
1149
|
),
|
|
985
1150
|
};
|
|
986
1151
|
for (const days of windows) {
|
|
@@ -989,7 +1154,7 @@ const buildPeriodSummaries = ({
|
|
|
989
1154
|
result[`${days}d`] = summarizeRows(
|
|
990
1155
|
selectRows(periodRows, selector),
|
|
991
1156
|
days,
|
|
992
|
-
withCalendarDays(periodRows),
|
|
1157
|
+
withCalendarDays(periodRows, from, maxTimestamp),
|
|
993
1158
|
);
|
|
994
1159
|
}
|
|
995
1160
|
return result;
|
|
@@ -1027,11 +1192,7 @@ export const splitRowsByTimestamp = (rows, validationSplit, testSplit = 0) => {
|
|
|
1027
1192
|
};
|
|
1028
1193
|
};
|
|
1029
1194
|
|
|
1030
|
-
export const splitRowsByTimestampBounds = (
|
|
1031
|
-
rows,
|
|
1032
|
-
tuningSince,
|
|
1033
|
-
testSince,
|
|
1034
|
-
) => {
|
|
1195
|
+
export const splitRowsByTimestampBounds = (rows, tuningSince, testSince) => {
|
|
1035
1196
|
if (!Number.isFinite(tuningSince) || !Number.isFinite(testSince)) {
|
|
1036
1197
|
throw new Error(
|
|
1037
1198
|
'Exact calendar partitions require both tuningSince and testSince',
|
|
@@ -1061,7 +1222,7 @@ const summarizeDirections = (rows, selector, summaryOptions) =>
|
|
|
1061
1222
|
direction,
|
|
1062
1223
|
summarizeRows(
|
|
1063
1224
|
selectRows(rows, (row) => row.direction === direction && selector(row)),
|
|
1064
|
-
getPeriodDays(rows),
|
|
1225
|
+
summaryOptions?.denominatorDays ?? getPeriodDays(rows),
|
|
1065
1226
|
summaryOptions,
|
|
1066
1227
|
),
|
|
1067
1228
|
]),
|
|
@@ -1091,7 +1252,8 @@ export const buildEquitySeries = (
|
|
|
1091
1252
|
if (timestamp === minTimestamp) result[0] = [timestamp, cumulative];
|
|
1092
1253
|
else result.push([timestamp, cumulative]);
|
|
1093
1254
|
}
|
|
1094
|
-
if (result.at(-1)[0] !== maxTimestamp)
|
|
1255
|
+
if (result.at(-1)[0] !== maxTimestamp)
|
|
1256
|
+
result.push([maxTimestamp, cumulative]);
|
|
1095
1257
|
return result;
|
|
1096
1258
|
};
|
|
1097
1259
|
|
|
@@ -1105,7 +1267,12 @@ const buildPeriodDirectionSummaries = ({
|
|
|
1105
1267
|
const result = {
|
|
1106
1268
|
full: summarizeDirections(rows, selector, {
|
|
1107
1269
|
...summaryOptions,
|
|
1108
|
-
calendarDays:
|
|
1270
|
+
calendarDays: summaryOptions.comparisonWindow
|
|
1271
|
+
? getCalendarDays([
|
|
1272
|
+
{ timestamp: summaryOptions.comparisonWindow.start },
|
|
1273
|
+
{ timestamp: maxTimestamp - 1 },
|
|
1274
|
+
])
|
|
1275
|
+
: getCalendarDays(rows),
|
|
1109
1276
|
}),
|
|
1110
1277
|
};
|
|
1111
1278
|
for (const days of windows) {
|
|
@@ -1113,7 +1280,13 @@ const buildPeriodDirectionSummaries = ({
|
|
|
1113
1280
|
const periodRows = selectRows(rows, (row) => row.timestamp >= from);
|
|
1114
1281
|
result[`${days}d`] = summarizeDirections(periodRows, selector, {
|
|
1115
1282
|
...summaryOptions,
|
|
1116
|
-
|
|
1283
|
+
...(summaryOptions.comparisonWindow ? { denominatorDays: days } : {}),
|
|
1284
|
+
calendarDays: summaryOptions.comparisonWindow
|
|
1285
|
+
? getCalendarDays([
|
|
1286
|
+
{ timestamp: from },
|
|
1287
|
+
{ timestamp: maxTimestamp - 1 },
|
|
1288
|
+
])
|
|
1289
|
+
: getCalendarDays(periodRows),
|
|
1117
1290
|
});
|
|
1118
1291
|
}
|
|
1119
1292
|
return result;
|
|
@@ -1254,9 +1427,8 @@ export const loadStandaloneStrategyEntries = async (sourceRepositoryRoot) => {
|
|
|
1254
1427
|
`Expected a standalone TradeJS strategy repository: ${sourceRepositoryRoot}`,
|
|
1255
1428
|
);
|
|
1256
1429
|
}
|
|
1257
|
-
const { entrypoint, packageJson } =
|
|
1258
|
-
sourceRepositoryRoot
|
|
1259
|
-
);
|
|
1430
|
+
const { entrypoint, packageJson } =
|
|
1431
|
+
resolveStandaloneStrategyEntrypoint(sourceRepositoryRoot);
|
|
1260
1432
|
let outputStat;
|
|
1261
1433
|
try {
|
|
1262
1434
|
outputStat = await fsp.stat(entrypoint);
|
|
@@ -1317,9 +1489,15 @@ export const ensureRuntimeBuild = async (frameworkRepositoryRoot) => {
|
|
|
1317
1489
|
output: aiModulePath,
|
|
1318
1490
|
sources: [
|
|
1319
1491
|
path.join(frameworkRepositoryRoot, 'packages/node/src/ai.ts'),
|
|
1320
|
-
path.join(
|
|
1492
|
+
path.join(
|
|
1493
|
+
frameworkRepositoryRoot,
|
|
1494
|
+
'packages/node/src/aiMarketContext.ts',
|
|
1495
|
+
),
|
|
1321
1496
|
path.join(frameworkRepositoryRoot, 'packages/node/src/aiShared.ts'),
|
|
1322
|
-
path.join(
|
|
1497
|
+
path.join(
|
|
1498
|
+
frameworkRepositoryRoot,
|
|
1499
|
+
'packages/node/src/strategyAdapters',
|
|
1500
|
+
),
|
|
1323
1501
|
],
|
|
1324
1502
|
command: 'yarn workspace @tradejs/node build',
|
|
1325
1503
|
},
|
|
@@ -1372,9 +1550,8 @@ const loadResearchRows = async ({
|
|
|
1372
1550
|
const require = createRequire(import.meta.url);
|
|
1373
1551
|
const { collectAiPocketFeatures } = require(pocketModulePath);
|
|
1374
1552
|
if (getSourceRepositoryKind(sourceRepositoryRoot) === 'strategy') {
|
|
1375
|
-
const { strategyEntries } =
|
|
1376
|
-
sourceRepositoryRoot
|
|
1377
|
-
);
|
|
1553
|
+
const { strategyEntries } =
|
|
1554
|
+
await loadStandaloneStrategyEntries(sourceRepositoryRoot);
|
|
1378
1555
|
registryModule.resetStrategyRegistryCache(projectRoot);
|
|
1379
1556
|
registryModule.registerStrategyEntries(strategyEntries, projectRoot);
|
|
1380
1557
|
} else {
|
|
@@ -1430,6 +1607,7 @@ const loadResearchRows = async ({
|
|
|
1430
1607
|
analysis.direction === source.direction &&
|
|
1431
1608
|
quality != null &&
|
|
1432
1609
|
quality >= minQuality,
|
|
1610
|
+
features,
|
|
1433
1611
|
movingAverageSource: {
|
|
1434
1612
|
provider: String(source.connectorName ?? '')
|
|
1435
1613
|
.trim()
|
|
@@ -3841,6 +4019,8 @@ export const buildAblationReport = ({
|
|
|
3841
4019
|
testSplit = 0,
|
|
3842
4020
|
tuningSince = null,
|
|
3843
4021
|
testSince = null,
|
|
4022
|
+
windowStart = null,
|
|
4023
|
+
windowEnd = null,
|
|
3844
4024
|
capacities = DEFAULT_CAPACITIES,
|
|
3845
4025
|
maxLossValue = null,
|
|
3846
4026
|
filePaths,
|
|
@@ -3851,8 +4031,29 @@ export const buildAblationReport = ({
|
|
|
3851
4031
|
featureInventory = [],
|
|
3852
4032
|
}) => {
|
|
3853
4033
|
if (!rows.length) throw new Error('No rows were evaluated');
|
|
3854
|
-
|
|
3855
|
-
|
|
4034
|
+
if ((windowStart == null) !== (windowEnd == null)) {
|
|
4035
|
+
throw new Error(
|
|
4036
|
+
'Common comparison window requires both windowStart and windowEnd',
|
|
4037
|
+
);
|
|
4038
|
+
}
|
|
4039
|
+
const explicitWindow = windowStart != null;
|
|
4040
|
+
if (
|
|
4041
|
+
explicitWindow &&
|
|
4042
|
+
(!Number.isFinite(windowStart) ||
|
|
4043
|
+
!Number.isFinite(windowEnd) ||
|
|
4044
|
+
windowStart >= windowEnd)
|
|
4045
|
+
) {
|
|
4046
|
+
throw new Error(
|
|
4047
|
+
'Common comparison window must have finite increasing bounds',
|
|
4048
|
+
);
|
|
4049
|
+
}
|
|
4050
|
+
if (explicitWindow) {
|
|
4051
|
+
rows = rows.filter(
|
|
4052
|
+
(row) => row.timestamp >= windowStart && row.timestamp < windowEnd,
|
|
4053
|
+
);
|
|
4054
|
+
}
|
|
4055
|
+
const minTimestamp = windowStart ?? rows[0].timestamp;
|
|
4056
|
+
const maxTimestamp = windowEnd ?? rows.at(-1).timestamp;
|
|
3856
4057
|
if ((tuningSince == null) !== (testSince == null)) {
|
|
3857
4058
|
throw new Error(
|
|
3858
4059
|
'Exact calendar partitions require both tuningSince and testSince',
|
|
@@ -3862,6 +4063,11 @@ export const buildAblationReport = ({
|
|
|
3862
4063
|
const split = exactCalendarPartitions
|
|
3863
4064
|
? splitRowsByTimestampBounds(rows, tuningSince, testSince)
|
|
3864
4065
|
: splitRowsByTimestamp(rows, validationSplit, testSplit);
|
|
4066
|
+
applyTimestampRotationPlacebos(rows, variants, [
|
|
4067
|
+
split.train,
|
|
4068
|
+
split.tuning,
|
|
4069
|
+
split.test,
|
|
4070
|
+
]);
|
|
3865
4071
|
const partitionEvidence = (partitionRows) => ({
|
|
3866
4072
|
rows: partitionRows.length,
|
|
3867
4073
|
events: new Set(partitionRows.map((row) => row.timestamp)).size,
|
|
@@ -3872,14 +4078,23 @@ export const buildAblationReport = ({
|
|
|
3872
4078
|
? new Date(partitionRows.at(-1).timestamp).toISOString()
|
|
3873
4079
|
: null,
|
|
3874
4080
|
});
|
|
3875
|
-
const summaryOptions = {
|
|
4081
|
+
const summaryOptions = {
|
|
4082
|
+
capacities,
|
|
4083
|
+
maxLossValue,
|
|
4084
|
+
...(explicitWindow
|
|
4085
|
+
? {
|
|
4086
|
+
comparisonWindow: { start: windowStart, end: windowEnd },
|
|
4087
|
+
denominatorDays: Math.max((windowEnd - windowStart) / DAY_MS, 1),
|
|
4088
|
+
}
|
|
4089
|
+
: {}),
|
|
4090
|
+
};
|
|
3876
4091
|
const baselineSelector = (row) => baselineSelectedAt(row, minQuality);
|
|
3877
4092
|
const baseline = {
|
|
3878
4093
|
equity: buildEquitySeries(
|
|
3879
4094
|
rows,
|
|
3880
4095
|
baselineSelector,
|
|
3881
4096
|
minTimestamp,
|
|
3882
|
-
maxTimestamp,
|
|
4097
|
+
maxTimestamp - Number(explicitWindow),
|
|
3883
4098
|
),
|
|
3884
4099
|
periods: buildPeriodSummaries({
|
|
3885
4100
|
rows,
|
|
@@ -3913,8 +4128,17 @@ export const buildAblationReport = ({
|
|
|
3913
4128
|
months: summarizeMonths(rows, baselineSelector, summaryOptions),
|
|
3914
4129
|
};
|
|
3915
4130
|
const variantReports = variants.map((variant, variantIndex) => {
|
|
3916
|
-
const
|
|
3917
|
-
|
|
4131
|
+
const candidateSelectorFor = (threshold) => {
|
|
4132
|
+
const baseSelector = (row) =>
|
|
4133
|
+
candidateSelectedAt(row, variant, variantIndex, threshold, minQuality);
|
|
4134
|
+
const selectedRows = selectRowsWithinCapacity({
|
|
4135
|
+
rows,
|
|
4136
|
+
selector: baseSelector,
|
|
4137
|
+
selection: variant.selection,
|
|
4138
|
+
});
|
|
4139
|
+
return (row) => selectedRows.has(row);
|
|
4140
|
+
};
|
|
4141
|
+
const candidateSelector = candidateSelectorFor(minQuality);
|
|
3918
4142
|
const matchedSelector = (row) => row.variantMatches[variantIndex];
|
|
3919
4143
|
const removedSelector = (row) =>
|
|
3920
4144
|
baselineSelector(row) && !candidateSelector(row);
|
|
@@ -3926,11 +4150,13 @@ export const buildAblationReport = ({
|
|
|
3926
4150
|
quality: variant.quality,
|
|
3927
4151
|
direction: variant.direction,
|
|
3928
4152
|
expression: variant.expression,
|
|
4153
|
+
selection: variant.selection ?? null,
|
|
4154
|
+
placebo: variant.placebo ?? null,
|
|
3929
4155
|
equity: buildEquitySeries(
|
|
3930
4156
|
rows,
|
|
3931
4157
|
candidateSelector,
|
|
3932
4158
|
minTimestamp,
|
|
3933
|
-
maxTimestamp,
|
|
4159
|
+
maxTimestamp - Number(explicitWindow),
|
|
3934
4160
|
),
|
|
3935
4161
|
periods: buildPeriodSummaries({
|
|
3936
4162
|
rows,
|
|
@@ -3951,22 +4177,17 @@ export const buildAblationReport = ({
|
|
|
3951
4177
|
tuning: summarizeSplit(split.tuning, candidateSelector, summaryOptions),
|
|
3952
4178
|
test: summarizeSplit(split.test, candidateSelector, summaryOptions),
|
|
3953
4179
|
qualityThresholds: Object.fromEntries(
|
|
3954
|
-
qualityThresholds.map((threshold) =>
|
|
3955
|
-
|
|
3956
|
-
|
|
3957
|
-
|
|
3958
|
-
|
|
3959
|
-
|
|
3960
|
-
|
|
3961
|
-
|
|
3962
|
-
threshold,
|
|
3963
|
-
minQuality,
|
|
3964
|
-
),
|
|
4180
|
+
qualityThresholds.map((threshold) => {
|
|
4181
|
+
const thresholdSelector = candidateSelectorFor(threshold);
|
|
4182
|
+
return [
|
|
4183
|
+
`q${threshold}+`,
|
|
4184
|
+
summarizeRows(
|
|
4185
|
+
selectRows(rows, thresholdSelector),
|
|
4186
|
+
getPeriodDays(rows),
|
|
4187
|
+
summaryOptions,
|
|
3965
4188
|
),
|
|
3966
|
-
|
|
3967
|
-
|
|
3968
|
-
),
|
|
3969
|
-
]),
|
|
4189
|
+
];
|
|
4190
|
+
}),
|
|
3970
4191
|
),
|
|
3971
4192
|
directions: summarizeDirections(rows, candidateSelector, summaryOptions),
|
|
3972
4193
|
months: summarizeMonths(rows, candidateSelector, summaryOptions),
|
|
@@ -4003,6 +4224,9 @@ export const buildAblationReport = ({
|
|
|
4003
4224
|
validationSplit,
|
|
4004
4225
|
testSplit,
|
|
4005
4226
|
partitionMode: exactCalendarPartitions ? 'exact-calendar' : 'ratio',
|
|
4227
|
+
comparisonWindow: explicitWindow
|
|
4228
|
+
? { start: windowStart, end: windowEnd, interval: '[start, end)' }
|
|
4229
|
+
: null,
|
|
4006
4230
|
tuningSince: exactCalendarPartitions
|
|
4007
4231
|
? new Date(tuningSince).toISOString()
|
|
4008
4232
|
: null,
|
|
@@ -5021,9 +5245,8 @@ export const main = async () => {
|
|
|
5021
5245
|
}
|
|
5022
5246
|
const sourceRepositoryRoot = findSourceRepositoryRoot();
|
|
5023
5247
|
const sourceRepositoryKind = getSourceRepositoryKind(sourceRepositoryRoot);
|
|
5024
|
-
const frameworkRepositoryRoot =
|
|
5025
|
-
sourceRepositoryRoot
|
|
5026
|
-
);
|
|
5248
|
+
const frameworkRepositoryRoot =
|
|
5249
|
+
findFrameworkRepositoryRoot(sourceRepositoryRoot);
|
|
5027
5250
|
if (options.crossStrategy) {
|
|
5028
5251
|
if (options.tuningSince != null || options.testSince != null) {
|
|
5029
5252
|
throw new Error(
|
|
@@ -5134,6 +5357,8 @@ export const main = async () => {
|
|
|
5134
5357
|
testSplit: options.testSplit,
|
|
5135
5358
|
tuningSince: options.tuningSince,
|
|
5136
5359
|
testSince: options.testSince,
|
|
5360
|
+
windowStart: options.windowStart,
|
|
5361
|
+
windowEnd: options.windowEnd,
|
|
5137
5362
|
capacities: options.capacities,
|
|
5138
5363
|
maxLossValue: options.maxLossValue,
|
|
5139
5364
|
sourceRepositoryRoot,
|
package/dist/skill-bundle/.codex/skills/ai-train-local-research/scripts/ai-gate-ablation.test.mjs
CHANGED
|
@@ -7,6 +7,7 @@ import test from 'node:test';
|
|
|
7
7
|
|
|
8
8
|
import {
|
|
9
9
|
aggregateBenchmarkDiscoveryRows,
|
|
10
|
+
applyTimestampRotationPlacebos,
|
|
10
11
|
applyBenchmarkEventSnapshots,
|
|
11
12
|
balanceCrossStrategyRows,
|
|
12
13
|
buildAblationReport,
|
|
@@ -35,6 +36,7 @@ import {
|
|
|
35
36
|
parseVariant,
|
|
36
37
|
partitionCrossStrategyFeatures,
|
|
37
38
|
resolveArtifactProjectRoot,
|
|
39
|
+
selectRowsWithinCapacity,
|
|
38
40
|
splitRowsByTimestamp,
|
|
39
41
|
splitRowsByTimestampBounds,
|
|
40
42
|
summarizeRows,
|
|
@@ -959,6 +961,181 @@ test('applies filter, exclude, add, and replace selection semantics', () => {
|
|
|
959
961
|
assert.equal(selected('replace', false, true), true);
|
|
960
962
|
});
|
|
961
963
|
|
|
964
|
+
test('applies deterministic timestamp-local capacity ranking', () => {
|
|
965
|
+
const timestamp = Date.UTC(2026, 0, 1);
|
|
966
|
+
const rows = [
|
|
967
|
+
{ timestamp, symbol: 'B', sequence: 0, features: { margin: 2 } },
|
|
968
|
+
{ timestamp, symbol: 'A', sequence: 1, features: { margin: 2 } },
|
|
969
|
+
{ timestamp, symbol: 'C', sequence: 2, features: { margin: 1 } },
|
|
970
|
+
{ timestamp, symbol: 'D', sequence: 3, features: {} },
|
|
971
|
+
];
|
|
972
|
+
const selected = selectRowsWithinCapacity({
|
|
973
|
+
rows,
|
|
974
|
+
selector: () => true,
|
|
975
|
+
selection: {
|
|
976
|
+
capacity: 2,
|
|
977
|
+
rankBy: [{ path: 'margin', order: 'desc' }],
|
|
978
|
+
},
|
|
979
|
+
});
|
|
980
|
+
|
|
981
|
+
assert.deepEqual(
|
|
982
|
+
rows.filter((row) => selected.has(row)).map((row) => row.symbol),
|
|
983
|
+
['B', 'A'],
|
|
984
|
+
);
|
|
985
|
+
});
|
|
986
|
+
|
|
987
|
+
test('applies variant capacity before every report slice', () => {
|
|
988
|
+
const timestamp = Date.UTC(2026, 0, 1);
|
|
989
|
+
const variant = parseVariant('ranked::replace@4::true');
|
|
990
|
+
variant.selection = {
|
|
991
|
+
capacity: 2,
|
|
992
|
+
rankBy: [{ path: 'margin', order: 'desc' }],
|
|
993
|
+
};
|
|
994
|
+
const rows = [1, 3, 2].map((margin, sequence) => ({
|
|
995
|
+
timestamp,
|
|
996
|
+
profit: margin,
|
|
997
|
+
symbol: `S${sequence}`,
|
|
998
|
+
direction: 'SHORT',
|
|
999
|
+
directionMatches: true,
|
|
1000
|
+
quality: 4,
|
|
1001
|
+
variantMatches: [true],
|
|
1002
|
+
features: { margin },
|
|
1003
|
+
sequence,
|
|
1004
|
+
}));
|
|
1005
|
+
const report = buildAblationReport({
|
|
1006
|
+
rows,
|
|
1007
|
+
variants: [variant],
|
|
1008
|
+
minQuality: 4,
|
|
1009
|
+
qualityThresholds: [4],
|
|
1010
|
+
terminalWindows: [7],
|
|
1011
|
+
validationSplit: 0,
|
|
1012
|
+
filePaths: ['part1.jsonl'],
|
|
1013
|
+
});
|
|
1014
|
+
|
|
1015
|
+
assert.equal(report.variants[0].periods.full.trades, 2);
|
|
1016
|
+
assert.equal(report.variants[0].periods.full.totalProfit, 5);
|
|
1017
|
+
assert.equal(report.variants[0].train.trades, 2);
|
|
1018
|
+
assert.deepEqual(report.variants[0].selection, variant.selection);
|
|
1019
|
+
});
|
|
1020
|
+
|
|
1021
|
+
test('uses common half-open comparison bounds for sparse gate exports', () => {
|
|
1022
|
+
const start = Date.UTC(2025, 0, 1);
|
|
1023
|
+
const day = 86400000;
|
|
1024
|
+
const end = start + 365 * day;
|
|
1025
|
+
const rows = [-1, 0, 100, 365].map((offset, sequence) => ({
|
|
1026
|
+
timestamp: start + offset * day,
|
|
1027
|
+
profit: 10,
|
|
1028
|
+
symbol: 'A',
|
|
1029
|
+
direction: 'LONG',
|
|
1030
|
+
directionMatches: true,
|
|
1031
|
+
quality: 4,
|
|
1032
|
+
variantMatches: [true],
|
|
1033
|
+
sequence,
|
|
1034
|
+
}));
|
|
1035
|
+
const args = {
|
|
1036
|
+
rows,
|
|
1037
|
+
variants: [parseVariant('pass::replace@4::true')],
|
|
1038
|
+
minQuality: 4,
|
|
1039
|
+
qualityThresholds: [4],
|
|
1040
|
+
terminalWindows: [30, 7],
|
|
1041
|
+
validationSplit: 0,
|
|
1042
|
+
filePaths: ['fixture.jsonl'],
|
|
1043
|
+
windowStart: start,
|
|
1044
|
+
windowEnd: end,
|
|
1045
|
+
};
|
|
1046
|
+
const report = buildAblationReport(args);
|
|
1047
|
+
assert.equal(report.run.rows, 2);
|
|
1048
|
+
assert.equal(report.variants[0].periods.full.totalProfit, 20);
|
|
1049
|
+
assert.equal(report.variants[0].periods.full.cadencePerDay, 2 / 365);
|
|
1050
|
+
assert.equal(
|
|
1051
|
+
report.variants[0].periodDirections.full.LONG.cadencePerDay,
|
|
1052
|
+
2 / 365,
|
|
1053
|
+
);
|
|
1054
|
+
assert.equal(report.variants[0].periods['30d'].trades, 0);
|
|
1055
|
+
assert.deepEqual(report.variants[0].equity.at(-1), [end - 1, 20]);
|
|
1056
|
+
assert.throws(
|
|
1057
|
+
() => buildAblationReport({ ...args, windowEnd: null }),
|
|
1058
|
+
/both windowStart/,
|
|
1059
|
+
);
|
|
1060
|
+
assert.throws(
|
|
1061
|
+
() => buildAblationReport({ ...args, windowEnd: start }),
|
|
1062
|
+
/increasing bounds/,
|
|
1063
|
+
);
|
|
1064
|
+
const empty = buildAblationReport({
|
|
1065
|
+
...args,
|
|
1066
|
+
windowStart: end + day,
|
|
1067
|
+
windowEnd: end + 2 * day,
|
|
1068
|
+
});
|
|
1069
|
+
assert.equal(empty.variants[0].periods.full.trades, 0);
|
|
1070
|
+
assert.equal(empty.variants[0].periods.full.totalProfit, 0);
|
|
1071
|
+
const parsed = parseCliArgs([
|
|
1072
|
+
'--windowStart',
|
|
1073
|
+
String(start),
|
|
1074
|
+
'--windowEnd',
|
|
1075
|
+
new Date(end).toISOString(),
|
|
1076
|
+
]);
|
|
1077
|
+
assert.equal(parsed.windowStart, start);
|
|
1078
|
+
assert.equal(parsed.windowEnd, end);
|
|
1079
|
+
});
|
|
1080
|
+
|
|
1081
|
+
test('rotates placebo approval timestamps while preserving event count', () => {
|
|
1082
|
+
const variants = [
|
|
1083
|
+
{ name: 'reference' },
|
|
1084
|
+
{
|
|
1085
|
+
name: 'rotated',
|
|
1086
|
+
placebo: {
|
|
1087
|
+
type: 'timestamp-rotation',
|
|
1088
|
+
referenceVariant: 'reference',
|
|
1089
|
+
offsetEvents: 1,
|
|
1090
|
+
},
|
|
1091
|
+
},
|
|
1092
|
+
];
|
|
1093
|
+
const rows = [0, 1, 2, 3].map((index) => ({
|
|
1094
|
+
timestamp: Date.UTC(2026, 0, index + 1),
|
|
1095
|
+
variantMatches: [index === 0 || index === 2, true],
|
|
1096
|
+
}));
|
|
1097
|
+
|
|
1098
|
+
applyTimestampRotationPlacebos(rows, variants);
|
|
1099
|
+
|
|
1100
|
+
assert.deepEqual(
|
|
1101
|
+
rows.filter((row) => row.variantMatches[1]).map((row) => row.timestamp),
|
|
1102
|
+
[Date.UTC(2026, 0, 2), Date.UTC(2026, 0, 4)],
|
|
1103
|
+
);
|
|
1104
|
+
});
|
|
1105
|
+
|
|
1106
|
+
test('preserves timestamp-rotation event count inside each supplied partition', () => {
|
|
1107
|
+
const variants = [
|
|
1108
|
+
{ name: 'reference' },
|
|
1109
|
+
{
|
|
1110
|
+
name: 'rotated',
|
|
1111
|
+
placebo: {
|
|
1112
|
+
type: 'timestamp-rotation',
|
|
1113
|
+
referenceVariant: 'reference',
|
|
1114
|
+
offsetEvents: 1,
|
|
1115
|
+
},
|
|
1116
|
+
},
|
|
1117
|
+
];
|
|
1118
|
+
const rows = [0, 1, 2, 3, 4, 5].map((index) => ({
|
|
1119
|
+
timestamp: Date.UTC(2026, 0, index + 1),
|
|
1120
|
+
variantMatches: [[0, 2, 3, 5].includes(index), true],
|
|
1121
|
+
}));
|
|
1122
|
+
const partitions = [rows.slice(0, 3), rows.slice(3)];
|
|
1123
|
+
|
|
1124
|
+
applyTimestampRotationPlacebos(rows, variants, partitions);
|
|
1125
|
+
|
|
1126
|
+
assert.deepEqual(
|
|
1127
|
+
partitions.map(
|
|
1128
|
+
(partition) =>
|
|
1129
|
+
new Set(
|
|
1130
|
+
partition
|
|
1131
|
+
.filter((row) => row.variantMatches[1])
|
|
1132
|
+
.map((row) => row.timestamp),
|
|
1133
|
+
).size,
|
|
1134
|
+
),
|
|
1135
|
+
[2, 2],
|
|
1136
|
+
);
|
|
1137
|
+
});
|
|
1138
|
+
|
|
962
1139
|
test('calculates required profit, drawdown, strict-loss, and cadence metrics', () => {
|
|
963
1140
|
const rows = [
|
|
964
1141
|
{
|
|
@@ -1067,12 +1244,18 @@ test('uses exact calendar boundaries without splitting timestamp events', () =>
|
|
|
1067
1244
|
|
|
1068
1245
|
const split = splitRowsByTimestampBounds(rows, tuning, testStart);
|
|
1069
1246
|
|
|
1070
|
-
assert.deepEqual(
|
|
1071
|
-
|
|
1072
|
-
'
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1247
|
+
assert.deepEqual(
|
|
1248
|
+
split.train.map((row) => row.id),
|
|
1249
|
+
['train'],
|
|
1250
|
+
);
|
|
1251
|
+
assert.deepEqual(
|
|
1252
|
+
split.tuning.map((row) => row.id),
|
|
1253
|
+
['tuning-a', 'tuning-b'],
|
|
1254
|
+
);
|
|
1255
|
+
assert.deepEqual(
|
|
1256
|
+
split.test.map((row) => row.id),
|
|
1257
|
+
['test'],
|
|
1258
|
+
);
|
|
1076
1259
|
});
|
|
1077
1260
|
|
|
1078
1261
|
test('builds timestamp-grouped cumulative equity with common endpoints', () => {
|
|
@@ -480,7 +480,7 @@ const dashboardSvg = (board) => {
|
|
|
480
480
|
const terminalLegend = terminalSeries
|
|
481
481
|
.map((candidate, index) => {
|
|
482
482
|
const x = bar.x + index * terminalLegendWidth;
|
|
483
|
-
return `<g data-terminal-legend="${escapeXml(candidate.id)}"><rect x="${x}" y="402" width="16" height="16" rx="3" fill="${candidate.color}"/><text x="${x + 24}" y="416" class="axis">${escapeXml(truncate(candidate.label,
|
|
483
|
+
return `<g data-terminal-legend="${escapeXml(candidate.id)}"><rect x="${x}" y="402" width="16" height="16" rx="3" fill="${candidate.color}"/><text x="${x + 24}" y="416" class="axis">${escapeXml(truncate(candidate.label, Math.floor((terminalLegendWidth - 35) / 9)))}</text></g>`;
|
|
484
484
|
})
|
|
485
485
|
.join('');
|
|
486
486
|
const pnlValues = terminalSeries.flatMap(({ terminal }) =>
|
|
@@ -556,18 +556,18 @@ const dashboardSvg = (board) => {
|
|
|
556
556
|
const x = sx(candidate.metrics.maxDrawdown);
|
|
557
557
|
const y = sy(candidate.metrics.pnl);
|
|
558
558
|
const selectedPoint = candidate.id === selected.id;
|
|
559
|
-
const labelY = y +
|
|
559
|
+
const labelY = y + 5;
|
|
560
560
|
const labelOnLeft = x > scatter.x + scatter.width * 0.66;
|
|
561
561
|
const labelX = x + (labelOnLeft ? -14 : 14);
|
|
562
562
|
const anchor = labelOnLeft ? 'end' : 'start';
|
|
563
|
-
return `${selectedPoint ? `<circle cx="${x}" cy="${y}" r="24" fill="${candidate.color}" opacity="0.18"/>` : ''}<circle cx="${x}" cy="${y}" r="${selectedPoint ? 12 : 9}" fill="${candidate.color}" stroke="${selectedPoint ? '#7a321b' : '#ffffff'}" stroke-width="${selectedPoint ? 3 : 2}"/><text x="${labelX}" y="${labelY}" text-anchor="${anchor}" class="pointLabel" fill="${selectedPoint ? candidate.color : '#24302a'}">${
|
|
563
|
+
return `${selectedPoint ? `<circle cx="${x}" cy="${y}" r="24" fill="${candidate.color}" opacity="0.18"/>` : ''}<circle cx="${x}" cy="${y}" r="${selectedPoint ? 12 : 9}" fill="${candidate.color}" stroke="${selectedPoint ? '#7a321b' : '#ffffff'}" stroke-width="${selectedPoint ? 3 : 2}"/><text x="${labelX}" y="${labelY}" text-anchor="${anchor}" class="pointLabel" fill="${selectedPoint ? candidate.color : '#24302a'}">${index === 0 ? '0' : String.fromCharCode(64 + index)}</text>`;
|
|
564
564
|
})
|
|
565
565
|
.join('');
|
|
566
566
|
|
|
567
567
|
const limitations = board.limitations.length
|
|
568
568
|
? `Limitations: ${board.limitations.join(' · ')}`
|
|
569
569
|
: 'Limitations: none recorded';
|
|
570
|
-
return `<svg xmlns="http://www.w3.org/2000/svg" width="${width}" height="${height}" viewBox="0 0 ${width} ${height}" role="img" aria-label="${escapeXml(board.strategy)} final composition dashboard"><style>.bg{fill:#f5f4ef}.panel{fill:#fff;stroke:#ddd9d0;stroke-width:2}.title{font:500 42px Arial,sans-serif;fill:#17211d}.subtitle{font:22px Arial,sans-serif;fill:#38433e}.cardLabel{font:21px Arial,sans-serif;fill:#303a35}.cardValue{font:500 44px Arial,sans-serif}.cardDetail{font:18px Arial,sans-serif;fill:#303a35}.section{font:500 27px Arial,sans-serif;fill:#17211d}.muted{font:18px Arial,sans-serif;fill:#45514a}.axis{font:15px Arial,sans-serif;fill:#58645e}.grid{stroke:#d9ddd9;stroke-width:1.5}.faint{opacity:.5}.barValue{font:16px Arial,sans-serif;fill:#24302a}.windowLabel{font:20px Arial,sans-serif;fill:#24302a}.windowCount{font:15px Arial,sans-serif;fill:#45514a}.pointLabel{font:16px Arial,sans-serif}.footer{font:18px Arial,sans-serif;fill:#6b4b1f}</style><rect width="100%" height="100%" class="bg"/><text x="72" y="70" class="title">${escapeXml(board.strategy)} · ${escapeXml(selected.label)}</text><text x="72" y="108" class="subtitle">${escapeXml(board.subtitle)}</text>${cardsMarkup}<rect x="72" y="330" width="1040" height="650" rx="20" class="panel"/><text x="100" y="380" class="section">PnL in terminal windows</text>${terminalLegend}<rect x="1145" y="330" width="585" height="650" rx="20" class="panel"/><text x="1180" y="380" class="section">Final compositions: PnL ↔ drawdown</text><text x="1180" y="412" class="muted">Higher and farther left is preferable</text><g>${barGrid}<line x1="${bar.x}" x2="${bar.x + bar.width}" y1="${zeroY}" y2="${zeroY}" stroke="#8f9994" stroke-width="1.5"/>${bars}</g><g>${scatterGrid}${points}<text x="${scatter.x + scatter.width / 2}" y="${scatter.y + scatter.height + 65}" text-anchor="middle" class="muted">
|
|
570
|
+
return `<svg xmlns="http://www.w3.org/2000/svg" width="${width}" height="${height}" viewBox="0 0 ${width} ${height}" role="img" aria-label="${escapeXml(board.strategy)} final composition dashboard"><style>.bg{fill:#f5f4ef}.panel{fill:#fff;stroke:#ddd9d0;stroke-width:2}.title{font:500 42px Arial,sans-serif;fill:#17211d}.subtitle{font:22px Arial,sans-serif;fill:#38433e}.cardLabel{font:21px Arial,sans-serif;fill:#303a35}.cardValue{font:500 44px Arial,sans-serif}.cardDetail{font:18px Arial,sans-serif;fill:#303a35}.section{font:500 27px Arial,sans-serif;fill:#17211d}.muted{font:18px Arial,sans-serif;fill:#45514a}.axis{font:15px Arial,sans-serif;fill:#58645e}.grid{stroke:#d9ddd9;stroke-width:1.5}.faint{opacity:.5}.barValue{font:16px Arial,sans-serif;fill:#24302a}.windowLabel{font:20px Arial,sans-serif;fill:#24302a}.windowCount{font:15px Arial,sans-serif;fill:#45514a}.pointLabel{font:16px Arial,sans-serif}.footer{font:18px Arial,sans-serif;fill:#6b4b1f}</style><rect width="100%" height="100%" class="bg"/><text x="72" y="70" class="title">${escapeXml(board.strategy)} · ${escapeXml(selected.label)}</text><text x="72" y="108" class="subtitle">${escapeXml(board.subtitle)}</text>${cardsMarkup}<rect x="72" y="330" width="1040" height="650" rx="20" class="panel"/><text x="100" y="380" class="section">PnL in terminal windows</text>${terminalLegend}<rect x="1145" y="330" width="585" height="650" rx="20" class="panel"/><text x="1180" y="380" class="section">Final compositions: PnL ↔ drawdown</text><text x="1180" y="412" class="muted">Higher and farther left is preferable</text><g>${barGrid}<line x1="${bar.x}" x2="${bar.x + bar.width}" y1="${zeroY}" y2="${zeroY}" stroke="#8f9994" stroke-width="1.5"/>${bars}</g><g>${scatterGrid}${points}<text x="${scatter.x + scatter.width / 2}" y="${scatter.y + scatter.height + 65}" text-anchor="middle" class="muted">MaxDD (trade stream)</text></g><g><rect x="72" y="1020" width="1658" height="112" rx="16" fill="#fff5dd" stroke="#efd79c" stroke-width="2"/>${svgTextLines({ lines: [truncate(limitations, 155), `Risk normalization: MAX_LOSS_VALUE=${board.normalization.maxLossValue} · ${board.normalization.pnlUnit} · ${board.researchId}`], x: 98, y: 1062, lineHeight: 30, className: 'footer' })}</g></svg>`;
|
|
571
571
|
};
|
|
572
572
|
|
|
573
573
|
const downsample = (points, maxPoints = 1200) => {
|
|
@@ -621,9 +621,7 @@ const equitySvg = (board) => {
|
|
|
621
621
|
}).join('');
|
|
622
622
|
const spanDays = (maxTime - minTime) / 86_400_000;
|
|
623
623
|
const xTicks = Array.from({ length: 6 }, (_, index) => {
|
|
624
|
-
const timestamp = Math.round(
|
|
625
|
-
minTime + ((maxTime - minTime) * index) / 5,
|
|
626
|
-
);
|
|
624
|
+
const timestamp = Math.round(minTime + ((maxTime - minTime) * index) / 5);
|
|
627
625
|
const date = new Date(timestamp);
|
|
628
626
|
const label =
|
|
629
627
|
spanDays >= 730
|
|
@@ -650,6 +648,9 @@ const equitySvg = (board) => {
|
|
|
650
648
|
const curves = board.candidates
|
|
651
649
|
.map((candidate) => {
|
|
652
650
|
const points = downsample(candidate.equity)
|
|
651
|
+
.flatMap((point, index, series) =>
|
|
652
|
+
index === 0 ? [point] : [[point[0], series[index - 1][1]], point],
|
|
653
|
+
)
|
|
653
654
|
.map(
|
|
654
655
|
([timestamp, pnl]) =>
|
|
655
656
|
`${x(timestamp).toFixed(1)},${y(pnl).toFixed(1)}`,
|
|
@@ -667,11 +668,11 @@ const equitySvg = (board) => {
|
|
|
667
668
|
const row = Math.floor(index / columns);
|
|
668
669
|
const lx = left + column * legendWidth;
|
|
669
670
|
const ly = height - bottom + 110 + row * 58;
|
|
670
|
-
return `<g transform="translate(${lx},${ly})"><rect width="18" height="18" rx="3" fill="${candidate.color}"/><text x="28" y="15" class="legendLabel">${escapeXml(truncate(candidate.label, 35))}</text><text x="28" y="37" class="legendMetric">N=${candidate.metrics.trades} · PnL=${formatNumber(candidate.metrics.pnl, 1)} · DD=${formatNumber(candidate.metrics.maxDrawdown, 1)}</text></g>`;
|
|
671
|
+
return `<g transform="translate(${lx},${ly})"><rect width="18" height="18" rx="3" fill="${candidate.color}"/><text x="28" y="15" class="legendLabel">${index === 0 ? '0' : String.fromCharCode(64 + index)} · ${escapeXml(truncate(candidate.label, 35))}</text><text x="28" y="37" class="legendMetric">N=${candidate.metrics.trades} · PnL=${formatNumber(candidate.metrics.pnl, 1)} · DD=${formatNumber(candidate.metrics.maxDrawdown, 1)}</text></g>`;
|
|
671
672
|
})
|
|
672
673
|
.join('');
|
|
673
674
|
const selected = board.candidates.find(({ id }) => id === board.selectedId);
|
|
674
|
-
return `<svg xmlns="http://www.w3.org/2000/svg" width="${width}" height="${height}" viewBox="0 0 ${width} ${height}" role="img" aria-label="${escapeXml(board.strategy)} final composition equity"><style>.bg{fill:#fff}.title{font:700 36px Arial,sans-serif;fill:#172033}.subtitle{font:18px Arial,sans-serif;fill:#687184}.axis{font:15px Arial,sans-serif;fill:#687184}.grid{stroke:#e2e6eb;stroke-width:1.5}.faint{opacity:.55}.legendLabel{font:600 17px Arial,sans-serif;fill:#263044}.legendMetric{font:15px Arial,sans-serif;fill:#687184}.axisTitle{font:17px Arial,sans-serif;fill:#394459}</style><rect width="100%" height="100%" class="bg"/><text x="${left}" y="58" class="title">${escapeXml(board.title)}</text><text x="${left}" y="92" class="subtitle">Baseline =
|
|
675
|
+
return `<svg xmlns="http://www.w3.org/2000/svg" width="${width}" height="${height}" viewBox="0 0 ${width} ${height}" role="img" aria-label="${escapeXml(board.strategy)} final composition equity"><style>.bg{fill:#fff}.title{font:700 36px Arial,sans-serif;fill:#172033}.subtitle{font:18px Arial,sans-serif;fill:#687184}.axis{font:15px Arial,sans-serif;fill:#687184}.grid{stroke:#e2e6eb;stroke-width:1.5}.faint{opacity:.55}.legendLabel{font:600 17px Arial,sans-serif;fill:#263044}.legendMetric{font:15px Arial,sans-serif;fill:#687184}.axisTitle{font:17px Arial,sans-serif;fill:#394459}</style><rect width="100%" height="100%" class="bg"/><text x="${left}" y="58" class="title">${escapeXml(board.title)}</text><text x="${left}" y="92" class="subtitle">Baseline = current gate behavior; candidates = core + own deterministic gate</text><text x="${left}" y="120" class="subtitle">Selected: ${escapeXml(selected.label)} · ${escapeXml(board.subtitle)}</text>${yGrid}${xGrid}<line x1="${left}" x2="${width - right}" y1="${y(0)}" y2="${y(0)}" stroke="#aab2bd" stroke-width="1.5"/>${curves}<line x1="${left}" x2="${left}" y1="${top}" y2="${height - bottom}" stroke="#7d8795" stroke-width="1.5"/><line x1="${left}" x2="${width - right}" y1="${height - bottom}" y2="${height - bottom}" stroke="#7d8795" stroke-width="1.5"/><text x="${left + plotWidth / 2}" y="${height - bottom + 67}" text-anchor="middle" class="axisTitle">Date (UTC)</text><text x="30" y="${top + plotHeight / 2}" transform="rotate(-90 30 ${top + plotHeight / 2})" text-anchor="middle" class="axisTitle">Cumulative PnL (${escapeXml(board.normalization.pnlUnit)})</text>${legend}</svg>`;
|
|
675
676
|
};
|
|
676
677
|
|
|
677
678
|
const verifyCandidateArtifacts = async (board, artifactRoot) => {
|
|
@@ -174,9 +174,23 @@ test('verifies candidate artifacts and renders the dashboard and equity board',
|
|
|
174
174
|
);
|
|
175
175
|
assert.match(dashboard, /PnL in terminal windows/u);
|
|
176
176
|
assert.match(dashboard, /Final compositions: PnL ↔ drawdown/u);
|
|
177
|
-
assert.match(equity, /
|
|
177
|
+
assert.match(equity, /Baseline = current gate behavior/u);
|
|
178
178
|
assert.match(equity, /candidate \+ own gate/u);
|
|
179
|
-
assert.match(equity, /
|
|
179
|
+
assert.match(equity, /Date \(UTC\)/u);
|
|
180
|
+
for (const [, coordinates] of equity.matchAll(
|
|
181
|
+
/<polyline points="([^"]+)"/gu,
|
|
182
|
+
)) {
|
|
183
|
+
const points = coordinates
|
|
184
|
+
.split(' ')
|
|
185
|
+
.map((point) => point.split(',').map(Number));
|
|
186
|
+
for (let index = 1; index < points.length; index += 1) {
|
|
187
|
+
assert.ok(
|
|
188
|
+
points[index][0] === points[index - 1][0] ||
|
|
189
|
+
points[index][1] === points[index - 1][1],
|
|
190
|
+
'Cumulative PnL must not interpolate gains between trade events',
|
|
191
|
+
);
|
|
192
|
+
}
|
|
193
|
+
}
|
|
180
194
|
assert.match(equity, /Nov 2023/u);
|
|
181
195
|
assert.match(equity, /Mar 2024/u);
|
|
182
196
|
});
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"schema": "tradejs-skill-bundle/v1",
|
|
3
3
|
"source": "TradeJS-Dev/TradeJS:.codex/skills",
|
|
4
|
-
"bundleSha256": "
|
|
4
|
+
"bundleSha256": "be995341ae9459133ad1bb60da5c8fac01c5ffbe6d72309c1d68448abbe03c0e",
|
|
5
5
|
"skills": [
|
|
6
6
|
"ai-train-local-research",
|
|
7
7
|
"backtest-config-redis",
|
|
@@ -19,10 +19,10 @@
|
|
|
19
19
|
"strategy-release"
|
|
20
20
|
],
|
|
21
21
|
"files": {
|
|
22
|
-
".codex/skills/ai-train-local-research/references/gate-ablation.md": "
|
|
22
|
+
".codex/skills/ai-train-local-research/references/gate-ablation.md": "a32ca7272a74678aca97dfce6b098ed15ca41fc385b936f25492f47bce4ec621",
|
|
23
23
|
".codex/skills/ai-train-local-research/references/reporting.md": "a5a5cc6438a8cc228560e302e944f1ff320f4c963988af83e1ba1443728f7149",
|
|
24
|
-
".codex/skills/ai-train-local-research/scripts/ai-gate-ablation.mjs": "
|
|
25
|
-
".codex/skills/ai-train-local-research/scripts/ai-gate-ablation.test.mjs": "
|
|
24
|
+
".codex/skills/ai-train-local-research/scripts/ai-gate-ablation.mjs": "4275088307342ef35d848f1bdd39528c22d2c66cf41aa7d3ed26f851c2fe803b",
|
|
25
|
+
".codex/skills/ai-train-local-research/scripts/ai-gate-ablation.test.mjs": "f9adcb02a779fb1da23e9c0aa99dead6152c5f45464138dd1de4283ece4ed363",
|
|
26
26
|
".codex/skills/ai-train-local-research/SKILL.md": "479e44aad9cb6ab6b5c8931c807c63cf2a1938a59d01f553c20f44645716a23a",
|
|
27
27
|
".codex/skills/backtest-config-redis/scripts/get_backtest_config.sh": "833e950d6348c5b7a4bd3f00d25af60f6394190bff744bd5ab64db9e43d826d5",
|
|
28
28
|
".codex/skills/backtest-config-redis/SKILL.md": "85e7b1fa425aa23dfa7950de05c1d3ce046d044dc3872f3665705e9e2f2a1977",
|
|
@@ -44,8 +44,8 @@
|
|
|
44
44
|
".codex/skills/strategy-improvement-research/references/final-composition-board.md": "05b49ea4aba85792ccea0aeaf4ee2637b808157962f0a38631d6468c342349d6",
|
|
45
45
|
".codex/skills/strategy-improvement-research/scripts/build-final-composition-spec.mjs": "b51873691fafbcf2dd70d8c9a6da842fec5f8358346c632cfe79211db08b18ba",
|
|
46
46
|
".codex/skills/strategy-improvement-research/scripts/build-final-composition-spec.test.mjs": "85d06b8fe09ee7611a3fb989173c361ba32f0e9364422925ff221bfe127b2bfa",
|
|
47
|
-
".codex/skills/strategy-improvement-research/scripts/final-composition-board.mjs": "
|
|
48
|
-
".codex/skills/strategy-improvement-research/scripts/final-composition-board.test.mjs": "
|
|
47
|
+
".codex/skills/strategy-improvement-research/scripts/final-composition-board.mjs": "b00f0160823a25c31afbcab66ce0c16fc70055928463a5741b3ffe8063ee8a81",
|
|
48
|
+
".codex/skills/strategy-improvement-research/scripts/final-composition-board.test.mjs": "a40e8b395ed1c758dc9862b44887e71e8cb9352ce26443d244397f76af891b62",
|
|
49
49
|
".codex/skills/strategy-improvement-research/scripts/freeze-gate-variants.mjs": "17b3c868880ddd5b326d309e6e42eaeef1ea22d625a5db947997ac73b221ad6d",
|
|
50
50
|
".codex/skills/strategy-improvement-research/scripts/freeze-gate-variants.test.mjs": "963e1c64ab3deef11fd48a96e10f3827af38036dfe2eb28daf7849f19dd414d8",
|
|
51
51
|
".codex/skills/strategy-improvement-research/SKILL.md": "6964eb154e40b0b775aa2735b2ebbe998984479f46333ed03eab88d2ee150d48",
|