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.
- package/dist/skill-bundle/.codex/skills/ai-train-local-research/references/gate-ablation.md +9 -0
- package/dist/skill-bundle/.codex/skills/ai-train-local-research/scripts/ai-gate-ablation.mjs +98 -1
- package/dist/skill-bundle/.codex/skills/ai-train-local-research/scripts/ai-gate-ablation.test.mjs +46 -0
- package/dist/skill-bundle/.codex/skills/strategy-backtest-research/SKILL.md +5 -4
- package/dist/skill-bundle/.codex/skills/strategy-forward-start/SKILL.md +10 -4
- package/dist/skill-bundle/.codex/skills/strategy-improvement-research/SKILL.md +47 -11
- package/dist/skill-bundle/.codex/skills/strategy-improvement-research/references/final-composition-board.md +188 -0
- package/dist/skill-bundle/.codex/skills/strategy-improvement-research/scripts/build-final-composition-spec.mjs +240 -0
- package/dist/skill-bundle/.codex/skills/strategy-improvement-research/scripts/build-final-composition-spec.test.mjs +159 -0
- package/dist/skill-bundle/.codex/skills/strategy-improvement-research/scripts/final-composition-board.mjs +825 -0
- package/dist/skill-bundle/.codex/skills/strategy-improvement-research/scripts/final-composition-board.test.mjs +223 -0
- package/dist/skill-bundle/.codex/skills/strategy-improvement-research/scripts/freeze-gate-variants.mjs +103 -0
- package/dist/skill-bundle/.codex/skills/strategy-improvement-research/scripts/freeze-gate-variants.test.mjs +52 -0
- package/dist/skill-bundle/.codex/tradejs-skill-bundle.json +14 -7
- package/package.json +1 -1
|
@@ -0,0 +1,240 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import crypto from 'node:crypto';
|
|
4
|
+
import fsp from 'node:fs/promises';
|
|
5
|
+
import path from 'node:path';
|
|
6
|
+
import { pathToFileURL } from 'node:url';
|
|
7
|
+
|
|
8
|
+
import { sha256File, stableStringify } from './final-composition-board.mjs';
|
|
9
|
+
|
|
10
|
+
const REQUIRED_WINDOWS = [365, 180, 90, 30, 7];
|
|
11
|
+
|
|
12
|
+
const sha256 = (value) =>
|
|
13
|
+
crypto.createHash('sha256').update(value).digest('hex');
|
|
14
|
+
|
|
15
|
+
const requiredText = (value, name) => {
|
|
16
|
+
if (typeof value !== 'string' || !value.trim()) {
|
|
17
|
+
throw new Error(`${name} must be a non-empty string`);
|
|
18
|
+
}
|
|
19
|
+
return value.trim();
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
export const buildFinalCompositionSpec = async ({
|
|
23
|
+
selection,
|
|
24
|
+
artifactRoot,
|
|
25
|
+
}) => {
|
|
26
|
+
if (selection?.schema !== 'tradejs-final-composition-selection/v1') {
|
|
27
|
+
throw new Error('Invalid final-composition selection schema');
|
|
28
|
+
}
|
|
29
|
+
const baselineSelection = selection.candidates?.find(
|
|
30
|
+
({ id }) => id === selection.baselineId,
|
|
31
|
+
);
|
|
32
|
+
if (!baselineSelection) {
|
|
33
|
+
throw new Error('baselineId must identify a selection candidate');
|
|
34
|
+
}
|
|
35
|
+
if (
|
|
36
|
+
baselineSelection.role !== 'baseline' ||
|
|
37
|
+
baselineSelection.gateSource !== 'current'
|
|
38
|
+
) {
|
|
39
|
+
throw new Error(
|
|
40
|
+
'baselineId must identify production core + current AI-gate with gateSource=current',
|
|
41
|
+
);
|
|
42
|
+
}
|
|
43
|
+
const root = path.resolve(artifactRoot);
|
|
44
|
+
const candidates = await Promise.all(
|
|
45
|
+
selection.candidates.map(async (candidate, index) => {
|
|
46
|
+
const prefix = `candidates[${index}]`;
|
|
47
|
+
const gateSource = candidate.gateSource ?? 'variant';
|
|
48
|
+
if (!['current', 'variant'].includes(gateSource)) {
|
|
49
|
+
throw new Error(`${prefix}.gateSource must be current or variant`);
|
|
50
|
+
}
|
|
51
|
+
if (gateSource === 'current' && candidate.id !== selection.baselineId) {
|
|
52
|
+
throw new Error(
|
|
53
|
+
`${prefix}.gateSource=current is reserved for baselineId`,
|
|
54
|
+
);
|
|
55
|
+
}
|
|
56
|
+
const gateReportPath = requiredText(
|
|
57
|
+
candidate.gateReport,
|
|
58
|
+
`${prefix}.gateReport`,
|
|
59
|
+
);
|
|
60
|
+
const report = JSON.parse(
|
|
61
|
+
await fsp.readFile(path.resolve(root, gateReportPath), 'utf8'),
|
|
62
|
+
);
|
|
63
|
+
const artifact = async (declaredPath) => ({
|
|
64
|
+
path: declaredPath,
|
|
65
|
+
sha256: await sha256File(path.resolve(root, declaredPath)),
|
|
66
|
+
});
|
|
67
|
+
const [coreResult, coreExport, gateReport] = await Promise.all([
|
|
68
|
+
artifact(candidate.coreResult),
|
|
69
|
+
artifact(candidate.coreExport),
|
|
70
|
+
artifact(gateReportPath),
|
|
71
|
+
]);
|
|
72
|
+
let gateAuthority;
|
|
73
|
+
let gateFingerprint;
|
|
74
|
+
let minQuality;
|
|
75
|
+
let variant;
|
|
76
|
+
if (gateSource === 'current') {
|
|
77
|
+
const gateAuthorityPath = requiredText(
|
|
78
|
+
candidate.gateAuthority,
|
|
79
|
+
`${prefix}.gateAuthority`,
|
|
80
|
+
);
|
|
81
|
+
gateAuthority = await artifact(gateAuthorityPath);
|
|
82
|
+
const authority = JSON.parse(
|
|
83
|
+
await fsp.readFile(path.resolve(root, gateAuthorityPath), 'utf8'),
|
|
84
|
+
);
|
|
85
|
+
const authoritySourceHashes =
|
|
86
|
+
authority.research?.lineage?.sourceSha256s ?? [];
|
|
87
|
+
if (!authoritySourceHashes.includes(coreExport.sha256)) {
|
|
88
|
+
throw new Error(
|
|
89
|
+
`${prefix}.gateAuthority is not bound to the declared core export`,
|
|
90
|
+
);
|
|
91
|
+
}
|
|
92
|
+
if (authority.run?.directionPolicy !== candidate.directionPolicy) {
|
|
93
|
+
throw new Error(
|
|
94
|
+
`${prefix}.gateAuthority directionPolicy does not match selection`,
|
|
95
|
+
);
|
|
96
|
+
}
|
|
97
|
+
variant = report.baseline;
|
|
98
|
+
if (!variant) {
|
|
99
|
+
throw new Error(`${prefix} gate report has no current-gate baseline`);
|
|
100
|
+
}
|
|
101
|
+
minQuality = authority.run?.minQuality ?? report.run?.minQuality ?? 4;
|
|
102
|
+
gateFingerprint = sha256(
|
|
103
|
+
stableStringify({
|
|
104
|
+
source: 'current-gate-authority',
|
|
105
|
+
authoritySha256: gateAuthority.sha256,
|
|
106
|
+
runtimeGateFingerprint:
|
|
107
|
+
authority.research?.lineage?.gateFingerprint ?? null,
|
|
108
|
+
mode: authority.run?.mode ?? null,
|
|
109
|
+
minQuality,
|
|
110
|
+
directionPolicy: candidate.directionPolicy,
|
|
111
|
+
}),
|
|
112
|
+
);
|
|
113
|
+
} else {
|
|
114
|
+
variant = report.variants?.find(
|
|
115
|
+
({ name }) => name === candidate.variantName,
|
|
116
|
+
);
|
|
117
|
+
if (!variant) {
|
|
118
|
+
throw new Error(`${prefix}.variantName is absent from gate report`);
|
|
119
|
+
}
|
|
120
|
+
minQuality = report.run?.minQuality ?? 4;
|
|
121
|
+
gateFingerprint = sha256(
|
|
122
|
+
stableStringify({
|
|
123
|
+
name: variant.name,
|
|
124
|
+
mode: variant.mode,
|
|
125
|
+
quality: variant.quality,
|
|
126
|
+
direction: variant.direction,
|
|
127
|
+
expression: variant.expression,
|
|
128
|
+
}),
|
|
129
|
+
);
|
|
130
|
+
}
|
|
131
|
+
const full = variant.periods?.full;
|
|
132
|
+
if (!full) throw new Error(`${prefix} gate report has no full period`);
|
|
133
|
+
return {
|
|
134
|
+
id: requiredText(candidate.id, `${prefix}.id`),
|
|
135
|
+
label: requiredText(candidate.label, `${prefix}.label`),
|
|
136
|
+
role: candidate.role,
|
|
137
|
+
status: candidate.status,
|
|
138
|
+
color: candidate.color,
|
|
139
|
+
riskUnit: selection.normalization.maxLossValue,
|
|
140
|
+
composition: {
|
|
141
|
+
kind: 'core+deterministic-gate',
|
|
142
|
+
gateSource,
|
|
143
|
+
coreResearchId: candidate.coreResearchId,
|
|
144
|
+
coreConfigSha256: candidate.coreConfigSha256,
|
|
145
|
+
coreResult,
|
|
146
|
+
coreExport,
|
|
147
|
+
gateReport,
|
|
148
|
+
...(gateAuthority === undefined ? {} : { gateAuthority }),
|
|
149
|
+
gateFingerprint,
|
|
150
|
+
configFingerprint: candidate.coreConfigSha256,
|
|
151
|
+
contextFingerprint: selection.contextFingerprint,
|
|
152
|
+
directionPolicy: candidate.directionPolicy,
|
|
153
|
+
minQuality,
|
|
154
|
+
},
|
|
155
|
+
metrics: {
|
|
156
|
+
trades: full.trades,
|
|
157
|
+
pnl: full.totalProfit,
|
|
158
|
+
profitFactor: full.profitFactor,
|
|
159
|
+
maxDrawdown: full.maxDrawdown,
|
|
160
|
+
},
|
|
161
|
+
terminal: REQUIRED_WINDOWS.map((days) => {
|
|
162
|
+
const period = variant.periods?.[`${days}d`];
|
|
163
|
+
if (!period) throw new Error(`${prefix} is missing ${days}d metrics`);
|
|
164
|
+
return { days, trades: period.trades, pnl: period.totalProfit };
|
|
165
|
+
}),
|
|
166
|
+
equity: variant.equity,
|
|
167
|
+
};
|
|
168
|
+
}),
|
|
169
|
+
);
|
|
170
|
+
return {
|
|
171
|
+
schema: 'tradejs-final-composition-board/v1',
|
|
172
|
+
strategy: selection.strategy,
|
|
173
|
+
researchId: selection.researchId,
|
|
174
|
+
title: selection.title,
|
|
175
|
+
subtitle: selection.subtitle,
|
|
176
|
+
baselineId: selection.baselineId,
|
|
177
|
+
selectedId: selection.selectedId,
|
|
178
|
+
...(selection.terminalComparisonIds === undefined
|
|
179
|
+
? {}
|
|
180
|
+
: { terminalComparisonIds: selection.terminalComparisonIds }),
|
|
181
|
+
comparisonWindow: selection.comparisonWindow,
|
|
182
|
+
normalization: selection.normalization,
|
|
183
|
+
limitations: selection.limitations,
|
|
184
|
+
candidates,
|
|
185
|
+
};
|
|
186
|
+
};
|
|
187
|
+
|
|
188
|
+
export const main = async (argv = process.argv.slice(2)) => {
|
|
189
|
+
const options = {};
|
|
190
|
+
for (let index = 0; index < argv.length; index += 1) {
|
|
191
|
+
const name = argv[index];
|
|
192
|
+
if (
|
|
193
|
+
![
|
|
194
|
+
'--selection',
|
|
195
|
+
'--output',
|
|
196
|
+
'--artifactRoot',
|
|
197
|
+
'--terminalComparisonIds',
|
|
198
|
+
].includes(name)
|
|
199
|
+
) {
|
|
200
|
+
throw new Error(`Unknown option: ${name}`);
|
|
201
|
+
}
|
|
202
|
+
const value = argv[++index];
|
|
203
|
+
if (!value || value.startsWith('--'))
|
|
204
|
+
throw new Error(`${name} requires a value`);
|
|
205
|
+
options[name.slice(2)] = value;
|
|
206
|
+
}
|
|
207
|
+
if (!options.selection || !options.output) {
|
|
208
|
+
throw new Error('Required: --selection and --output');
|
|
209
|
+
}
|
|
210
|
+
const artifactRoot = path.resolve(
|
|
211
|
+
options.artifactRoot || process.env.PROJECT_CWD || process.cwd(),
|
|
212
|
+
);
|
|
213
|
+
const selection = JSON.parse(
|
|
214
|
+
await fsp.readFile(path.resolve(artifactRoot, options.selection), 'utf8'),
|
|
215
|
+
);
|
|
216
|
+
const terminalComparisonIds = options.terminalComparisonIds
|
|
217
|
+
?.split(',')
|
|
218
|
+
.map((value) => value.trim())
|
|
219
|
+
.filter(Boolean);
|
|
220
|
+
const spec = await buildFinalCompositionSpec({
|
|
221
|
+
selection:
|
|
222
|
+
terminalComparisonIds === undefined
|
|
223
|
+
? selection
|
|
224
|
+
: { ...selection, terminalComparisonIds },
|
|
225
|
+
artifactRoot,
|
|
226
|
+
});
|
|
227
|
+
const outputPath = path.resolve(artifactRoot, options.output);
|
|
228
|
+
await fsp.mkdir(path.dirname(outputPath), { recursive: true });
|
|
229
|
+
await fsp.writeFile(outputPath, `${JSON.stringify(spec, null, 2)}\n`, 'utf8');
|
|
230
|
+
process.stdout.write(`${await sha256File(outputPath)} ${options.output}\n`);
|
|
231
|
+
};
|
|
232
|
+
|
|
233
|
+
if (
|
|
234
|
+
pathToFileURL(path.resolve(process.argv[1] ?? '')).href === import.meta.url
|
|
235
|
+
) {
|
|
236
|
+
main().catch((error) => {
|
|
237
|
+
console.error(error instanceof Error ? error.stack : String(error));
|
|
238
|
+
process.exitCode = 1;
|
|
239
|
+
});
|
|
240
|
+
}
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
import assert from 'node:assert/strict';
|
|
2
|
+
import { createHash } from 'node:crypto';
|
|
3
|
+
import fsp from 'node:fs/promises';
|
|
4
|
+
import os from 'node:os';
|
|
5
|
+
import path from 'node:path';
|
|
6
|
+
import test from 'node:test';
|
|
7
|
+
|
|
8
|
+
import { buildFinalCompositionSpec } from './build-final-composition-spec.mjs';
|
|
9
|
+
|
|
10
|
+
const sha256 = (value) => createHash('sha256').update(value).digest('hex');
|
|
11
|
+
|
|
12
|
+
const periods = (totalProfit) =>
|
|
13
|
+
Object.fromEntries(
|
|
14
|
+
['full', '365d', '180d', '90d', '30d', '7d'].map((key) => [
|
|
15
|
+
key,
|
|
16
|
+
{
|
|
17
|
+
trades: 1,
|
|
18
|
+
totalProfit,
|
|
19
|
+
profitFactor: 2,
|
|
20
|
+
maxDrawdown: 1,
|
|
21
|
+
},
|
|
22
|
+
]),
|
|
23
|
+
);
|
|
24
|
+
|
|
25
|
+
const makeSelection = (fingerprint) => ({
|
|
26
|
+
schema: 'tradejs-final-composition-selection/v1',
|
|
27
|
+
strategy: 'Example',
|
|
28
|
+
researchId: 'example-v1',
|
|
29
|
+
title: 'Example',
|
|
30
|
+
subtitle: 'Example',
|
|
31
|
+
baselineId: 'current-baseline',
|
|
32
|
+
selectedId: 'current-baseline',
|
|
33
|
+
terminalComparisonIds: ['rebuilt-own-gate'],
|
|
34
|
+
comparisonWindow: { start: 1, end: 3 },
|
|
35
|
+
normalization: { pnlUnit: 'PnL', maxLossValue: 10 },
|
|
36
|
+
contextFingerprint: fingerprint,
|
|
37
|
+
limitations: [],
|
|
38
|
+
candidates: [
|
|
39
|
+
{
|
|
40
|
+
id: 'current-baseline',
|
|
41
|
+
label: 'production core + current AI-gate',
|
|
42
|
+
role: 'baseline',
|
|
43
|
+
status: 'production-control',
|
|
44
|
+
color: '#000000',
|
|
45
|
+
gateSource: 'current',
|
|
46
|
+
gateAuthority: 'authority.json',
|
|
47
|
+
coreResearchId: 'core',
|
|
48
|
+
coreConfigSha256: fingerprint,
|
|
49
|
+
coreResult: 'core.json',
|
|
50
|
+
coreExport: 'export.jsonl',
|
|
51
|
+
gateReport: 'gate.json',
|
|
52
|
+
directionPolicy: 'both',
|
|
53
|
+
},
|
|
54
|
+
{
|
|
55
|
+
id: 'rebuilt-own-gate',
|
|
56
|
+
label: 'production core + rebuilt own gate',
|
|
57
|
+
role: 'candidate',
|
|
58
|
+
status: 'research-only',
|
|
59
|
+
color: '#ffffff',
|
|
60
|
+
gateSource: 'variant',
|
|
61
|
+
coreResearchId: 'core',
|
|
62
|
+
coreConfigSha256: fingerprint,
|
|
63
|
+
coreResult: 'core.json',
|
|
64
|
+
coreExport: 'export.jsonl',
|
|
65
|
+
gateReport: 'gate.json',
|
|
66
|
+
variantName: 'own-gate',
|
|
67
|
+
directionPolicy: 'both',
|
|
68
|
+
},
|
|
69
|
+
],
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
const createFixture = async (t) => {
|
|
73
|
+
const root = await fsp.mkdtemp(
|
|
74
|
+
path.join(os.tmpdir(), 'final-composition-spec-'),
|
|
75
|
+
);
|
|
76
|
+
t.after(() => fsp.rm(root, { recursive: true, force: true }));
|
|
77
|
+
const exportContents = '{}\n';
|
|
78
|
+
await Promise.all([
|
|
79
|
+
fsp.writeFile(path.join(root, 'core.json'), '{}\n'),
|
|
80
|
+
fsp.writeFile(path.join(root, 'export.jsonl'), exportContents),
|
|
81
|
+
fsp.writeFile(
|
|
82
|
+
path.join(root, 'authority.json'),
|
|
83
|
+
JSON.stringify({
|
|
84
|
+
run: {
|
|
85
|
+
mode: 'local-deterministic',
|
|
86
|
+
minQuality: 4,
|
|
87
|
+
directionPolicy: 'both',
|
|
88
|
+
},
|
|
89
|
+
research: {
|
|
90
|
+
lineage: {
|
|
91
|
+
gateFingerprint: 'runtime-gate-v1',
|
|
92
|
+
sourceSha256s: [sha256(exportContents)],
|
|
93
|
+
},
|
|
94
|
+
},
|
|
95
|
+
}),
|
|
96
|
+
),
|
|
97
|
+
fsp.writeFile(
|
|
98
|
+
path.join(root, 'gate.json'),
|
|
99
|
+
JSON.stringify({
|
|
100
|
+
run: { minQuality: 4 },
|
|
101
|
+
baseline: {
|
|
102
|
+
equity: [
|
|
103
|
+
[1, 0],
|
|
104
|
+
[2, 7],
|
|
105
|
+
],
|
|
106
|
+
periods: periods(7),
|
|
107
|
+
},
|
|
108
|
+
variants: [
|
|
109
|
+
{
|
|
110
|
+
name: 'own-gate',
|
|
111
|
+
mode: 'replace',
|
|
112
|
+
quality: 4,
|
|
113
|
+
direction: null,
|
|
114
|
+
expression: 'true',
|
|
115
|
+
equity: [
|
|
116
|
+
[1, 0],
|
|
117
|
+
[2, 5],
|
|
118
|
+
],
|
|
119
|
+
periods: periods(5),
|
|
120
|
+
},
|
|
121
|
+
],
|
|
122
|
+
}),
|
|
123
|
+
),
|
|
124
|
+
]);
|
|
125
|
+
return root;
|
|
126
|
+
};
|
|
127
|
+
|
|
128
|
+
test('uses current gate as baseline and rebuilt gate as a separate candidate', async (t) => {
|
|
129
|
+
const root = await createFixture(t);
|
|
130
|
+
const spec = await buildFinalCompositionSpec({
|
|
131
|
+
artifactRoot: root,
|
|
132
|
+
selection: makeSelection('a'.repeat(64)),
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
assert.equal(spec.baselineId, 'current-baseline');
|
|
136
|
+
assert.equal(spec.candidates[0].composition.gateSource, 'current');
|
|
137
|
+
assert.match(
|
|
138
|
+
spec.candidates[0].composition.gateAuthority.sha256,
|
|
139
|
+
/^[a-f0-9]{64}$/u,
|
|
140
|
+
);
|
|
141
|
+
assert.equal(spec.candidates[0].metrics.pnl, 7);
|
|
142
|
+
assert.equal(spec.candidates[1].composition.gateSource, 'variant');
|
|
143
|
+
assert.equal(spec.candidates[1].metrics.pnl, 5);
|
|
144
|
+
assert.deepEqual(spec.candidates[0].equity, [
|
|
145
|
+
[1, 0],
|
|
146
|
+
[2, 7],
|
|
147
|
+
]);
|
|
148
|
+
assert.deepEqual(spec.terminalComparisonIds, ['rebuilt-own-gate']);
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
test('rejects a rebuilt gate as the declared baseline', async (t) => {
|
|
152
|
+
const root = await createFixture(t);
|
|
153
|
+
const selection = makeSelection('a'.repeat(64));
|
|
154
|
+
selection.candidates[0].gateSource = 'variant';
|
|
155
|
+
await assert.rejects(
|
|
156
|
+
buildFinalCompositionSpec({ artifactRoot: root, selection }),
|
|
157
|
+
/production core \+ current AI-gate/u,
|
|
158
|
+
);
|
|
159
|
+
});
|