@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.
@@ -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
+ });