@openfinclaw/openfinclaw-strategy 0.0.11
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/LICENSE +21 -0
- package/README.md +185 -0
- package/index.test.ts +269 -0
- package/index.ts +1005 -0
- package/openclaw.plugin.json +35 -0
- package/package.json +45 -0
- package/skills/openfinclaw/SKILL.md +301 -0
- package/skills/skill-publish/SKILL.md +316 -0
- package/skills/strategy-builder/SKILL.md +555 -0
- package/skills/strategy-fork/SKILL.md +165 -0
- package/skills/strategy-pack/SKILL.md +285 -0
- package/src/cli.ts +321 -0
- package/src/fork.ts +342 -0
- package/src/strategy-storage.test.ts +109 -0
- package/src/strategy-storage.ts +303 -0
- package/src/types.ts +494 -0
- package/src/validate.test.ts +841 -0
- package/src/validate.ts +594 -0
|
@@ -0,0 +1,841 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for strategy package validation (FEP v2.0).
|
|
3
|
+
* Uses real node:fs/promises for file operations.
|
|
4
|
+
*/
|
|
5
|
+
import { mkdtempSync, mkdirSync, rmSync, writeFileSync } from "node:fs";
|
|
6
|
+
import { readFile } from "node:fs/promises";
|
|
7
|
+
import { tmpdir } from "node:os";
|
|
8
|
+
import path from "node:path";
|
|
9
|
+
import { afterEach, beforeEach, describe, expect, it } from "vitest";
|
|
10
|
+
import { validateStrategyPackage } from "./validate.js";
|
|
11
|
+
|
|
12
|
+
describe("validateStrategyPackage (FEP v2.0)", () => {
|
|
13
|
+
let tmpDir: string;
|
|
14
|
+
|
|
15
|
+
beforeEach(() => {
|
|
16
|
+
tmpDir = mkdtempSync(path.join(tmpdir(), "fep-v2-validate-"));
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
afterEach(() => {
|
|
20
|
+
if (tmpDir) {
|
|
21
|
+
try {
|
|
22
|
+
rmSync(tmpDir, { recursive: true, force: true });
|
|
23
|
+
} catch {
|
|
24
|
+
// ignore
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* 创建最小有效策略包
|
|
31
|
+
*/
|
|
32
|
+
function createMinimalValidPackage(): string {
|
|
33
|
+
writeFileSync(
|
|
34
|
+
path.join(tmpDir, "fep.yaml"),
|
|
35
|
+
`
|
|
36
|
+
fep: "2.0"
|
|
37
|
+
identity:
|
|
38
|
+
id: fin-test-minimal
|
|
39
|
+
name: "Test Strategy"
|
|
40
|
+
type: strategy
|
|
41
|
+
version: "1.0.0"
|
|
42
|
+
style: trend
|
|
43
|
+
visibility: private
|
|
44
|
+
summary: "A test strategy"
|
|
45
|
+
description: "A simple test strategy for validation"
|
|
46
|
+
license: MIT
|
|
47
|
+
tags: [test, validation]
|
|
48
|
+
author:
|
|
49
|
+
name: "Test Author"
|
|
50
|
+
changelog:
|
|
51
|
+
- version: "1.0.0"
|
|
52
|
+
date: "2025-01-01"
|
|
53
|
+
changes: "Initial release"
|
|
54
|
+
technical:
|
|
55
|
+
language: python
|
|
56
|
+
entryPoint: strategy.py
|
|
57
|
+
backtest:
|
|
58
|
+
symbol: "BTC/USDT"
|
|
59
|
+
defaultPeriod:
|
|
60
|
+
startDate: "2024-01-01"
|
|
61
|
+
endDate: "2024-12-31"
|
|
62
|
+
initialCapital: 10000
|
|
63
|
+
`,
|
|
64
|
+
);
|
|
65
|
+
|
|
66
|
+
const scriptDir = path.join(tmpDir, "scripts");
|
|
67
|
+
mkdirSync(scriptDir, { recursive: true });
|
|
68
|
+
writeFileSync(
|
|
69
|
+
path.join(scriptDir, "strategy.py"),
|
|
70
|
+
`
|
|
71
|
+
import numpy as np
|
|
72
|
+
|
|
73
|
+
def compute(data, context=None):
|
|
74
|
+
close = data["close"].values
|
|
75
|
+
price = float(close[-1])
|
|
76
|
+
return {"action": "hold", "amount": 0, "price": price, "reason": "test"}
|
|
77
|
+
`,
|
|
78
|
+
);
|
|
79
|
+
|
|
80
|
+
return tmpDir;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// ── 基础验证测试 ──
|
|
84
|
+
|
|
85
|
+
it("returns valid for minimal FEP v2.0 package", async () => {
|
|
86
|
+
const dir = createMinimalValidPackage();
|
|
87
|
+
const result = await validateStrategyPackage(dir);
|
|
88
|
+
expect(result.valid).toBe(true);
|
|
89
|
+
expect(result.errors).toEqual([]);
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
it("returns invalid when fep.yaml is missing", async () => {
|
|
93
|
+
const scriptDir = path.join(tmpDir, "scripts");
|
|
94
|
+
mkdirSync(scriptDir, { recursive: true });
|
|
95
|
+
writeFileSync(path.join(scriptDir, "strategy.py"), "def compute(data): return {}");
|
|
96
|
+
|
|
97
|
+
const result = await validateStrategyPackage(tmpDir);
|
|
98
|
+
expect(result.valid).toBe(false);
|
|
99
|
+
expect(result.errors.some((e) => e.includes("fep.yaml"))).toBe(true);
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
it("returns invalid when scripts/strategy.py is missing", async () => {
|
|
103
|
+
writeFileSync(path.join(tmpDir, "fep.yaml"), `fep: "2.0"\nidentity:\n id: test\n name: Test`);
|
|
104
|
+
|
|
105
|
+
const result = await validateStrategyPackage(tmpDir);
|
|
106
|
+
expect(result.valid).toBe(false);
|
|
107
|
+
expect(result.errors.some((e) => e.includes("strategy.py"))).toBe(true);
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
// ── 版本验证测试 ──
|
|
111
|
+
|
|
112
|
+
it("rejects fep version other than 2.0", async () => {
|
|
113
|
+
writeFileSync(path.join(tmpDir, "fep.yaml"), `fep: "1.2"\nidentity:\n id: test\n name: Test`);
|
|
114
|
+
|
|
115
|
+
const scriptDir = path.join(tmpDir, "scripts");
|
|
116
|
+
mkdirSync(scriptDir, { recursive: true });
|
|
117
|
+
writeFileSync(path.join(scriptDir, "strategy.py"), "def compute(data): return {}");
|
|
118
|
+
|
|
119
|
+
const result = await validateStrategyPackage(tmpDir);
|
|
120
|
+
expect(result.valid).toBe(false);
|
|
121
|
+
expect(result.errors.some((e) => e.includes("版本必须为") && e.includes("2.0"))).toBe(true);
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
// ── identity 必填字段测试 ──
|
|
125
|
+
|
|
126
|
+
it("requires identity.id", async () => {
|
|
127
|
+
writeFileSync(path.join(tmpDir, "fep.yaml"), `fep: "2.0"\nidentity:\n name: Test`);
|
|
128
|
+
|
|
129
|
+
const scriptDir = path.join(tmpDir, "scripts");
|
|
130
|
+
mkdirSync(scriptDir, { recursive: true });
|
|
131
|
+
writeFileSync(path.join(scriptDir, "strategy.py"), "def compute(data): return {}");
|
|
132
|
+
|
|
133
|
+
const result = await validateStrategyPackage(tmpDir);
|
|
134
|
+
expect(result.valid).toBe(false);
|
|
135
|
+
expect(result.errors.some((e) => e.includes("identity") && e.includes("id"))).toBe(true);
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
it("requires identity.name", async () => {
|
|
139
|
+
writeFileSync(path.join(tmpDir, "fep.yaml"), `fep: "2.0"\nidentity:\n id: test`);
|
|
140
|
+
|
|
141
|
+
const scriptDir = path.join(tmpDir, "scripts");
|
|
142
|
+
mkdirSync(scriptDir, { recursive: true });
|
|
143
|
+
writeFileSync(path.join(scriptDir, "strategy.py"), "def compute(data): return {}");
|
|
144
|
+
|
|
145
|
+
const result = await validateStrategyPackage(tmpDir);
|
|
146
|
+
expect(result.valid).toBe(false);
|
|
147
|
+
expect(result.errors.some((e) => e.includes("identity") && e.includes("name"))).toBe(true);
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
it("requires identity.author.name", async () => {
|
|
151
|
+
writeFileSync(
|
|
152
|
+
path.join(tmpDir, "fep.yaml"),
|
|
153
|
+
`
|
|
154
|
+
fep: "2.0"
|
|
155
|
+
identity:
|
|
156
|
+
id: test
|
|
157
|
+
name: Test
|
|
158
|
+
type: strategy
|
|
159
|
+
version: "1.0.0"
|
|
160
|
+
style: trend
|
|
161
|
+
visibility: private
|
|
162
|
+
summary: "test"
|
|
163
|
+
description: "test"
|
|
164
|
+
license: MIT
|
|
165
|
+
tags: [test]
|
|
166
|
+
author:
|
|
167
|
+
wallet: "0x..."
|
|
168
|
+
changelog:
|
|
169
|
+
- version: "1.0.0"
|
|
170
|
+
date: "2025-01-01"
|
|
171
|
+
changes: "Initial"
|
|
172
|
+
backtest:
|
|
173
|
+
symbol: "BTC/USDT"
|
|
174
|
+
defaultPeriod:
|
|
175
|
+
startDate: "2024-01-01"
|
|
176
|
+
endDate: "2024-12-31"
|
|
177
|
+
initialCapital: 10000
|
|
178
|
+
`,
|
|
179
|
+
);
|
|
180
|
+
|
|
181
|
+
const scriptDir = path.join(tmpDir, "scripts");
|
|
182
|
+
mkdirSync(scriptDir, { recursive: true });
|
|
183
|
+
writeFileSync(path.join(scriptDir, "strategy.py"), "def compute(data): return {}");
|
|
184
|
+
|
|
185
|
+
const result = await validateStrategyPackage(tmpDir);
|
|
186
|
+
expect(result.valid).toBe(false);
|
|
187
|
+
expect(result.errors.some((e) => e.includes("author") && e.includes("name"))).toBe(true);
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
// ── style 枚举验证测试 ──
|
|
191
|
+
|
|
192
|
+
it("validates style enum values", async () => {
|
|
193
|
+
writeFileSync(
|
|
194
|
+
path.join(tmpDir, "fep.yaml"),
|
|
195
|
+
`
|
|
196
|
+
fep: "2.0"
|
|
197
|
+
identity:
|
|
198
|
+
id: test
|
|
199
|
+
name: Test
|
|
200
|
+
type: strategy
|
|
201
|
+
version: "1.0.0"
|
|
202
|
+
style: invalid_style
|
|
203
|
+
visibility: private
|
|
204
|
+
summary: "test"
|
|
205
|
+
description: "test"
|
|
206
|
+
license: MIT
|
|
207
|
+
tags: [test]
|
|
208
|
+
author:
|
|
209
|
+
name: "Author"
|
|
210
|
+
changelog:
|
|
211
|
+
- version: "1.0.0"
|
|
212
|
+
date: "2025-01-01"
|
|
213
|
+
changes: "Initial"
|
|
214
|
+
backtest:
|
|
215
|
+
symbol: "BTC/USDT"
|
|
216
|
+
defaultPeriod:
|
|
217
|
+
startDate: "2024-01-01"
|
|
218
|
+
endDate: "2024-12-31"
|
|
219
|
+
initialCapital: 10000
|
|
220
|
+
`,
|
|
221
|
+
);
|
|
222
|
+
|
|
223
|
+
const scriptDir = path.join(tmpDir, "scripts");
|
|
224
|
+
mkdirSync(scriptDir, { recursive: true });
|
|
225
|
+
writeFileSync(path.join(scriptDir, "strategy.py"), "def compute(data): return {}");
|
|
226
|
+
|
|
227
|
+
const result = await validateStrategyPackage(tmpDir);
|
|
228
|
+
expect(result.valid).toBe(false);
|
|
229
|
+
expect(result.errors.some((e) => e.includes("style"))).toBe(true);
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
// ── backtest 验证测试 ──
|
|
233
|
+
|
|
234
|
+
it("requires backtest.symbol", async () => {
|
|
235
|
+
writeFileSync(
|
|
236
|
+
path.join(tmpDir, "fep.yaml"),
|
|
237
|
+
`
|
|
238
|
+
fep: "2.0"
|
|
239
|
+
identity:
|
|
240
|
+
id: test
|
|
241
|
+
name: Test
|
|
242
|
+
type: strategy
|
|
243
|
+
version: "1.0.0"
|
|
244
|
+
style: trend
|
|
245
|
+
visibility: private
|
|
246
|
+
summary: "test"
|
|
247
|
+
description: "test"
|
|
248
|
+
license: MIT
|
|
249
|
+
tags: [test]
|
|
250
|
+
author:
|
|
251
|
+
name: "Author"
|
|
252
|
+
changelog:
|
|
253
|
+
- version: "1.0.0"
|
|
254
|
+
date: "2025-01-01"
|
|
255
|
+
changes: "Initial"
|
|
256
|
+
backtest:
|
|
257
|
+
defaultPeriod:
|
|
258
|
+
startDate: "2024-01-01"
|
|
259
|
+
endDate: "2024-12-31"
|
|
260
|
+
initialCapital: 10000
|
|
261
|
+
`,
|
|
262
|
+
);
|
|
263
|
+
|
|
264
|
+
const scriptDir = path.join(tmpDir, "scripts");
|
|
265
|
+
mkdirSync(scriptDir, { recursive: true });
|
|
266
|
+
writeFileSync(path.join(scriptDir, "strategy.py"), "def compute(data): return {}");
|
|
267
|
+
|
|
268
|
+
const result = await validateStrategyPackage(tmpDir);
|
|
269
|
+
expect(result.valid).toBe(false);
|
|
270
|
+
expect(result.errors.some((e) => e.includes("symbol"))).toBe(true);
|
|
271
|
+
});
|
|
272
|
+
|
|
273
|
+
it("requires backtest.initialCapital", async () => {
|
|
274
|
+
writeFileSync(
|
|
275
|
+
path.join(tmpDir, "fep.yaml"),
|
|
276
|
+
`
|
|
277
|
+
fep: "2.0"
|
|
278
|
+
identity:
|
|
279
|
+
id: test
|
|
280
|
+
name: Test
|
|
281
|
+
type: strategy
|
|
282
|
+
version: "1.0.0"
|
|
283
|
+
style: trend
|
|
284
|
+
visibility: private
|
|
285
|
+
summary: "test"
|
|
286
|
+
description: "test"
|
|
287
|
+
license: MIT
|
|
288
|
+
tags: [test]
|
|
289
|
+
author:
|
|
290
|
+
name: "Author"
|
|
291
|
+
changelog:
|
|
292
|
+
- version: "1.0.0"
|
|
293
|
+
date: "2025-01-01"
|
|
294
|
+
changes: "Initial"
|
|
295
|
+
backtest:
|
|
296
|
+
symbol: "BTC/USDT"
|
|
297
|
+
defaultPeriod:
|
|
298
|
+
startDate: "2024-01-01"
|
|
299
|
+
endDate: "2024-12-31"
|
|
300
|
+
`,
|
|
301
|
+
);
|
|
302
|
+
|
|
303
|
+
const scriptDir = path.join(tmpDir, "scripts");
|
|
304
|
+
mkdirSync(scriptDir, { recursive: true });
|
|
305
|
+
writeFileSync(path.join(scriptDir, "strategy.py"), "def compute(data): return {}");
|
|
306
|
+
|
|
307
|
+
const result = await validateStrategyPackage(tmpDir);
|
|
308
|
+
expect(result.valid).toBe(false);
|
|
309
|
+
expect(result.errors.some((e) => e.includes("initialCapital"))).toBe(true);
|
|
310
|
+
});
|
|
311
|
+
|
|
312
|
+
// ── symbol 格式验证测试 ──
|
|
313
|
+
|
|
314
|
+
it("recognizes Crypto symbol format (BTC/USDT)", async () => {
|
|
315
|
+
const dir = createMinimalValidPackage();
|
|
316
|
+
const result = await validateStrategyPackage(dir);
|
|
317
|
+
expect(result.valid).toBe(true);
|
|
318
|
+
});
|
|
319
|
+
|
|
320
|
+
it("recognizes A-share symbol format (000001.SZ)", async () => {
|
|
321
|
+
writeFileSync(
|
|
322
|
+
path.join(tmpDir, "fep.yaml"),
|
|
323
|
+
`
|
|
324
|
+
fep: "2.0"
|
|
325
|
+
identity:
|
|
326
|
+
id: test
|
|
327
|
+
name: Test
|
|
328
|
+
type: strategy
|
|
329
|
+
version: "1.0.0"
|
|
330
|
+
style: trend
|
|
331
|
+
visibility: private
|
|
332
|
+
summary: "test"
|
|
333
|
+
description: "test"
|
|
334
|
+
license: MIT
|
|
335
|
+
tags: [test]
|
|
336
|
+
author:
|
|
337
|
+
name: "Author"
|
|
338
|
+
changelog:
|
|
339
|
+
- version: "1.0.0"
|
|
340
|
+
date: "2025-01-01"
|
|
341
|
+
changes: "Initial"
|
|
342
|
+
backtest:
|
|
343
|
+
symbol: "000001.SZ"
|
|
344
|
+
defaultPeriod:
|
|
345
|
+
startDate: "2024-01-01"
|
|
346
|
+
endDate: "2024-12-31"
|
|
347
|
+
initialCapital: 100000
|
|
348
|
+
`,
|
|
349
|
+
);
|
|
350
|
+
|
|
351
|
+
const scriptDir = path.join(tmpDir, "scripts");
|
|
352
|
+
mkdirSync(scriptDir, { recursive: true });
|
|
353
|
+
writeFileSync(path.join(scriptDir, "strategy.py"), "def compute(data): return {}");
|
|
354
|
+
|
|
355
|
+
const result = await validateStrategyPackage(tmpDir);
|
|
356
|
+
expect(result.valid).toBe(true);
|
|
357
|
+
});
|
|
358
|
+
|
|
359
|
+
it("recognizes US stock symbol format (AAPL)", async () => {
|
|
360
|
+
writeFileSync(
|
|
361
|
+
path.join(tmpDir, "fep.yaml"),
|
|
362
|
+
`
|
|
363
|
+
fep: "2.0"
|
|
364
|
+
identity:
|
|
365
|
+
id: test
|
|
366
|
+
name: Test
|
|
367
|
+
type: strategy
|
|
368
|
+
version: "1.0.0"
|
|
369
|
+
style: trend
|
|
370
|
+
visibility: private
|
|
371
|
+
summary: "test"
|
|
372
|
+
description: "test"
|
|
373
|
+
license: MIT
|
|
374
|
+
tags: [test]
|
|
375
|
+
author:
|
|
376
|
+
name: "Author"
|
|
377
|
+
changelog:
|
|
378
|
+
- version: "1.0.0"
|
|
379
|
+
date: "2025-01-01"
|
|
380
|
+
changes: "Initial"
|
|
381
|
+
backtest:
|
|
382
|
+
symbol: "AAPL"
|
|
383
|
+
defaultPeriod:
|
|
384
|
+
startDate: "2024-01-01"
|
|
385
|
+
endDate: "2024-12-31"
|
|
386
|
+
initialCapital: 10000
|
|
387
|
+
`,
|
|
388
|
+
);
|
|
389
|
+
|
|
390
|
+
const scriptDir = path.join(tmpDir, "scripts");
|
|
391
|
+
mkdirSync(scriptDir, { recursive: true });
|
|
392
|
+
writeFileSync(path.join(scriptDir, "strategy.py"), "def compute(data): return {}");
|
|
393
|
+
|
|
394
|
+
const result = await validateStrategyPackage(tmpDir);
|
|
395
|
+
expect(result.valid).toBe(true);
|
|
396
|
+
});
|
|
397
|
+
|
|
398
|
+
it("recognizes HK stock symbol format (00700.HK)", async () => {
|
|
399
|
+
writeFileSync(
|
|
400
|
+
path.join(tmpDir, "fep.yaml"),
|
|
401
|
+
`
|
|
402
|
+
fep: "2.0"
|
|
403
|
+
identity:
|
|
404
|
+
id: test
|
|
405
|
+
name: Test
|
|
406
|
+
type: strategy
|
|
407
|
+
version: "1.0.0"
|
|
408
|
+
style: trend
|
|
409
|
+
visibility: private
|
|
410
|
+
summary: "test"
|
|
411
|
+
description: "test"
|
|
412
|
+
license: MIT
|
|
413
|
+
tags: [test]
|
|
414
|
+
author:
|
|
415
|
+
name: "Author"
|
|
416
|
+
changelog:
|
|
417
|
+
- version: "1.0.0"
|
|
418
|
+
date: "2025-01-01"
|
|
419
|
+
changes: "Initial"
|
|
420
|
+
backtest:
|
|
421
|
+
symbol: "00700.HK"
|
|
422
|
+
defaultPeriod:
|
|
423
|
+
startDate: "2024-01-01"
|
|
424
|
+
endDate: "2024-12-31"
|
|
425
|
+
initialCapital: 10000
|
|
426
|
+
`,
|
|
427
|
+
);
|
|
428
|
+
|
|
429
|
+
const scriptDir = path.join(tmpDir, "scripts");
|
|
430
|
+
mkdirSync(scriptDir, { recursive: true });
|
|
431
|
+
writeFileSync(path.join(scriptDir, "strategy.py"), "def compute(data): return {}");
|
|
432
|
+
|
|
433
|
+
const result = await validateStrategyPackage(tmpDir);
|
|
434
|
+
expect(result.valid).toBe(true);
|
|
435
|
+
});
|
|
436
|
+
|
|
437
|
+
// ── 策略函数验证测试 ──
|
|
438
|
+
|
|
439
|
+
it("accepts compute(data) function", async () => {
|
|
440
|
+
const dir = createMinimalValidPackage();
|
|
441
|
+
const result = await validateStrategyPackage(dir);
|
|
442
|
+
expect(result.valid).toBe(true);
|
|
443
|
+
});
|
|
444
|
+
|
|
445
|
+
it("accepts compute(data, context=None) function", async () => {
|
|
446
|
+
const dir = createMinimalValidPackage();
|
|
447
|
+
|
|
448
|
+
const scriptDir = path.join(tmpDir, "scripts");
|
|
449
|
+
writeFileSync(
|
|
450
|
+
path.join(scriptDir, "strategy.py"),
|
|
451
|
+
`
|
|
452
|
+
def compute(data, context=None):
|
|
453
|
+
position = context.get("position") if context else None
|
|
454
|
+
return {"action": "hold", "amount": 0, "price": 0, "reason": "test"}
|
|
455
|
+
`,
|
|
456
|
+
);
|
|
457
|
+
|
|
458
|
+
const result = await validateStrategyPackage(dir);
|
|
459
|
+
expect(result.valid).toBe(true);
|
|
460
|
+
});
|
|
461
|
+
|
|
462
|
+
it("accepts select(universe) function for multi-asset strategies", async () => {
|
|
463
|
+
writeFileSync(
|
|
464
|
+
path.join(tmpDir, "fep.yaml"),
|
|
465
|
+
`
|
|
466
|
+
fep: "2.0"
|
|
467
|
+
identity:
|
|
468
|
+
id: test-rotation
|
|
469
|
+
name: "Rotation Strategy"
|
|
470
|
+
type: strategy
|
|
471
|
+
version: "1.0.0"
|
|
472
|
+
style: rotation
|
|
473
|
+
visibility: private
|
|
474
|
+
summary: "test"
|
|
475
|
+
description: "test"
|
|
476
|
+
license: MIT
|
|
477
|
+
tags: [test]
|
|
478
|
+
author:
|
|
479
|
+
name: "Author"
|
|
480
|
+
changelog:
|
|
481
|
+
- version: "1.0.0"
|
|
482
|
+
date: "2025-01-01"
|
|
483
|
+
changes: "Initial"
|
|
484
|
+
backtest:
|
|
485
|
+
symbol: "000001.SZ"
|
|
486
|
+
universe:
|
|
487
|
+
symbols:
|
|
488
|
+
- "000001.SZ"
|
|
489
|
+
- "000002.SZ"
|
|
490
|
+
- "600519.SH"
|
|
491
|
+
defaultPeriod:
|
|
492
|
+
startDate: "2024-01-01"
|
|
493
|
+
endDate: "2024-12-31"
|
|
494
|
+
initialCapital: 1000000
|
|
495
|
+
`,
|
|
496
|
+
);
|
|
497
|
+
|
|
498
|
+
const scriptDir = path.join(tmpDir, "scripts");
|
|
499
|
+
mkdirSync(scriptDir, { recursive: true });
|
|
500
|
+
writeFileSync(
|
|
501
|
+
path.join(scriptDir, "strategy.py"),
|
|
502
|
+
`
|
|
503
|
+
import numpy as np
|
|
504
|
+
|
|
505
|
+
def select(universe):
|
|
506
|
+
scores = []
|
|
507
|
+
for symbol, df in universe.items():
|
|
508
|
+
close = df["close"].values
|
|
509
|
+
if len(close) >= 20:
|
|
510
|
+
momentum = (close[-1] / close[-20]) - 1
|
|
511
|
+
scores.append((symbol, momentum))
|
|
512
|
+
scores.sort(key=lambda x: x[1], reverse=True)
|
|
513
|
+
return [s[0] for s in scores]
|
|
514
|
+
`,
|
|
515
|
+
);
|
|
516
|
+
|
|
517
|
+
const result = await validateStrategyPackage(tmpDir);
|
|
518
|
+
expect(result.valid).toBe(true);
|
|
519
|
+
});
|
|
520
|
+
|
|
521
|
+
it("rejects strategy without compute or select function", async () => {
|
|
522
|
+
writeFileSync(
|
|
523
|
+
path.join(tmpDir, "fep.yaml"),
|
|
524
|
+
`
|
|
525
|
+
fep: "2.0"
|
|
526
|
+
identity:
|
|
527
|
+
id: test
|
|
528
|
+
name: Test
|
|
529
|
+
type: strategy
|
|
530
|
+
version: "1.0.0"
|
|
531
|
+
style: trend
|
|
532
|
+
visibility: private
|
|
533
|
+
summary: "test"
|
|
534
|
+
description: "test"
|
|
535
|
+
license: MIT
|
|
536
|
+
tags: [test]
|
|
537
|
+
author:
|
|
538
|
+
name: "Author"
|
|
539
|
+
changelog:
|
|
540
|
+
- version: "1.0.0"
|
|
541
|
+
date: "2025-01-01"
|
|
542
|
+
changes: "Initial"
|
|
543
|
+
backtest:
|
|
544
|
+
symbol: "BTC/USDT"
|
|
545
|
+
defaultPeriod:
|
|
546
|
+
startDate: "2024-01-01"
|
|
547
|
+
endDate: "2024-12-31"
|
|
548
|
+
initialCapital: 10000
|
|
549
|
+
`,
|
|
550
|
+
);
|
|
551
|
+
|
|
552
|
+
const scriptDir = path.join(tmpDir, "scripts");
|
|
553
|
+
mkdirSync(scriptDir, { recursive: true });
|
|
554
|
+
writeFileSync(
|
|
555
|
+
path.join(scriptDir, "strategy.py"),
|
|
556
|
+
`
|
|
557
|
+
# No compute or select function
|
|
558
|
+
def helper():
|
|
559
|
+
pass
|
|
560
|
+
`,
|
|
561
|
+
);
|
|
562
|
+
|
|
563
|
+
const result = await validateStrategyPackage(tmpDir);
|
|
564
|
+
expect(result.valid).toBe(false);
|
|
565
|
+
expect(result.errors.some((e) => e.includes("compute") || e.includes("select"))).toBe(true);
|
|
566
|
+
});
|
|
567
|
+
|
|
568
|
+
// ── 安全沙箱测试 ──
|
|
569
|
+
|
|
570
|
+
it("rejects forbidden import os", async () => {
|
|
571
|
+
const dir = createMinimalValidPackage();
|
|
572
|
+
|
|
573
|
+
const scriptDir = path.join(tmpDir, "scripts");
|
|
574
|
+
writeFileSync(
|
|
575
|
+
path.join(scriptDir, "strategy.py"),
|
|
576
|
+
`
|
|
577
|
+
import os
|
|
578
|
+
|
|
579
|
+
def compute(data):
|
|
580
|
+
return {"action": "hold"}
|
|
581
|
+
`,
|
|
582
|
+
);
|
|
583
|
+
|
|
584
|
+
const result = await validateStrategyPackage(dir);
|
|
585
|
+
expect(result.valid).toBe(false);
|
|
586
|
+
expect(result.errors.some((e) => e.includes("禁止的导入") && e.includes("os"))).toBe(true);
|
|
587
|
+
});
|
|
588
|
+
|
|
589
|
+
it("rejects forbidden eval() call", async () => {
|
|
590
|
+
const dir = createMinimalValidPackage();
|
|
591
|
+
|
|
592
|
+
const scriptDir = path.join(tmpDir, "scripts");
|
|
593
|
+
writeFileSync(
|
|
594
|
+
path.join(scriptDir, "strategy.py"),
|
|
595
|
+
`
|
|
596
|
+
def compute(data):
|
|
597
|
+
result = eval("1 + 1")
|
|
598
|
+
return {"action": "hold"}
|
|
599
|
+
`,
|
|
600
|
+
);
|
|
601
|
+
|
|
602
|
+
const result = await validateStrategyPackage(dir);
|
|
603
|
+
expect(result.valid).toBe(false);
|
|
604
|
+
expect(result.errors.some((e) => e.includes("禁止的函数调用") && e.includes("eval"))).toBe(
|
|
605
|
+
true,
|
|
606
|
+
);
|
|
607
|
+
});
|
|
608
|
+
|
|
609
|
+
it("rejects datetime.now() that breaks backtest consistency", async () => {
|
|
610
|
+
const dir = createMinimalValidPackage();
|
|
611
|
+
|
|
612
|
+
const scriptDir = path.join(tmpDir, "scripts");
|
|
613
|
+
writeFileSync(
|
|
614
|
+
path.join(scriptDir, "strategy.py"),
|
|
615
|
+
`
|
|
616
|
+
import datetime
|
|
617
|
+
|
|
618
|
+
def compute(data):
|
|
619
|
+
now = datetime.datetime.now()
|
|
620
|
+
return {"action": "hold"}
|
|
621
|
+
`,
|
|
622
|
+
);
|
|
623
|
+
|
|
624
|
+
const result = await validateStrategyPackage(dir);
|
|
625
|
+
expect(result.valid).toBe(false);
|
|
626
|
+
expect(result.errors.some((e) => e.includes("datetime.now") && e.includes("回测一致性"))).toBe(
|
|
627
|
+
true,
|
|
628
|
+
);
|
|
629
|
+
});
|
|
630
|
+
|
|
631
|
+
it("ignores datetime.now() in comments", async () => {
|
|
632
|
+
const dir = createMinimalValidPackage();
|
|
633
|
+
|
|
634
|
+
const scriptDir = path.join(tmpDir, "scripts");
|
|
635
|
+
writeFileSync(
|
|
636
|
+
path.join(scriptDir, "strategy.py"),
|
|
637
|
+
`
|
|
638
|
+
# Note: do not use datetime.now() in production
|
|
639
|
+
def compute(data):
|
|
640
|
+
return {"action": "hold"}
|
|
641
|
+
`,
|
|
642
|
+
);
|
|
643
|
+
|
|
644
|
+
const result = await validateStrategyPackage(dir);
|
|
645
|
+
expect(result.valid).toBe(true);
|
|
646
|
+
});
|
|
647
|
+
|
|
648
|
+
it("rejects forbidden requests import", async () => {
|
|
649
|
+
const dir = createMinimalValidPackage();
|
|
650
|
+
|
|
651
|
+
const scriptDir = path.join(tmpDir, "scripts");
|
|
652
|
+
writeFileSync(
|
|
653
|
+
path.join(scriptDir, "strategy.py"),
|
|
654
|
+
`
|
|
655
|
+
import requests
|
|
656
|
+
|
|
657
|
+
def compute(data):
|
|
658
|
+
return {"action": "hold"}
|
|
659
|
+
`,
|
|
660
|
+
);
|
|
661
|
+
|
|
662
|
+
const result = await validateStrategyPackage(dir);
|
|
663
|
+
expect(result.valid).toBe(false);
|
|
664
|
+
expect(result.errors.some((e) => e.includes("requests"))).toBe(true);
|
|
665
|
+
});
|
|
666
|
+
|
|
667
|
+
// ── timeframe 验证测试 ──
|
|
668
|
+
|
|
669
|
+
it("validates timeframe enum values", async () => {
|
|
670
|
+
writeFileSync(
|
|
671
|
+
path.join(tmpDir, "fep.yaml"),
|
|
672
|
+
`
|
|
673
|
+
fep: "2.0"
|
|
674
|
+
identity:
|
|
675
|
+
id: test
|
|
676
|
+
name: Test
|
|
677
|
+
type: strategy
|
|
678
|
+
version: "1.0.0"
|
|
679
|
+
style: trend
|
|
680
|
+
visibility: private
|
|
681
|
+
summary: "test"
|
|
682
|
+
description: "test"
|
|
683
|
+
license: MIT
|
|
684
|
+
tags: [test]
|
|
685
|
+
author:
|
|
686
|
+
name: "Author"
|
|
687
|
+
changelog:
|
|
688
|
+
- version: "1.0.0"
|
|
689
|
+
date: "2025-01-01"
|
|
690
|
+
changes: "Initial"
|
|
691
|
+
backtest:
|
|
692
|
+
symbol: "BTC/USDT"
|
|
693
|
+
timeframe: 1h
|
|
694
|
+
defaultPeriod:
|
|
695
|
+
startDate: "2024-01-01"
|
|
696
|
+
endDate: "2024-12-31"
|
|
697
|
+
initialCapital: 10000
|
|
698
|
+
`,
|
|
699
|
+
);
|
|
700
|
+
|
|
701
|
+
const scriptDir = path.join(tmpDir, "scripts");
|
|
702
|
+
mkdirSync(scriptDir, { recursive: true });
|
|
703
|
+
writeFileSync(path.join(scriptDir, "strategy.py"), "def compute(data): return {}");
|
|
704
|
+
|
|
705
|
+
const result = await validateStrategyPackage(tmpDir);
|
|
706
|
+
expect(result.valid).toBe(true);
|
|
707
|
+
});
|
|
708
|
+
|
|
709
|
+
it("rejects invalid timeframe value", async () => {
|
|
710
|
+
writeFileSync(
|
|
711
|
+
path.join(tmpDir, "fep.yaml"),
|
|
712
|
+
`
|
|
713
|
+
fep: "2.0"
|
|
714
|
+
identity:
|
|
715
|
+
id: test
|
|
716
|
+
name: Test
|
|
717
|
+
type: strategy
|
|
718
|
+
version: "1.0.0"
|
|
719
|
+
style: trend
|
|
720
|
+
visibility: private
|
|
721
|
+
summary: "test"
|
|
722
|
+
description: "test"
|
|
723
|
+
license: MIT
|
|
724
|
+
tags: [test]
|
|
725
|
+
author:
|
|
726
|
+
name: "Author"
|
|
727
|
+
changelog:
|
|
728
|
+
- version: "1.0.0"
|
|
729
|
+
date: "2025-01-01"
|
|
730
|
+
changes: "Initial"
|
|
731
|
+
backtest:
|
|
732
|
+
symbol: "BTC/USDT"
|
|
733
|
+
timeframe: 2h
|
|
734
|
+
defaultPeriod:
|
|
735
|
+
startDate: "2024-01-01"
|
|
736
|
+
endDate: "2024-12-31"
|
|
737
|
+
initialCapital: 10000
|
|
738
|
+
`,
|
|
739
|
+
);
|
|
740
|
+
|
|
741
|
+
const scriptDir = path.join(tmpDir, "scripts");
|
|
742
|
+
mkdirSync(scriptDir, { recursive: true });
|
|
743
|
+
writeFileSync(path.join(scriptDir, "strategy.py"), "def compute(data): return {}");
|
|
744
|
+
|
|
745
|
+
const result = await validateStrategyPackage(tmpDir);
|
|
746
|
+
expect(result.valid).toBe(false);
|
|
747
|
+
expect(result.errors.some((e) => e.includes("timeframe"))).toBe(true);
|
|
748
|
+
});
|
|
749
|
+
|
|
750
|
+
// ── 完整配置测试 ──
|
|
751
|
+
|
|
752
|
+
it("accepts full configuration with all optional fields", async () => {
|
|
753
|
+
writeFileSync(
|
|
754
|
+
path.join(tmpDir, "fep.yaml"),
|
|
755
|
+
`
|
|
756
|
+
fep: "2.0"
|
|
757
|
+
identity:
|
|
758
|
+
id: fin-full-test
|
|
759
|
+
name: "Full Test Strategy"
|
|
760
|
+
type: strategy
|
|
761
|
+
version: "1.0.0"
|
|
762
|
+
style: hybrid
|
|
763
|
+
visibility: public
|
|
764
|
+
summary: "A comprehensive test strategy"
|
|
765
|
+
description: "Full configuration test with all optional fields"
|
|
766
|
+
license: MIT
|
|
767
|
+
tags: [test, full, hybrid]
|
|
768
|
+
author:
|
|
769
|
+
name: "Test Author"
|
|
770
|
+
wallet: "0x1234567890abcdef"
|
|
771
|
+
changelog:
|
|
772
|
+
- version: "1.0.0"
|
|
773
|
+
date: "2025-01-01"
|
|
774
|
+
changes: "Initial release"
|
|
775
|
+
technical:
|
|
776
|
+
language: python
|
|
777
|
+
entryPoint: strategy.py
|
|
778
|
+
parameters:
|
|
779
|
+
- name: fast_period
|
|
780
|
+
default: 12
|
|
781
|
+
type: integer
|
|
782
|
+
label: "快速周期"
|
|
783
|
+
range: { min: 5, max: 50 }
|
|
784
|
+
- name: slow_period
|
|
785
|
+
default: 26
|
|
786
|
+
type: integer
|
|
787
|
+
label: "慢速周期"
|
|
788
|
+
backtest:
|
|
789
|
+
symbol: "BTC/USDT"
|
|
790
|
+
timeframe: 4h
|
|
791
|
+
defaultPeriod:
|
|
792
|
+
startDate: "2023-01-01"
|
|
793
|
+
endDate: "2024-12-31"
|
|
794
|
+
initialCapital: 50000
|
|
795
|
+
risk:
|
|
796
|
+
maxDrawdownThreshold: 20
|
|
797
|
+
dailyLossLimitPct: 5
|
|
798
|
+
maxTradesPerDay: 10
|
|
799
|
+
paper:
|
|
800
|
+
barIntervalSeconds: 60
|
|
801
|
+
maxDurationHours: 24
|
|
802
|
+
warmupBars: 100
|
|
803
|
+
timeframe: 1h
|
|
804
|
+
classification:
|
|
805
|
+
archetype: systematic
|
|
806
|
+
market: Crypto
|
|
807
|
+
assetClasses: [crypto]
|
|
808
|
+
frequency: daily
|
|
809
|
+
riskProfile: medium
|
|
810
|
+
`,
|
|
811
|
+
);
|
|
812
|
+
|
|
813
|
+
const scriptDir = path.join(tmpDir, "scripts");
|
|
814
|
+
mkdirSync(scriptDir, { recursive: true });
|
|
815
|
+
writeFileSync(
|
|
816
|
+
path.join(scriptDir, "strategy.py"),
|
|
817
|
+
`
|
|
818
|
+
import numpy as np
|
|
819
|
+
import pandas as pd
|
|
820
|
+
|
|
821
|
+
def compute(data, context=None):
|
|
822
|
+
close = data["close"].values
|
|
823
|
+
price = float(close[-1])
|
|
824
|
+
ma20 = float(np.mean(close[-20:])) if len(close) >= 20 else price
|
|
825
|
+
|
|
826
|
+
has_position = context and context.get("position") is not None
|
|
827
|
+
|
|
828
|
+
if not has_position and price > ma20:
|
|
829
|
+
return {"action": "buy", "amount": 1000, "price": price, "reason": "Price above MA20"}
|
|
830
|
+
elif has_position and price < ma20:
|
|
831
|
+
return {"action": "sell", "reason": "Price below MA20"}
|
|
832
|
+
|
|
833
|
+
return {"action": "hold", "reason": f"MA20={ma20:.2f}"}
|
|
834
|
+
`,
|
|
835
|
+
);
|
|
836
|
+
|
|
837
|
+
const result = await validateStrategyPackage(tmpDir);
|
|
838
|
+
expect(result.valid).toBe(true);
|
|
839
|
+
expect(result.errors).toEqual([]);
|
|
840
|
+
});
|
|
841
|
+
});
|