create-tradejs 3.1.24 → 3.1.25

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.
@@ -116,6 +116,10 @@ set, pass `--spec path/to/variants.json`:
116
116
  }
117
117
  ```
118
118
 
119
+ JSON reports include timestamp-grouped cumulative `equity` arrays for the
120
+ current-gate baseline and every variant. Use these checksum-bound arrays for
121
+ final-composition charts instead of reconstructing curves by hand.
122
+
119
123
  ## Expression Grammar
120
124
 
121
125
  Expressions support parentheses, `&&`, `||`, and comparisons:
@@ -151,6 +155,11 @@ Plain `--testSplit` exposes test metrics and cannot be called untouched after
151
155
  the report is read. Open the sealed tail once with the frozen fixed-rule
152
156
  ablation.
153
157
 
158
+ When several core candidates must be compared, pass the same exact UTC
159
+ `--tuningSince` and `--testSince` boundaries to every candidate ablation.
160
+ Exact boundaries take precedence over ratio splits and keep sparse candidates
161
+ on one calendar partition contract.
162
+
154
163
  ## Cross-Strategy Feasibility
155
164
 
156
165
  Use `--crossStrategy` to test whether the latest merged export for every
@@ -34,6 +34,8 @@ Options:
34
34
  --terminalWindows <list> Terminal windows in days (default: 180,90,30,7)
35
35
  --validationSplit <ratio> Trailing timestamp-grouped tuning share (default: 0.25)
36
36
  --testSplit <ratio> Later timestamp-grouped test share (default: 0)
37
+ --tuningSince <timestamp> Exact UTC boundary where tuning starts
38
+ --testSince <timestamp> Exact UTC boundary where test starts
37
39
  --capacities <list> Capacity stress limits (default: 1,3,5)
38
40
  --maxLossValue <n> Per-order loss budget for capacity stress
39
41
  --featurePattern <regex> Inventory matching causal feature paths
@@ -89,6 +91,8 @@ export const parseCliArgs = (argv) => {
89
91
  terminalWindows: DEFAULT_WINDOWS,
90
92
  validationSplit: 0.25,
91
93
  testSplit: 0,
94
+ tuningSince: null,
95
+ testSince: null,
92
96
  capacities: DEFAULT_CAPACITIES,
93
97
  maxLossValue: null,
94
98
  variants: [],
@@ -161,6 +165,13 @@ export const parseCliArgs = (argv) => {
161
165
  options.testSplit = Number.isFinite(parsed)
162
166
  ? Math.max(0, Math.min(0.9, parsed))
163
167
  : 0;
168
+ } else if (name === 'tuningSince' || name === 'testSince') {
169
+ const numeric = Number(value);
170
+ const parsed = Number.isFinite(numeric) ? numeric : Date.parse(value);
171
+ if (!Number.isFinite(parsed)) {
172
+ throw new Error(`Invalid timestamp for --${name}: ${value}`);
173
+ }
174
+ options[name] = parsed;
164
175
  } else if (name === 'capacities') {
165
176
  options.capacities = parseNumberList(value, DEFAULT_CAPACITIES);
166
177
  } else if (name === 'maxLossValue') {
@@ -1016,6 +1027,28 @@ export const splitRowsByTimestamp = (rows, validationSplit, testSplit = 0) => {
1016
1027
  };
1017
1028
  };
1018
1029
 
1030
+ export const splitRowsByTimestampBounds = (
1031
+ rows,
1032
+ tuningSince,
1033
+ testSince,
1034
+ ) => {
1035
+ if (!Number.isFinite(tuningSince) || !Number.isFinite(testSince)) {
1036
+ throw new Error(
1037
+ 'Exact calendar partitions require both tuningSince and testSince',
1038
+ );
1039
+ }
1040
+ if (tuningSince >= testSince) {
1041
+ throw new Error('tuningSince must be earlier than testSince');
1042
+ }
1043
+ return {
1044
+ train: rows.filter((row) => row.timestamp < tuningSince),
1045
+ tuning: rows.filter(
1046
+ (row) => row.timestamp >= tuningSince && row.timestamp < testSince,
1047
+ ),
1048
+ test: rows.filter((row) => row.timestamp >= testSince),
1049
+ };
1050
+ };
1051
+
1019
1052
  const summarizeSplit = (rows, selector, summaryOptions) =>
1020
1053
  summarizeRows(selectRows(rows, selector), getPeriodDays(rows), {
1021
1054
  ...summaryOptions,
@@ -1034,6 +1067,34 @@ const summarizeDirections = (rows, selector, summaryOptions) =>
1034
1067
  ]),
1035
1068
  );
1036
1069
 
1070
+ export const buildEquitySeries = (
1071
+ rows,
1072
+ selector,
1073
+ minTimestamp = rows[0]?.timestamp ?? null,
1074
+ maxTimestamp = rows.at(-1)?.timestamp ?? null,
1075
+ ) => {
1076
+ if (!Number.isFinite(minTimestamp) || !Number.isFinite(maxTimestamp)) {
1077
+ return [];
1078
+ }
1079
+ const byTimestamp = new Map();
1080
+ for (const row of rows) {
1081
+ if (!selector(row)) continue;
1082
+ byTimestamp.set(
1083
+ row.timestamp,
1084
+ (byTimestamp.get(row.timestamp) ?? 0) + row.profit,
1085
+ );
1086
+ }
1087
+ let cumulative = 0;
1088
+ const result = [[minTimestamp, 0]];
1089
+ for (const [timestamp, profit] of byTimestamp) {
1090
+ cumulative += profit;
1091
+ if (timestamp === minTimestamp) result[0] = [timestamp, cumulative];
1092
+ else result.push([timestamp, cumulative]);
1093
+ }
1094
+ if (result.at(-1)[0] !== maxTimestamp) result.push([maxTimestamp, cumulative]);
1095
+ return result;
1096
+ };
1097
+
1037
1098
  const buildPeriodDirectionSummaries = ({
1038
1099
  rows,
1039
1100
  selector,
@@ -3778,6 +3839,8 @@ export const buildAblationReport = ({
3778
3839
  terminalWindows,
3779
3840
  validationSplit,
3780
3841
  testSplit = 0,
3842
+ tuningSince = null,
3843
+ testSince = null,
3781
3844
  capacities = DEFAULT_CAPACITIES,
3782
3845
  maxLossValue = null,
3783
3846
  filePaths,
@@ -3790,7 +3853,15 @@ export const buildAblationReport = ({
3790
3853
  if (!rows.length) throw new Error('No rows were evaluated');
3791
3854
  const minTimestamp = rows[0].timestamp;
3792
3855
  const maxTimestamp = rows.at(-1).timestamp;
3793
- const split = splitRowsByTimestamp(rows, validationSplit, testSplit);
3856
+ if ((tuningSince == null) !== (testSince == null)) {
3857
+ throw new Error(
3858
+ 'Exact calendar partitions require both tuningSince and testSince',
3859
+ );
3860
+ }
3861
+ const exactCalendarPartitions = tuningSince != null;
3862
+ const split = exactCalendarPartitions
3863
+ ? splitRowsByTimestampBounds(rows, tuningSince, testSince)
3864
+ : splitRowsByTimestamp(rows, validationSplit, testSplit);
3794
3865
  const partitionEvidence = (partitionRows) => ({
3795
3866
  rows: partitionRows.length,
3796
3867
  events: new Set(partitionRows.map((row) => row.timestamp)).size,
@@ -3804,6 +3875,12 @@ export const buildAblationReport = ({
3804
3875
  const summaryOptions = { capacities, maxLossValue };
3805
3876
  const baselineSelector = (row) => baselineSelectedAt(row, minQuality);
3806
3877
  const baseline = {
3878
+ equity: buildEquitySeries(
3879
+ rows,
3880
+ baselineSelector,
3881
+ minTimestamp,
3882
+ maxTimestamp,
3883
+ ),
3807
3884
  periods: buildPeriodSummaries({
3808
3885
  rows,
3809
3886
  selector: baselineSelector,
@@ -3849,6 +3926,12 @@ export const buildAblationReport = ({
3849
3926
  quality: variant.quality,
3850
3927
  direction: variant.direction,
3851
3928
  expression: variant.expression,
3929
+ equity: buildEquitySeries(
3930
+ rows,
3931
+ candidateSelector,
3932
+ minTimestamp,
3933
+ maxTimestamp,
3934
+ ),
3852
3935
  periods: buildPeriodSummaries({
3853
3936
  rows,
3854
3937
  selector: candidateSelector,
@@ -3919,6 +4002,13 @@ export const buildAblationReport = ({
3919
4002
  terminalWindows,
3920
4003
  validationSplit,
3921
4004
  testSplit,
4005
+ partitionMode: exactCalendarPartitions ? 'exact-calendar' : 'ratio',
4006
+ tuningSince: exactCalendarPartitions
4007
+ ? new Date(tuningSince).toISOString()
4008
+ : null,
4009
+ testSince: exactCalendarPartitions
4010
+ ? new Date(testSince).toISOString()
4011
+ : null,
3922
4012
  capacities,
3923
4013
  maxLossValue,
3924
4014
  trainRows: split.train.length,
@@ -4935,6 +5025,11 @@ export const main = async () => {
4935
5025
  sourceRepositoryRoot,
4936
5026
  );
4937
5027
  if (options.crossStrategy) {
5028
+ if (options.tuningSince != null || options.testSince != null) {
5029
+ throw new Error(
5030
+ '--tuningSince/--testSince are supported by candidate ablation only',
5031
+ );
5032
+ }
4938
5033
  const groups = latestDatasetGroupsByStrategy(
4939
5034
  await listDatasetGroups(outDir),
4940
5035
  );
@@ -5037,6 +5132,8 @@ export const main = async () => {
5037
5132
  terminalWindows: options.terminalWindows,
5038
5133
  validationSplit: options.validationSplit,
5039
5134
  testSplit: options.testSplit,
5135
+ tuningSince: options.tuningSince,
5136
+ testSince: options.testSince,
5040
5137
  capacities: options.capacities,
5041
5138
  maxLossValue: options.maxLossValue,
5042
5139
  sourceRepositoryRoot,
@@ -11,6 +11,7 @@ import {
11
11
  balanceCrossStrategyRows,
12
12
  buildAblationReport,
13
13
  buildCrossStrategyReport,
14
+ buildEquitySeries,
14
15
  buildMovingAverageVariants,
15
16
  calculateMovingAverageGrid,
16
17
  buildShiftedProfitLookups,
@@ -35,6 +36,7 @@ import {
35
36
  partitionCrossStrategyFeatures,
36
37
  resolveArtifactProjectRoot,
37
38
  splitRowsByTimestamp,
39
+ splitRowsByTimestampBounds,
38
40
  summarizeRows,
39
41
  summarizeMovingAverageRedundancy,
40
42
  } from './ai-gate-ablation.mjs';
@@ -195,6 +197,8 @@ test('parses repeated variants and research windows', () => {
195
197
  '--qualityThresholds',
196
198
  '4,5',
197
199
  '--testSplit=0.2',
200
+ '--tuningSince=2025-01-01T00:00:00.000Z',
201
+ '--testSince=2025-07-01T00:00:00.000Z',
198
202
  '--capacities=1,3,5',
199
203
  '--maxLossValue=0.2',
200
204
  ]);
@@ -207,6 +211,8 @@ test('parses repeated variants and research windows', () => {
207
211
  assert.deepEqual(options.terminalWindows, [180, 90, 30, 7]);
208
212
  assert.deepEqual(options.qualityThresholds, [4, 5]);
209
213
  assert.equal(options.testSplit, 0.2);
214
+ assert.equal(options.tuningSince, Date.UTC(2025, 0, 1));
215
+ assert.equal(options.testSince, Date.UTC(2025, 6, 1));
210
216
  assert.deepEqual(options.capacities, [1, 3, 5]);
211
217
  assert.equal(options.maxLossValue, 0.2);
212
218
  });
@@ -1048,6 +1054,46 @@ test('groups split and fan-out metrics by decision timestamp', () => {
1048
1054
  );
1049
1055
  });
1050
1056
 
1057
+ test('uses exact calendar boundaries without splitting timestamp events', () => {
1058
+ const train = Date.UTC(2025, 0, 1);
1059
+ const tuning = Date.UTC(2025, 3, 1);
1060
+ const testStart = Date.UTC(2025, 6, 1);
1061
+ const rows = [
1062
+ { timestamp: train, id: 'train' },
1063
+ { timestamp: tuning, id: 'tuning-a' },
1064
+ { timestamp: tuning, id: 'tuning-b' },
1065
+ { timestamp: testStart, id: 'test' },
1066
+ ];
1067
+
1068
+ const split = splitRowsByTimestampBounds(rows, tuning, testStart);
1069
+
1070
+ assert.deepEqual(split.train.map((row) => row.id), ['train']);
1071
+ assert.deepEqual(split.tuning.map((row) => row.id), [
1072
+ 'tuning-a',
1073
+ 'tuning-b',
1074
+ ]);
1075
+ assert.deepEqual(split.test.map((row) => row.id), ['test']);
1076
+ });
1077
+
1078
+ test('builds timestamp-grouped cumulative equity with common endpoints', () => {
1079
+ const start = Date.UTC(2025, 0, 1);
1080
+ const end = Date.UTC(2025, 0, 3);
1081
+ const rows = [
1082
+ { timestamp: start, profit: 2, keep: true },
1083
+ { timestamp: start, profit: -1, keep: true },
1084
+ { timestamp: Date.UTC(2025, 0, 2), profit: 10, keep: false },
1085
+ { timestamp: end, profit: 3, keep: true },
1086
+ ];
1087
+
1088
+ assert.deepEqual(
1089
+ buildEquitySeries(rows, (row) => row.keep, start, end),
1090
+ [
1091
+ [start, 1],
1092
+ [end, 4],
1093
+ ],
1094
+ );
1095
+ });
1096
+
1051
1097
  test('builds full and terminal period comparisons for a candidate', () => {
1052
1098
  const start = Date.UTC(2025, 0, 1);
1053
1099
  const variants = [parseVariant('keep::filter[SHORT]::feature.keep == true')];
@@ -113,14 +113,15 @@ and PnL reconcile within the documented per-symbol rounding tolerance, use the
113
113
  row-level export as the authoritative trade-economic total instead of swapping
114
114
  in the cent-rounded Redis aggregate.
115
115
 
116
- For a `$strategy-improvement-research` final composition, extend the same permanent report
116
+ For every `$strategy-improvement-research` final composition, extend the same permanent report
117
117
  to `1095d/1460d/1825d-or-exact-maximum/365d/180d/90d/30d/7d`. When cached
118
118
  coverage is shorter than 1825 days, report the exact covered duration (for
119
119
  example 1800d) and do not label it a complete five-year window. Reuse this
120
120
  tool's full ALL/LONG/SHORT statistics; do not replace them with a compact custom
121
- parser. The release workflow must then run `ai-train --localOnly --chart -n 0`
122
- on the exact full export so the UI chart and structured gate statistics share
123
- the finalist lineage.
121
+ parser. The improvement workflow then builds one independently verified
122
+ deterministic gate on each exact core export, including the baseline export, and
123
+ compares only the resulting `core + own gate` compositions. Raw-core metrics
124
+ remain diagnostic and must never be mixed into the final-composition chart.
124
125
 
125
126
  ## Required Core Metric Cohorts
126
127
 
@@ -51,10 +51,16 @@ Do not infer production from Redis.
51
51
  Stop before mutation if the target binding is ambiguous, credentials/registry
52
52
  authorization is missing, the candidate is not reproducible or implementable,
53
53
  the maximum-covered historical edge is non-positive, required evidence/chart
54
- hashes are missing, required checks fail, another rollout is active, or safe
55
- atomic deployment is unavailable. These are operational or falsifiability
56
- boundaries and operator-directed mode does not waive them. Give the exact
57
- command or UI boundary the user must complete; never start an interactive
54
+ hashes are missing, required checks fail, another rollout for the same strategy
55
+ is active, a deployment or rollout cutover on the target is still in flight, or
56
+ safe atomic deployment is unavailable. An active risk-1 forward test for a
57
+ different strategy on the same deployment or account is not a blocker: preserve
58
+ its declaration and evidence, and do not confuse the presence of its active
59
+ rollout ledger with an in-flight cutover. Multiple strategies may intentionally
60
+ run concurrent forward tests, including when the operator uses
61
+ `MAX_LOSS_VALUE=1` as the forward-test marker. These operational and
62
+ falsifiability boundaries are not waived by operator-directed mode. Give the
63
+ exact command or UI boundary the user must complete; never start an interactive
58
64
  authentication flow.
59
65
 
60
66
  ## Release and configure
@@ -48,11 +48,17 @@ hypothesis-family choice, trial budget, parent/child decisions, candidate
48
48
  selection, and final handoff. Delegate each preregistered core implementation
49
49
  and backtest to `$strategy-backtest-research`; that skill returns reconciled
50
50
  experiment evidence and does not choose the next candidate. After freezing the
51
- core finalist/export, delegate deterministic-gate analysis to
52
- `$ai-train-local-research`; it must not reopen core selection. Read those
51
+ complete core board, delegate one independent deterministic-gate board for the
52
+ baseline and for every complete, reconciled, behavior-changing core candidate
53
+ to `$ai-train-local-research`; it must not reopen core selection. Read those
53
54
  specialist skills when their stage begins instead of duplicating their command,
54
55
  metric, or reporting contracts here.
55
56
 
57
+ Before preregistration, read
58
+ [`references/final-composition-board.md`](references/final-composition-board.md).
59
+ It defines candidate-specific gate isolation, common partitions, the final
60
+ composition ledger, and the mandatory visual artifacts.
61
+
56
62
  ## Required contour
57
63
 
58
64
  1. Start a new immutable research lineage. Freeze data bounds, point-in-time
@@ -74,15 +80,36 @@ metric, or reporting contracts here.
74
80
  tail. Stop only when a reproducible best candidate is frozen, the fresh
75
81
  budget is exhausted, or every remaining family has a recorded hard causal
76
82
  blocker.
77
- 6. Keep one chronological tail sealed during discovery when coverage permits.
78
- Open it once for the final selected behavior. Track all exposed tests for
79
- multiple-testing/deflated-Sharpe interpretation.
80
- 7. Freeze the selected core/export before opening deterministic-gate research.
81
- Run the gate stage through `$ai-train-local-research`; do not retune or
82
- relabel the raw-core result inside gate tooling.
83
- 8. Run package formatting, typecheck, tests, and build in the lineage worktree.
84
- Commit only the selected candidate and its tests on that worktree branch;
85
- preserve rejected experiments as immutable evidence, not source clutter.
83
+ 6. Keep one common chronological core tail sealed during core discovery when
84
+ coverage permits. Freeze the complete raw-core board before any gate result
85
+ may influence core-family selection. Track all exposed core and gate tests
86
+ for multiple-testing/deflated-Sharpe interpretation.
87
+ 7. Freeze an acceptance-grade export for the baseline and every complete,
88
+ reconciled, behavior-changing core candidate. Give each export its own gate
89
+ discovery and exactly one selected deterministic gate; never reuse another
90
+ core's gate metrics or compare a gated candidate with an ungated baseline.
91
+ On the production core export, also replay the exact current production
92
+ AI-gate and bind it to its checksum-verified gate-authority report. This
93
+ `production core + current AI-gate` composition is the mandatory baseline;
94
+ the production core with its newly rebuilt gate is a separate research
95
+ candidate and must not replace the current-gate baseline.
96
+ Use common calendar train/tuning/test boundaries and open every sealed gate
97
+ tail together only after all per-core gate variants are frozen. Run every
98
+ gate board through `$ai-train-local-research`; do not retune or relabel the
99
+ raw-core result inside gate tooling.
100
+ 8. Build the final leaderboard from the mandatory `production core + current
101
+ AI-gate` baseline and `core + own deterministic gate` research candidates,
102
+ including `production core + rebuilt gate` as its own candidate. Preserve
103
+ raw-core metrics in a separate diagnostic table. Apply the final selection
104
+ rules and multiple-testing denominator to the complete core-by-gate trial
105
+ ledger, not only to the winning core or gate.
106
+ 9. Generate and checksum the mandatory final-composition dashboard and
107
+ cumulative-equity chart with the permanent script from the referenced
108
+ contract. A metrics-only handoff or a chart mixing raw and gated candidates
109
+ is incomplete.
110
+ 10. Run package formatting, typecheck, tests, and build in the lineage worktree.
111
+ Commit only the selected candidate and its tests on that worktree branch;
112
+ preserve rejected experiments as immutable evidence, not source clutter.
86
113
 
87
114
  ## Selection objective
88
115
 
@@ -102,3 +129,12 @@ source/package SHA, full resolved core config, deterministic gate/context,
102
129
  direction policy, evidence hashes, trial count, metric matrix, chart, freshness,
103
130
  limitations, and forward-test eligibility. End with the selected candidate,
104
131
  why it beat production and prior candidates, and exactly one next skill.
132
+
133
+ The handoff must link both visual artifacts for the final compositions:
134
+
135
+ - `final-composition-dashboard.{svg,png}` with KPI deltas against the exact
136
+ production-core + current-AI-gate baseline, terminal
137
+ `365d/180d/90d/30d/7d` PnL, and the all-candidate PnL-versus-MaxDD plane;
138
+ - `final-composition-equity.{svg,png}` with cumulative PnL for the gated
139
+ production baseline, the rebuilt production-core gate, and every final
140
+ `core + own gate` composition over the common comparison window.
@@ -0,0 +1,188 @@
1
+ # Final composition board
2
+
3
+ Use this contract after raw-core discovery is complete. Its purpose is to make
4
+ the research unit an executable strategy composition, not an intermediate core
5
+ or a gate tuned on another core's signal population.
6
+
7
+ ## Gate isolation
8
+
9
+ 1. Freeze the raw-core board first. It contains the authoritative baseline and
10
+ every complete, reconciled, behavior-changing core candidate. Core family
11
+ continuation and rescue choices use only raw-core evidence; a gate result
12
+ cannot reopen or rewrite that board.
13
+ 2. Produce one acceptance-grade raw AI export for every board member, including
14
+ the baseline. Bind its core research ID, resolved-config SHA-256, source diff
15
+ or commit, export SHA-256, window, universe, execution assumptions, and
16
+ context lineage.
17
+ 3. Run `$ai-train-local-research` independently for every export. Each core gets
18
+ its own current-gate control, pocket discovery, five-variant maximum ablation,
19
+ selected gate expression or implementation, full structured report, and
20
+ fingerprint. On the production core export, preserve two distinct final
21
+ compositions: the exact current AI-gate replay, checksum-bound to its
22
+ gate-authority report, and the independently rebuilt gate. The current gate
23
+ is the baseline; the rebuilt gate is a research candidate. A no-op,
24
+ zero-approval, or rejected gate still receives a checksum-bound report and
25
+ explicit disposition; it is not silently omitted.
26
+ 4. Freeze common calendar train, tuning, and test boundaries once in the parent
27
+ lineage. Keep all rows sharing a timestamp in one partition. Candidate
28
+ exports may have different row counts, but final comparison windows and test
29
+ timestamps must not move to make one candidate look better. Discovery may
30
+ inspect only train and tuning. Open all candidate test tails together after
31
+ every gate spec is immutable.
32
+ Use `ai-gate-ablation.mjs --tuningSince <UTC> --testSince <UTC>` with the
33
+ same values for every candidate; ratio-only splits are not comparable when
34
+ core event cadence differs.
35
+ 5. Count every inspected core and gate behavior in the final trial ledger. The
36
+ selected strategy is `core identity + candidate-specific gate identity +
37
+ context identity + direction policy + quality threshold`. Reusing economics
38
+ from another core, using the production gate without replaying it on the new
39
+ export, or comparing a gated candidate with raw baseline is invalid.
40
+
41
+ The five gate slots are a hard per-core maximum, not a target. Prefer a compact
42
+ causal board: current gate, explicit direction pass-through/block when the raw
43
+ side evidence warrants it, one causal pocket, one protected pocket, and one
44
+ direction-aware replacement. Do not fill slots with adjacent threshold nudges.
45
+
46
+ Freeze independently discovered causal pockets into an auditable candidate
47
+ spec before opening comparison evidence:
48
+
49
+ ```bash
50
+ node .codex/skills/strategy-improvement-research/scripts/freeze-gate-variants.mjs \
51
+ --pocket data/ai/output/<candidate>-pocket.json \
52
+ --candidateId <candidate> --output data/ai/output/<candidate>-gate-spec.json
53
+ ```
54
+
55
+ ## Final comparison
56
+
57
+ Build two separate tables:
58
+
59
+ - **Raw-core diagnostics**: baseline and every raw core with the fixed
60
+ ALL/LONG/SHORT contract. These metrics explain the mechanism but never enter
61
+ the final strategy leaderboard as if they were deployable compositions.
62
+ - **Final compositions**: the exact production core + current AI-gate baseline,
63
+ the production core + rebuilt gate candidate, and exactly one frozen own-gate
64
+ result for every other raw-core candidate. Apply economic validity, support,
65
+ drawdown, cadence, concentration, stability, cost, and holdout rules here.
66
+
67
+ Normalize PnL and drawdown to one recorded `MAX_LOSS_VALUE` risk unit before
68
+ ranking or charting. Use the same half-open comparison window and terminal
69
+ `365d`, `180d`, `90d`, `30d`, and `7d` slices for every composition, including
70
+ zero-trade windows. A gate without sufficient untouched support remains visible
71
+ as `research-only` or `blocked`; the chart is not evidence of readiness.
72
+
73
+ ## Mandatory visual artifacts
74
+
75
+ Create the board from authoritative structured reports with:
76
+
77
+ ```bash
78
+ node .codex/skills/strategy-improvement-research/scripts/build-final-composition-spec.mjs \
79
+ --selection data/strategy-release/<lineage>/final-composition-selection.json \
80
+ --output data/strategy-release/<lineage>/final-composition-board.json \
81
+ --terminalComparisonIds <selected-id>,<additional-candidate-id>
82
+ node .codex/skills/strategy-improvement-research/scripts/final-composition-board.mjs \
83
+ --spec data/strategy-release/<lineage>/final-composition-board.json \
84
+ --outDir data/strategy-release/<lineage>/final-composition-charts
85
+ ```
86
+
87
+ The input uses schema `tradejs-final-composition-board/v1`. Every composition
88
+ must declare `composition.kind = core+deterministic-gate` and `gateSource` as
89
+ `current` or `variant`, the same normalized risk unit, complete full/terminal
90
+ metrics, cumulative equity points, and three checksum-verified inputs: core
91
+ result, core export, and gate report. The baseline additionally requires the
92
+ checksum-verified current `gateAuthority`. The script recomputes those hashes,
93
+ requires `baselineId` to identify the current-gate composition, and derives the
94
+ composition fingerprint; do not paste an unverified fingerprint into the spec.
95
+ `--terminalComparisonIds` is an optional presentation override for a derived
96
+ board: it leaves `selectedId` unchanged and allows a versioned comparison chart
97
+ without rewriting the frozen selection artifact.
98
+
99
+ Required outputs:
100
+
101
+ - `final-composition-dashboard.svg` and `.png`: selected composition KPI cards
102
+ versus the production core + current AI-gate baseline, grouped terminal PnL bars with trade counts for
103
+ the baseline plus `terminalComparisonIds`, a PnL-versus-realized-MaxDD plot
104
+ of every final composition, and limitations. `terminalComparisonIds`
105
+ defaults to `[selectedId]`; it may include up to three candidate IDs, must
106
+ include `selectedId`, and does not change the research selection;
107
+ - `final-composition-equity.svg` and `.png`: cumulative PnL curves for the
108
+ current production composition and every research composition on the common
109
+ time axis;
110
+ - `final-composition-summary.json`: derived composition fingerprints, source
111
+ hashes, normalized metrics, and hashes of all four rendered files.
112
+
113
+ SVG is the canonical deterministic rendering. PNG is the chat-ready rendering
114
+ of the same SVG. Link or display both charts in the final answer and immutable
115
+ research note. Store the complete board spec and summary in Project-owned
116
+ evidence; paths alone do not replace the note's machine-readable metrics.
117
+
118
+ ## Spec shape
119
+
120
+ ```json
121
+ {
122
+ "schema": "tradejs-final-composition-board/v1",
123
+ "strategy": "ExampleStrategy",
124
+ "researchId": "example-improvement-20260825-v1",
125
+ "title": "ExampleStrategy final compositions",
126
+ "subtitle": "Common cache-only window; sealed test opened once",
127
+ "baselineId": "production-current-gate",
128
+ "selectedId": "candidate-a-own-gate",
129
+ "terminalComparisonIds": ["candidate-a-own-gate", "candidate-b-own-gate"],
130
+ "comparisonWindow": { "start": 1690848000000, "end": 1787616000000 },
131
+ "normalization": { "pnlUnit": "research PnL", "maxLossValue": 10 },
132
+ "limitations": ["Point-in-time universe is incomplete"],
133
+ "candidates": [
134
+ {
135
+ "id": "production-current-gate",
136
+ "label": "production core + current AI-gate",
137
+ "role": "baseline",
138
+ "status": "eligible",
139
+ "color": "#315f7d",
140
+ "riskUnit": 10,
141
+ "composition": {
142
+ "kind": "core+deterministic-gate",
143
+ "gateSource": "current",
144
+ "coreResearchId": "example-control",
145
+ "coreConfigSha256": "<sha256>",
146
+ "coreResult": {
147
+ "path": "data/research/core/example-control/result.json",
148
+ "sha256": "<sha256>"
149
+ },
150
+ "coreExport": {
151
+ "path": "data/ai/export/example.jsonl",
152
+ "sha256": "<sha256>"
153
+ },
154
+ "gateReport": {
155
+ "path": "data/ai/output/example-gate.json",
156
+ "sha256": "<sha256>"
157
+ },
158
+ "gateAuthority": {
159
+ "path": "data/ai/output/example-current-gate-authority.json",
160
+ "sha256": "<sha256>"
161
+ },
162
+ "gateFingerprint": "<sha256>",
163
+ "configFingerprint": "<sha256>",
164
+ "contextFingerprint": "<sha256>",
165
+ "directionPolicy": "both",
166
+ "minQuality": 4
167
+ },
168
+ "metrics": {
169
+ "trades": 100,
170
+ "pnl": 250,
171
+ "profitFactor": 1.5,
172
+ "maxDrawdown": 70
173
+ },
174
+ "terminal": [
175
+ { "days": 365, "trades": 40, "pnl": 120 },
176
+ { "days": 180, "trades": 20, "pnl": 60 },
177
+ { "days": 90, "trades": 10, "pnl": 30 },
178
+ { "days": 30, "trades": 3, "pnl": 8 },
179
+ { "days": 7, "trades": 1, "pnl": 2 }
180
+ ],
181
+ "equity": [
182
+ [1690848000000, 0],
183
+ [1787615999999, 250]
184
+ ]
185
+ }
186
+ ]
187
+ }
188
+ ```