create-tradejs 3.1.28-beta.254 → 3.1.28-beta.256
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 +10 -0
- package/dist/skill-bundle/.codex/skills/ai-train-local-research/references/pocket-feature-exclusion.md +43 -0
- package/dist/skill-bundle/.codex/skills/ai-train-local-research/scripts/ai-gate-ablation.mjs +11 -0
- package/dist/skill-bundle/.codex/skills/ai-train-local-research/scripts/ai-gate-ablation.test.mjs +56 -0
- package/dist/skill-bundle/.codex/skills/strategy-improvement-research/references/final-composition-board.md +10 -0
- package/dist/skill-bundle/.codex/skills/strategy-improvement-research/scripts/final-composition-board.mjs +37 -14
- package/dist/skill-bundle/.codex/skills/strategy-improvement-research/scripts/final-composition-board.test.mjs +172 -7
- package/dist/skill-bundle/.codex/tradejs-skill-bundle.json +8 -7
- package/package.json +1 -1
|
@@ -120,6 +120,16 @@ 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
|
+
JSON reports also include `approvedSignals` for the baseline and every variant
|
|
124
|
+
at `run.minQuality`. Each entry records the source row `sequence`, `signalId`,
|
|
125
|
+
timestamp, symbol, direction, and profit. The trace uses the same selector as
|
|
126
|
+
the metrics, after the common half-open window, direction rule, placebo, and
|
|
127
|
+
optional capacity limit. Use these identities to join approval decisions to
|
|
128
|
+
the frozen export for loss localization and overlapping-position analysis.
|
|
129
|
+
Do not reconstruct a second gate or infer individual approvals from equity
|
|
130
|
+
points. The trace describes historical gate decisions, not submitted or filled
|
|
131
|
+
runtime orders.
|
|
132
|
+
|
|
123
133
|
For a deterministic timestamp-local portfolio limit, a JSON spec variant may
|
|
124
134
|
also define `selection`. The tool first evaluates the gate, then keeps the
|
|
125
135
|
highest-ranked rows independently inside each decision timestamp. Missing rank
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
# Excluding features before pocket search
|
|
2
|
+
|
|
3
|
+
`ai-pocket-search --excludeFeaturePattern <regex>` (short form `-x`) applies an
|
|
4
|
+
optional JavaScript regular expression to dot-separated feature paths before
|
|
5
|
+
feature buckets and atomic predicates are ranked. The expression is case
|
|
6
|
+
sensitive, without slash delimiters or flags. An invalid expression fails during
|
|
7
|
+
option normalization, before loading a dataset or strategy. Omission or an empty
|
|
8
|
+
string preserves the existing search behavior.
|
|
9
|
+
|
|
10
|
+
Use this option when the research protocol excludes a provenance family that the
|
|
11
|
+
general `causal-stationary` classification permits. For example:
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
yarn ai-pocket-search --strategy AdaptiveMomentumRibbon --file <exact-export.jsonl> \
|
|
15
|
+
-n 0 --featurePolicy causal-stationary --featureProfile all \
|
|
16
|
+
--excludeFeaturePattern 'marketBreadths|(^|\.)cmc|referenceContexts\.(?!BTCUSDT(\.|$))'
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
Freeze the actual expression with the experiment before inspecting pocket
|
|
20
|
+
outcomes. This example is only a provenance filter; it is not a complete feature
|
|
21
|
+
policy or evidence of profitability. Audit the remaining feature paths against
|
|
22
|
+
the protocol, including source metadata and clock fields. A symbol-specific
|
|
23
|
+
exception requires a fixed benchmark established in the protocol.
|
|
24
|
+
|
|
25
|
+
Matching a source branch skips its descendants. Filtering takes place before
|
|
26
|
+
derived features are calculated, so an excluded source value cannot populate a
|
|
27
|
+
derived alias. Generated feature paths are also filtered before search. Existing
|
|
28
|
+
outcome, coverage, and stationarity exclusions remain in force; this option can
|
|
29
|
+
only remove features. Removing a source field can also remove a derived feature
|
|
30
|
+
that depended on it.
|
|
31
|
+
|
|
32
|
+
The JSON run metadata records `excludeFeaturePattern`. JSON
|
|
33
|
+
`featurePolicyAudit["operator-excluded"]` reports the number of distinct excluded
|
|
34
|
+
paths and up to five sample paths. These are research policy exclusions, not
|
|
35
|
+
missing-data or data-quality failures. A matched branch counts once, rather than
|
|
36
|
+
counting its unvisited descendant leaves. Text and Markdown reports also record
|
|
37
|
+
the expression. Preserve the expression, report, source revision, exact dataset,
|
|
38
|
+
and checksums together.
|
|
39
|
+
|
|
40
|
+
A post-search filter does not provide the same contract: prohibited predicates
|
|
41
|
+
may have already consumed the atomic ranking budget and displaced allowed ones.
|
|
42
|
+
This option does not change trade labels, PnL, gate decisions, or execution, and
|
|
43
|
+
does not automatically impose the same expression on a separate gate replay.
|
package/dist/skill-bundle/.codex/skills/ai-train-local-research/scripts/ai-gate-ablation.mjs
CHANGED
|
@@ -4089,7 +4089,17 @@ export const buildAblationReport = ({
|
|
|
4089
4089
|
: {}),
|
|
4090
4090
|
};
|
|
4091
4091
|
const baselineSelector = (row) => baselineSelectedAt(row, minQuality);
|
|
4092
|
+
const approvedSignalTrace = (selector) =>
|
|
4093
|
+
selectRows(rows, selector).map((row) => ({
|
|
4094
|
+
sequence: row.sequence ?? null,
|
|
4095
|
+
signalId: row.signalId ?? null,
|
|
4096
|
+
timestamp: row.timestamp,
|
|
4097
|
+
symbol: row.symbol,
|
|
4098
|
+
direction: row.direction,
|
|
4099
|
+
profit: row.profit,
|
|
4100
|
+
}));
|
|
4092
4101
|
const baseline = {
|
|
4102
|
+
approvedSignals: approvedSignalTrace(baselineSelector),
|
|
4093
4103
|
equity: buildEquitySeries(
|
|
4094
4104
|
rows,
|
|
4095
4105
|
baselineSelector,
|
|
@@ -4152,6 +4162,7 @@ export const buildAblationReport = ({
|
|
|
4152
4162
|
expression: variant.expression,
|
|
4153
4163
|
selection: variant.selection ?? null,
|
|
4154
4164
|
placebo: variant.placebo ?? null,
|
|
4165
|
+
approvedSignals: approvedSignalTrace(candidateSelector),
|
|
4155
4166
|
equity: buildEquitySeries(
|
|
4156
4167
|
rows,
|
|
4157
4168
|
candidateSelector,
|
package/dist/skill-bundle/.codex/skills/ai-train-local-research/scripts/ai-gate-ablation.test.mjs
CHANGED
|
@@ -1016,6 +1016,60 @@ test('applies variant capacity before every report slice', () => {
|
|
|
1016
1016
|
assert.equal(report.variants[0].periods.full.totalProfit, 5);
|
|
1017
1017
|
assert.equal(report.variants[0].train.trades, 2);
|
|
1018
1018
|
assert.deepEqual(report.variants[0].selection, variant.selection);
|
|
1019
|
+
assert.deepEqual(
|
|
1020
|
+
report.variants[0].approvedSignals.map((row) => row.sequence),
|
|
1021
|
+
[1, 2],
|
|
1022
|
+
);
|
|
1023
|
+
});
|
|
1024
|
+
|
|
1025
|
+
test('traces the exact approved signals without replaying the gate', () => {
|
|
1026
|
+
const start = Date.UTC(2026, 0, 1);
|
|
1027
|
+
const rows = [4, 3, 5, 4].map((quality, sequence) => ({
|
|
1028
|
+
timestamp: start + sequence * 900000,
|
|
1029
|
+
signalId: `signal-${sequence}`,
|
|
1030
|
+
sequence,
|
|
1031
|
+
symbol: 'A',
|
|
1032
|
+
direction: 'LONG',
|
|
1033
|
+
directionMatches: sequence !== 2,
|
|
1034
|
+
quality,
|
|
1035
|
+
profit: sequence - 1,
|
|
1036
|
+
variantMatches: [sequence === 1],
|
|
1037
|
+
}));
|
|
1038
|
+
const report = buildAblationReport({
|
|
1039
|
+
rows,
|
|
1040
|
+
variants: [parseVariant('replacement::replace@4::true')],
|
|
1041
|
+
minQuality: 4,
|
|
1042
|
+
qualityThresholds: [4],
|
|
1043
|
+
terminalWindows: [7],
|
|
1044
|
+
validationSplit: 0,
|
|
1045
|
+
filePaths: ['fixture.jsonl'],
|
|
1046
|
+
windowStart: start,
|
|
1047
|
+
windowEnd: start + 3 * 900000,
|
|
1048
|
+
});
|
|
1049
|
+
assert.deepEqual(report.baseline.approvedSignals, [
|
|
1050
|
+
{
|
|
1051
|
+
sequence: 0,
|
|
1052
|
+
signalId: 'signal-0',
|
|
1053
|
+
timestamp: start,
|
|
1054
|
+
symbol: 'A',
|
|
1055
|
+
direction: 'LONG',
|
|
1056
|
+
profit: -1,
|
|
1057
|
+
},
|
|
1058
|
+
]);
|
|
1059
|
+
assert.deepEqual(
|
|
1060
|
+
report.variants[0].approvedSignals.map((row) => row.signalId),
|
|
1061
|
+
['signal-1'],
|
|
1062
|
+
);
|
|
1063
|
+
for (const composition of [report.baseline, ...report.variants]) {
|
|
1064
|
+
assert.equal(
|
|
1065
|
+
composition.approvedSignals.length,
|
|
1066
|
+
composition.periods.full.trades,
|
|
1067
|
+
);
|
|
1068
|
+
assert.equal(
|
|
1069
|
+
composition.approvedSignals.reduce((sum, row) => sum + row.profit, 0),
|
|
1070
|
+
composition.periods.full.totalProfit,
|
|
1071
|
+
);
|
|
1072
|
+
}
|
|
1019
1073
|
});
|
|
1020
1074
|
|
|
1021
1075
|
test('uses common half-open comparison bounds for sparse gate exports', () => {
|
|
@@ -1068,6 +1122,8 @@ test('uses common half-open comparison bounds for sparse gate exports', () => {
|
|
|
1068
1122
|
});
|
|
1069
1123
|
assert.equal(empty.variants[0].periods.full.trades, 0);
|
|
1070
1124
|
assert.equal(empty.variants[0].periods.full.totalProfit, 0);
|
|
1125
|
+
assert.deepEqual(empty.baseline.approvedSignals, []);
|
|
1126
|
+
assert.deepEqual(empty.variants[0].approvedSignals, []);
|
|
1071
1127
|
const parsed = parseCliArgs([
|
|
1072
1128
|
'--windowStart',
|
|
1073
1129
|
String(start),
|
|
@@ -115,6 +115,16 @@ of the same SVG. Link or display both charts in the final answer and immutable
|
|
|
115
115
|
research note. Store the complete board spec and summary in Project-owned
|
|
116
116
|
evidence; paths alone do not replace the note's machine-readable metrics.
|
|
117
117
|
|
|
118
|
+
For these research comparison charts, use continuous straight segments between
|
|
119
|
+
the recorded equity samples by default, not staircase corners. This matches the
|
|
120
|
+
preferred presentation without averaging returns or rounding away drawdowns.
|
|
121
|
+
Keep the observed endpoints and extrema; do not use moving averages or splines
|
|
122
|
+
that invent peaks or troughs. The connecting line is a visual interpolation,
|
|
123
|
+
not a claim about account value between events or an intratrade mark-to-market
|
|
124
|
+
path. Record `rendering.equityInterpolation = linear` in the chart summary.
|
|
125
|
+
For presentation-only updates, render to a new output directory and retain the
|
|
126
|
+
frozen reports, equity arrays, metrics, selections, and original chart files.
|
|
127
|
+
|
|
118
128
|
## Spec shape
|
|
119
129
|
|
|
120
130
|
```json
|
|
@@ -551,23 +551,40 @@ const dashboardSvg = (board) => {
|
|
|
551
551
|
const x = sx(dd);
|
|
552
552
|
return `<line x1="${scatter.x}" x2="${scatter.x + scatter.width}" y1="${y}" y2="${y}" class="grid"/><text x="${scatter.x - 12}" y="${y + 5}" text-anchor="end" class="axis">${escapeXml(formatCompact(pnl))}</text><line x1="${x}" x2="${x}" y1="${scatter.y}" y2="${scatter.y + scatter.height}" class="grid faint"/><text x="${x}" y="${scatter.y + scatter.height + 28}" text-anchor="middle" class="axis">${escapeXml(formatCompact(dd))}</text>`;
|
|
553
553
|
}).join('');
|
|
554
|
+
const pointGroups = new Map();
|
|
554
555
|
const points = board.candidates
|
|
555
556
|
.map((candidate, index) => {
|
|
556
557
|
const x = sx(candidate.metrics.maxDrawdown);
|
|
557
558
|
const y = sy(candidate.metrics.pnl);
|
|
558
559
|
const selectedPoint = candidate.id === selected.id;
|
|
559
|
-
const
|
|
560
|
+
const key = stableStringify([
|
|
561
|
+
candidate.metrics.maxDrawdown,
|
|
562
|
+
candidate.metrics.pnl,
|
|
563
|
+
]);
|
|
564
|
+
const group = pointGroups.get(key) ?? [];
|
|
565
|
+
group.push({ candidate, index, x, y });
|
|
566
|
+
pointGroups.set(key, group);
|
|
567
|
+
return `${selectedPoint ? `<circle cx="${x}" cy="${y}" r="24" fill="${candidate.color}" opacity="0.18"/>` : ''}<circle data-scatter-point="${escapeXml(candidate.id)}" cx="${x}" cy="${y}" r="${selectedPoint ? 12 : 9}" fill="${candidate.color}" stroke="${selectedPoint ? '#7a321b' : '#ffffff'}" stroke-width="${selectedPoint ? 3 : 2}"/>`;
|
|
568
|
+
})
|
|
569
|
+
.join('');
|
|
570
|
+
const pointLabels = [...pointGroups.values()]
|
|
571
|
+
.map((group) => {
|
|
572
|
+
const { candidate, x, y } =
|
|
573
|
+
group.find((point) => point.candidate.id === selected.id) ?? group[0];
|
|
560
574
|
const labelOnLeft = x > scatter.x + scatter.width * 0.66;
|
|
561
|
-
const
|
|
562
|
-
|
|
563
|
-
|
|
575
|
+
const label = group
|
|
576
|
+
.map(({ index }) =>
|
|
577
|
+
index === 0 ? '0' : String.fromCharCode(64 + index),
|
|
578
|
+
)
|
|
579
|
+
.join(' / ');
|
|
580
|
+
return `<text data-point-label-ids="${escapeXml(group.map(({ candidate }) => candidate.id).join(','))}" x="${x + (labelOnLeft ? -14 : 14)}" y="${y + 5}" text-anchor="${labelOnLeft ? 'end' : 'start'}" class="pointLabel" fill="${candidate.id === selected.id ? candidate.color : '#24302a'}">${label}</text>`;
|
|
564
581
|
})
|
|
565
582
|
.join('');
|
|
566
583
|
|
|
567
584
|
const limitations = board.limitations.length
|
|
568
585
|
? `Limitations: ${board.limitations.join(' · ')}`
|
|
569
586
|
: '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">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>`;
|
|
587
|
+
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}${pointLabels}<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
588
|
};
|
|
572
589
|
|
|
573
590
|
const downsample = (points, maxPoints = 1200) => {
|
|
@@ -598,13 +615,17 @@ const downsample = (points, maxPoints = 1200) => {
|
|
|
598
615
|
|
|
599
616
|
const equitySvg = (board) => {
|
|
600
617
|
const width = 1800;
|
|
601
|
-
const height = 1200;
|
|
602
618
|
const left = 110;
|
|
603
619
|
const right = 70;
|
|
604
620
|
const top = 150;
|
|
605
|
-
const
|
|
621
|
+
const columns = 3;
|
|
622
|
+
const legendRows = Math.ceil(board.candidates.length / columns);
|
|
623
|
+
const legendTop = 110;
|
|
624
|
+
const legendRowHeight = 58;
|
|
625
|
+
const bottom = Math.max(300, legendTop + legendRows * legendRowHeight + 32);
|
|
626
|
+
const plotHeight = 750;
|
|
627
|
+
const height = top + plotHeight + bottom;
|
|
606
628
|
const plotWidth = width - left - right;
|
|
607
|
-
const plotHeight = height - top - bottom;
|
|
608
629
|
const minTime = board.comparisonWindow.start;
|
|
609
630
|
const maxTime = board.comparisonWindow.end - 1;
|
|
610
631
|
const values = board.candidates.flatMap(({ equity }) =>
|
|
@@ -648,9 +669,6 @@ const equitySvg = (board) => {
|
|
|
648
669
|
const curves = board.candidates
|
|
649
670
|
.map((candidate) => {
|
|
650
671
|
const points = downsample(candidate.equity)
|
|
651
|
-
.flatMap((point, index, series) =>
|
|
652
|
-
index === 0 ? [point] : [[point[0], series[index - 1][1]], point],
|
|
653
|
-
)
|
|
654
672
|
.map(
|
|
655
673
|
([timestamp, pnl]) =>
|
|
656
674
|
`${x(timestamp).toFixed(1)},${y(pnl).toFixed(1)}`,
|
|
@@ -660,15 +678,14 @@ const equitySvg = (board) => {
|
|
|
660
678
|
return `<polyline points="${points}" fill="none" stroke="${candidate.color}" stroke-width="${selected ? 4.5 : 3}" opacity="${selected ? 1 : 0.82}" stroke-linejoin="round" stroke-linecap="round"/>`;
|
|
661
679
|
})
|
|
662
680
|
.join('');
|
|
663
|
-
const columns = 3;
|
|
664
681
|
const legendWidth = (width - left - right) / columns;
|
|
665
682
|
const legend = board.candidates
|
|
666
683
|
.map((candidate, index) => {
|
|
667
684
|
const column = index % columns;
|
|
668
685
|
const row = Math.floor(index / columns);
|
|
669
686
|
const lx = left + column * legendWidth;
|
|
670
|
-
const ly = height - bottom +
|
|
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>`;
|
|
687
|
+
const ly = height - bottom + legendTop + row * legendRowHeight;
|
|
688
|
+
return `<g data-equity-legend="${escapeXml(candidate.id)}" 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>`;
|
|
672
689
|
})
|
|
673
690
|
.join('');
|
|
674
691
|
const selected = board.candidates.find(({ id }) => id === board.selectedId);
|
|
@@ -768,6 +785,12 @@ export const generateFinalCompositionBoard = async ({
|
|
|
768
785
|
terminalComparisonIds: board.terminalComparisonIds,
|
|
769
786
|
comparisonWindow: board.comparisonWindow,
|
|
770
787
|
normalization: board.normalization,
|
|
788
|
+
rendering: {
|
|
789
|
+
equityInterpolation: 'linear',
|
|
790
|
+
meaning:
|
|
791
|
+
'Lines connect event samples; not an intratrade or mark-to-market path.',
|
|
792
|
+
metricInputsChanged: false,
|
|
793
|
+
},
|
|
771
794
|
limitations: board.limitations,
|
|
772
795
|
candidates: board.candidates.map((candidate) => ({
|
|
773
796
|
id: candidate.id,
|
|
@@ -183,18 +183,83 @@ test('verifies candidate artifacts and renders the dashboard and equity board',
|
|
|
183
183
|
const points = coordinates
|
|
184
184
|
.split(' ')
|
|
185
185
|
.map((point) => point.split(',').map(Number));
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
points[index][1] === points[index - 1][1],
|
|
190
|
-
'Cumulative PnL must not interpolate gains between trade events',
|
|
191
|
-
);
|
|
192
|
-
}
|
|
186
|
+
assert.equal(points.length, 2, 'Do not insert synthetic step corners');
|
|
187
|
+
assert.ok(points[1][0] > points[0][0]);
|
|
188
|
+
assert.ok(points[1][1] < points[0][1], 'Join event samples directly');
|
|
193
189
|
}
|
|
190
|
+
assert.equal(summary.rendering.equityInterpolation, 'linear');
|
|
191
|
+
assert.equal(summary.rendering.metricInputsChanged, false);
|
|
194
192
|
assert.match(equity, /Nov 2023/u);
|
|
195
193
|
assert.match(equity, /Mar 2024/u);
|
|
196
194
|
});
|
|
197
195
|
|
|
196
|
+
test('linear presentation preserves flat samples, peaks, troughs and frozen metrics', async () => {
|
|
197
|
+
const root = await mkdtemp(path.join(os.tmpdir(), 'tradejs-final-board-'));
|
|
198
|
+
const contents = 'immutable-evidence\n';
|
|
199
|
+
const spec = makeSpec(hash(contents));
|
|
200
|
+
spec.candidates[0].equity = [
|
|
201
|
+
[1_700_000_000_000, 0],
|
|
202
|
+
[1_701_000_000_000, 0],
|
|
203
|
+
[1_703_000_000_000, 180],
|
|
204
|
+
[1_706_000_000_000, -20],
|
|
205
|
+
[1_710_000_000_000, 100],
|
|
206
|
+
];
|
|
207
|
+
spec.candidates[0].metrics.maxDrawdown = 200;
|
|
208
|
+
const frozenSpec = structuredClone(spec);
|
|
209
|
+
for (const candidate of spec.candidates) {
|
|
210
|
+
for (const key of [
|
|
211
|
+
'coreResult',
|
|
212
|
+
'coreExport',
|
|
213
|
+
'gateReport',
|
|
214
|
+
'gateAuthority',
|
|
215
|
+
]) {
|
|
216
|
+
const reference = candidate.composition[key];
|
|
217
|
+
if (reference) await writeFile(path.join(root, reference.path), contents);
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
const { summary } = await generateFinalCompositionBoard({
|
|
221
|
+
spec,
|
|
222
|
+
artifactRoot: root,
|
|
223
|
+
outDir: path.join(root, 'charts'),
|
|
224
|
+
png: false,
|
|
225
|
+
});
|
|
226
|
+
const svg = await readFile(
|
|
227
|
+
path.join(root, 'charts', 'final-composition-equity.svg'),
|
|
228
|
+
'utf8',
|
|
229
|
+
);
|
|
230
|
+
const points = /<polyline points="([^"]+)"/u
|
|
231
|
+
.exec(svg)[1]
|
|
232
|
+
.split(' ')
|
|
233
|
+
.map((point) => point.split(',').map(Number));
|
|
234
|
+
assert.equal(points.length, frozenSpec.candidates[0].equity.length);
|
|
235
|
+
assert.equal(
|
|
236
|
+
points[0][1],
|
|
237
|
+
points[1][1],
|
|
238
|
+
'A flat observed interval stays flat',
|
|
239
|
+
);
|
|
240
|
+
assert.equal(
|
|
241
|
+
Math.min(...points.map((p) => p[1])),
|
|
242
|
+
points[2][1],
|
|
243
|
+
'Keep the observed peak',
|
|
244
|
+
);
|
|
245
|
+
assert.equal(
|
|
246
|
+
Math.max(...points.map((p) => p[1])),
|
|
247
|
+
points[3][1],
|
|
248
|
+
'Keep the observed trough',
|
|
249
|
+
);
|
|
250
|
+
for (let i = 1; i < points.length; i++)
|
|
251
|
+
assert.ok(points[i][0] > points[i - 1][0]);
|
|
252
|
+
assert.deepEqual(spec, frozenSpec, 'Rendering must not mutate evidence');
|
|
253
|
+
assert.deepEqual(
|
|
254
|
+
summary.candidates.map((c) => c.metrics),
|
|
255
|
+
spec.candidates.map((c) => c.metrics),
|
|
256
|
+
);
|
|
257
|
+
assert.deepEqual(
|
|
258
|
+
summary.candidates.map((c) => c.terminal),
|
|
259
|
+
spec.candidates.map((c) => c.terminal),
|
|
260
|
+
);
|
|
261
|
+
});
|
|
262
|
+
|
|
198
263
|
test('renders an additional terminal comparison without changing selectedId', async () => {
|
|
199
264
|
const root = await mkdtemp(path.join(os.tmpdir(), 'tradejs-final-board-'));
|
|
200
265
|
const contents = 'immutable-evidence\n';
|
|
@@ -238,3 +303,103 @@ test('renders an additional terminal comparison without changing selectedId', as
|
|
|
238
303
|
assert.match(dashboard, /data-terminal-series="transition"/u);
|
|
239
304
|
assert.match(dashboard, /Transition breakout \+ own gate/u);
|
|
240
305
|
});
|
|
306
|
+
|
|
307
|
+
for (const candidateCount of [14, 20]) {
|
|
308
|
+
test(`keeps every legend row visible for ${candidateCount} compositions and groups coincident point labels`, async () => {
|
|
309
|
+
const root = await mkdtemp(path.join(os.tmpdir(), 'tradejs-final-board-'));
|
|
310
|
+
const contents = 'immutable-evidence\n';
|
|
311
|
+
const artifactSha = hash(contents);
|
|
312
|
+
const spec = makeSpec(artifactSha);
|
|
313
|
+
for (let index = 2; index < candidateCount; index += 1) {
|
|
314
|
+
spec.candidates.push(
|
|
315
|
+
makeCandidate({
|
|
316
|
+
id: `candidate-${index}`,
|
|
317
|
+
role: 'candidate',
|
|
318
|
+
label: `Composition ${index}`,
|
|
319
|
+
color: '#315f7d',
|
|
320
|
+
pnl: index < 4 ? 160 : index * 30,
|
|
321
|
+
trades: 28,
|
|
322
|
+
drawdown: index < 4 ? 26 : index * 7,
|
|
323
|
+
artifactSha,
|
|
324
|
+
}),
|
|
325
|
+
);
|
|
326
|
+
}
|
|
327
|
+
for (const candidate of spec.candidates) {
|
|
328
|
+
for (const artifact of [
|
|
329
|
+
'coreResult',
|
|
330
|
+
'coreExport',
|
|
331
|
+
'gateReport',
|
|
332
|
+
'gateAuthority',
|
|
333
|
+
]) {
|
|
334
|
+
const reference = candidate.composition[artifact];
|
|
335
|
+
if (reference)
|
|
336
|
+
await writeFile(path.join(root, reference.path), contents);
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
const { summary } = await generateFinalCompositionBoard({
|
|
340
|
+
spec,
|
|
341
|
+
artifactRoot: root,
|
|
342
|
+
outDir: path.join(root, 'charts'),
|
|
343
|
+
});
|
|
344
|
+
assert.deepEqual(
|
|
345
|
+
summary.candidates.map(({ metrics }) => metrics),
|
|
346
|
+
spec.candidates.map(({ metrics }) => metrics),
|
|
347
|
+
);
|
|
348
|
+
const equity = await readFile(
|
|
349
|
+
path.join(root, 'charts', 'final-composition-equity.svg'),
|
|
350
|
+
'utf8',
|
|
351
|
+
);
|
|
352
|
+
const height = Number(/<svg[^>]* height="(\d+)"/u.exec(equity)[1]);
|
|
353
|
+
const legends = [
|
|
354
|
+
...equity.matchAll(
|
|
355
|
+
/data-equity-legend="([^"]+)" transform="translate\([^,]+,([\d.]+)\)"/gu,
|
|
356
|
+
),
|
|
357
|
+
];
|
|
358
|
+
assert.equal(legends.length, candidateCount);
|
|
359
|
+
for (const [, id, top] of legends) {
|
|
360
|
+
assert.ok(
|
|
361
|
+
Number(top) + 42 < height - 24,
|
|
362
|
+
`${id}: legend label and metrics must fit with bottom margin`,
|
|
363
|
+
);
|
|
364
|
+
}
|
|
365
|
+
const { default: sharp } = await import('sharp');
|
|
366
|
+
const image = sharp(
|
|
367
|
+
await readFile(path.join(root, 'charts', 'final-composition-equity.png')),
|
|
368
|
+
);
|
|
369
|
+
assert.equal((await image.metadata()).height, height);
|
|
370
|
+
const lastRowTop = Number(legends.at(-1)[2]);
|
|
371
|
+
const pixels = await image
|
|
372
|
+
.extract({ left: 110, top: lastRowTop, width: 500, height: 42 })
|
|
373
|
+
.stats();
|
|
374
|
+
assert.ok(
|
|
375
|
+
pixels.channels.some(({ min }) => min < 200),
|
|
376
|
+
'Last legend row must be painted inside the PNG',
|
|
377
|
+
);
|
|
378
|
+
const dashboard = await readFile(
|
|
379
|
+
path.join(root, 'charts', 'final-composition-dashboard.svg'),
|
|
380
|
+
'utf8',
|
|
381
|
+
);
|
|
382
|
+
const points = [
|
|
383
|
+
...dashboard.matchAll(
|
|
384
|
+
/data-scatter-point="([^"]+)" cx="([^"]+)" cy="([^"]+)"/gu,
|
|
385
|
+
),
|
|
386
|
+
];
|
|
387
|
+
assert.equal(points.length, candidateCount);
|
|
388
|
+
const coincident = points.filter(([, id]) =>
|
|
389
|
+
['candidate', 'candidate-2', 'candidate-3'].includes(id),
|
|
390
|
+
);
|
|
391
|
+
assert.equal(
|
|
392
|
+
new Set(coincident.map(([, , x, y]) => `${x},${y}`)).size,
|
|
393
|
+
1,
|
|
394
|
+
'Coincident data points must not be jittered',
|
|
395
|
+
);
|
|
396
|
+
assert.match(
|
|
397
|
+
dashboard,
|
|
398
|
+
/data-point-label-ids="candidate,candidate-2,candidate-3"[^>]*>A \/ B \/ C<\/text>/u,
|
|
399
|
+
);
|
|
400
|
+
assert.equal(
|
|
401
|
+
[...dashboard.matchAll(/data-point-label-ids=/gu)].length,
|
|
402
|
+
candidateCount - 2,
|
|
403
|
+
);
|
|
404
|
+
});
|
|
405
|
+
}
|
|
@@ -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": "f31f221397fa300534800c147d1ea57e549627083723acc7c97b75364d31da11",
|
|
5
5
|
"skills": [
|
|
6
6
|
"ai-train-local-research",
|
|
7
7
|
"backtest-config-redis",
|
|
@@ -19,10 +19,11 @@
|
|
|
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": "afc8334de0282fd0fac3886f467a1fca8e67c624a48cb5de66351635a039421e",
|
|
23
|
+
".codex/skills/ai-train-local-research/references/pocket-feature-exclusion.md": "c7af65fcfcd404d5324f0222678126d5e166fb77c4c27afea96369ef427f4bb0",
|
|
23
24
|
".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": "
|
|
25
|
+
".codex/skills/ai-train-local-research/scripts/ai-gate-ablation.mjs": "f68a1a7c898f4c2f46860709a52cabd3abd4cc8cd14443de2b5f827d261be935",
|
|
26
|
+
".codex/skills/ai-train-local-research/scripts/ai-gate-ablation.test.mjs": "f13d2284a4c04a5220588652be81d104d2acde9650481cf569fa0efe56118f08",
|
|
26
27
|
".codex/skills/ai-train-local-research/SKILL.md": "479e44aad9cb6ab6b5c8931c807c63cf2a1938a59d01f553c20f44645716a23a",
|
|
27
28
|
".codex/skills/backtest-config-redis/scripts/get_backtest_config.sh": "833e950d6348c5b7a4bd3f00d25af60f6394190bff744bd5ab64db9e43d826d5",
|
|
28
29
|
".codex/skills/backtest-config-redis/SKILL.md": "85e7b1fa425aa23dfa7950de05c1d3ce046d044dc3872f3665705e9e2f2a1977",
|
|
@@ -41,11 +42,11 @@
|
|
|
41
42
|
".codex/skills/strategy-forward-start/SKILL.md": "a86311aeb4c5b46e43011806ffa0f665c4ec0fddce8d7f8c1232184e4a14bfa7",
|
|
42
43
|
".codex/skills/strategy-forward-status/SKILL.md": "b41f117680259fd7b4d5cbe3c0a1be75a42f08c8cf78f9ad7ccb0d90bfc66269",
|
|
43
44
|
".codex/skills/strategy-improvement-plan/SKILL.md": "f5f0f390941dd3332890a2f0ffae1248abde4925db2cf49a41c0c0c1b47e7864",
|
|
44
|
-
".codex/skills/strategy-improvement-research/references/final-composition-board.md": "
|
|
45
|
+
".codex/skills/strategy-improvement-research/references/final-composition-board.md": "bc88878e088744ff933f06b54f73af96231d1f22dc162a44c9eb2e81d6cb51aa",
|
|
45
46
|
".codex/skills/strategy-improvement-research/scripts/build-final-composition-spec.mjs": "b51873691fafbcf2dd70d8c9a6da842fec5f8358346c632cfe79211db08b18ba",
|
|
46
47
|
".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": "
|
|
48
|
+
".codex/skills/strategy-improvement-research/scripts/final-composition-board.mjs": "c7834c9d567d3f02c39fcca2b0116a0097bc0384891a5185edbcafa23eeea1db",
|
|
49
|
+
".codex/skills/strategy-improvement-research/scripts/final-composition-board.test.mjs": "14572bbb8083f83b7e05b6dad2a9c93a88c212425b09cc31ee15ab7a684ea8c5",
|
|
49
50
|
".codex/skills/strategy-improvement-research/scripts/freeze-gate-variants.mjs": "17b3c868880ddd5b326d309e6e42eaeef1ea22d625a5db947997ac73b221ad6d",
|
|
50
51
|
".codex/skills/strategy-improvement-research/scripts/freeze-gate-variants.test.mjs": "963e1c64ab3deef11fd48a96e10f3827af38036dfe2eb28daf7849f19dd414d8",
|
|
51
52
|
".codex/skills/strategy-improvement-research/SKILL.md": "6964eb154e40b0b775aa2735b2ebbe998984479f46333ed03eab88d2ee150d48",
|