@openfinclaw/openfinclaw-strategy 0.0.11 → 0.1.1

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.
@@ -1,11 +1,10 @@
1
1
  /**
2
2
  * Strategy package validation for Findoo Backtest Agent (FEP v2.0).
3
3
  * Validates fep.yaml structure, strategy script safety, and required fields.
4
- * @module openfinclaw/validate
5
4
  */
6
5
  import { readFile } from "node:fs/promises";
7
6
  import path from "node:path";
8
- import type { FepV2Style, FepV2Timeframe } from "./types.js";
7
+ import type { FepV2Style, FepV2Timeframe } from "../types.js";
9
8
 
10
9
  /** FEP 版本常量 */
11
10
  const FEP_VERSION = "2.0";
@@ -223,7 +222,6 @@ function validateSymbol(symbol: string): { valid: boolean; market?: string } {
223
222
 
224
223
  /**
225
224
  * 提取 YAML 块内容
226
- * 提取指定块名下的所有内容(缩进的子行)
227
225
  */
228
226
  function extractYamlBlock(yamlContent: string, blockName: string): string {
229
227
  const lines = yamlContent.split("\n");
@@ -287,7 +285,6 @@ function validateFepYaml(
287
285
  ): { hasUniverse: boolean } {
288
286
  let hasUniverse = false;
289
287
 
290
- // 检查 fep 版本
291
288
  const fepMatch = /^\s*fep\s*:\s*["']?([^"'\s]+)["']?/m.exec(fepStr);
292
289
  if (!fepMatch) {
293
290
  errors.push("fep.yaml 必须包含 'fep:' 版本声明(例如 fep: \"2.0\")");
@@ -297,13 +294,11 @@ function validateFepYaml(
297
294
  );
298
295
  }
299
296
 
300
- // ── identity 验证 ──
301
297
  if (!hasField(fepStr, "identity")) {
302
298
  errors.push("fep.yaml 必须包含 'identity:' 节");
303
299
  } else {
304
300
  const identityBlock = extractYamlBlock(fepStr, "identity");
305
301
 
306
- // 必填字段检查
307
302
  const requiredIdentityFields = [
308
303
  { field: "id", message: "策略唯一标识" },
309
304
  { field: "name", message: "策略显示名称" },
@@ -324,7 +319,6 @@ function validateFepYaml(
324
319
  }
325
320
  }
326
321
 
327
- // author.name 必填
328
322
  if (!hasField(identityBlock, "author")) {
329
323
  errors.push("fep.yaml identity 必须包含 'author' 节");
330
324
  } else {
@@ -334,7 +328,6 @@ function validateFepYaml(
334
328
  }
335
329
  }
336
330
 
337
- // style 枚举验证
338
331
  const styleValue = getFieldValue(identityBlock, "style");
339
332
  if (styleValue) {
340
333
  const cleanedStyle = styleValue.replace(/["']/g, "");
@@ -343,7 +336,6 @@ function validateFepYaml(
343
336
  }
344
337
  }
345
338
 
346
- // visibility 枚举验证
347
339
  const visibilityValue = getFieldValue(identityBlock, "visibility");
348
340
  if (visibilityValue) {
349
341
  const cleanedVisibility = visibilityValue.replace(/["']/g, "");
@@ -354,23 +346,20 @@ function validateFepYaml(
354
346
  }
355
347
  }
356
348
 
357
- // license 枚举验证
358
349
  const licenseValue = getFieldValue(identityBlock, "license");
359
350
  if (licenseValue) {
360
351
  const cleanedLicense = licenseValue.replace(/["']/g, "");
361
352
  if (!VALID_LICENSES.includes(cleanedLicense)) {
362
- warnings.push(`fep.yaml identitylicense 建议: ${VALID_LICENSES.join(", ")}`);
353
+ warnings.push(`fep.yaml identity.license 建议: ${VALID_LICENSES.join(", ")}`);
363
354
  }
364
355
  }
365
356
 
366
- // tags 格式验证
367
357
  const tagsValue = getFieldValue(identityBlock, "tags");
368
358
  if (tagsValue && !tagsValue.startsWith("[")) {
369
359
  warnings.push("identity.tags 应使用行内数组格式,如: tags: [trend, btc, crypto]");
370
360
  }
371
361
  }
372
362
 
373
- // ── technical 验证(可选,有默认值)──
374
363
  if (hasField(fepStr, "technical")) {
375
364
  const technicalBlock = extractYamlBlock(fepStr, "technical");
376
365
 
@@ -385,13 +374,11 @@ function validateFepYaml(
385
374
  }
386
375
  }
387
376
 
388
- // ── backtest 验证(必填)──
389
377
  if (!hasField(fepStr, "backtest")) {
390
378
  errors.push("fep.yaml 必须包含 'backtest:' 节");
391
379
  } else {
392
380
  const backtestBlock = extractYamlBlock(fepStr, "backtest");
393
381
 
394
- // symbol 必填
395
382
  if (!hasField(backtestBlock, "symbol")) {
396
383
  errors.push("fep.yaml backtest 必须包含 'symbol'(交易品种)");
397
384
  } else {
@@ -405,7 +392,6 @@ function validateFepYaml(
405
392
  }
406
393
  }
407
394
 
408
- // defaultPeriod 必填
409
395
  if (!hasField(backtestBlock, "defaultPeriod")) {
410
396
  errors.push("fep.yaml backtest 必须包含 'defaultPeriod'");
411
397
  } else {
@@ -418,12 +404,10 @@ function validateFepYaml(
418
404
  }
419
405
  }
420
406
 
421
- // initialCapital 必填
422
407
  if (!hasField(backtestBlock, "initialCapital")) {
423
408
  errors.push("fep.yaml backtest 必须包含 'initialCapital'(初始资金)");
424
409
  }
425
410
 
426
- // timeframe 枚举验证(可选)
427
411
  const timeframeValue = getFieldValue(backtestBlock, "timeframe");
428
412
  if (timeframeValue) {
429
413
  const cleanedTimeframe = timeframeValue.replace(/["']/g, "");
@@ -432,7 +416,6 @@ function validateFepYaml(
432
416
  }
433
417
  }
434
418
 
435
- // universe 检测
436
419
  if (hasField(backtestBlock, "universe")) {
437
420
  hasUniverse = true;
438
421
  const universeBlock = extractYamlBlock(backtestBlock, "universe");
@@ -441,7 +424,6 @@ function validateFepYaml(
441
424
  }
442
425
  }
443
426
 
444
- // rebalance 验证(可选)
445
427
  if (hasField(backtestBlock, "rebalance")) {
446
428
  const rebalanceBlock = extractYamlBlock(backtestBlock, "rebalance");
447
429
  const freqValue = getFieldValue(rebalanceBlock, "frequency");
@@ -454,7 +436,6 @@ function validateFepYaml(
454
436
  }
455
437
  }
456
438
 
457
- // ── risk 验证(可选)──
458
439
  if (hasField(fepStr, "risk")) {
459
440
  const riskBlock = extractYamlBlock(fepStr, "risk");
460
441
  const thresholdValue = getFieldValue(riskBlock, "maxDrawdownThreshold");
@@ -466,7 +447,6 @@ function validateFepYaml(
466
447
  }
467
448
  }
468
449
 
469
- // ── paper 验证(可选)──
470
450
  if (hasField(fepStr, "paper")) {
471
451
  const paperBlock = extractYamlBlock(fepStr, "paper");
472
452
  const barIntervalValue = getFieldValue(paperBlock, "barIntervalSeconds");
@@ -492,7 +472,6 @@ function validateStrategyScript(
492
472
  ): void {
493
473
  const codeWithoutComments = removePythonComments(scriptStr);
494
474
 
495
- // 检查策略函数
496
475
  const hasCompute =
497
476
  /\bdef\s+compute\s*\(\s*data\s*\)/.test(scriptStr) ||
498
477
  /\bdef\s+compute\s*\(\s*data\s*,\s*context\s*(?:=\s*None)?\s*\)/.test(scriptStr);
@@ -502,22 +481,18 @@ function validateStrategyScript(
502
481
  errors.push("scripts/strategy.py 必须定义 compute(data) 或 select(universe) 函数");
503
482
  }
504
483
 
505
- // 如果有 universe 配置,推荐使用 select
506
484
  if (hasUniverse && !hasSelect) {
507
485
  warnings.push("backtest 配置了 universe,建议使用 select(universe) 函数实现多标的策略");
508
486
  }
509
487
 
510
- // 如果没有 universe 但使用 select,给出警告
511
488
  if (!hasUniverse && hasSelect && !hasCompute) {
512
489
  warnings.push("使用 select(universe) 函数时,建议在 backtest 中配置 universe");
513
490
  }
514
491
 
515
- // 检查返回值结构(简单检查)
516
492
  if (hasCompute && !/\baction\b/.test(scriptStr)) {
517
493
  warnings.push("compute(data) 返回值应包含 action 字段(buy/sell/hold/target)");
518
494
  }
519
495
 
520
- // 检查禁止的 import
521
496
  for (const pattern of FORBIDDEN_IMPORT_PATTERNS) {
522
497
  if (pattern.test(scriptStr)) {
523
498
  const match = pattern.exec(scriptStr);
@@ -526,7 +501,6 @@ function validateStrategyScript(
526
501
  }
527
502
  }
528
503
 
529
- // 检查禁止的函数调用(忽略注释)
530
504
  for (const pattern of FORBIDDEN_CALL_PATTERNS) {
531
505
  if (pattern.test(codeWithoutComments)) {
532
506
  const match = pattern.exec(codeWithoutComments);
@@ -535,7 +509,6 @@ function validateStrategyScript(
535
509
  }
536
510
  }
537
511
 
538
- // 检查破坏回测一致性的模式(忽略注释)
539
512
  for (const pattern of BACKTEST_BREAKING_PATTERNS) {
540
513
  if (pattern.test(codeWithoutComments)) {
541
514
  const match = pattern.exec(codeWithoutComments);
@@ -549,8 +522,6 @@ function validateStrategyScript(
549
522
 
550
523
  /**
551
524
  * 验证策略包目录(FEP v2.0)
552
- * @param dirPath 策略包目录路径
553
- * @returns 验证结果
554
525
  */
555
526
  export async function validateStrategyPackage(dirPath: string): Promise<ValidateResult> {
556
527
  const errors: string[] = [];
@@ -561,7 +532,6 @@ export async function validateStrategyPackage(dirPath: string): Promise<Validate
561
532
  const scriptDir = path.join(normalizedDir, "scripts");
562
533
  const strategyPath = path.join(scriptDir, "strategy.py");
563
534
 
564
- // ── 检查必需文件 ──
565
535
  let fepContent: string;
566
536
  try {
567
537
  const raw = await readFile(fepPath, "utf-8");
@@ -580,10 +550,8 @@ export async function validateStrategyPackage(dirPath: string): Promise<Validate
580
550
  return { valid: false, errors };
581
551
  }
582
552
 
583
- // ── 验证 fep.yaml ──
584
553
  const { hasUniverse } = validateFepYaml(fepContent, errors, warnings);
585
554
 
586
- // ── 验证 strategy.py ──
587
555
  validateStrategyScript(strategyContent, hasUniverse, errors, warnings);
588
556
 
589
557
  return {
@@ -591,4 +559,4 @@ export async function validateStrategyPackage(dirPath: string): Promise<Validate
591
559
  errors,
592
560
  warnings: warnings.length > 0 ? warnings : undefined,
593
561
  };
594
- }
562
+ }
package/src/types.ts CHANGED
@@ -492,3 +492,45 @@ export interface LeaderboardResponse {
492
492
  total: number;
493
493
  cachedAt: string;
494
494
  }
495
+
496
+ // ─────────────────────────────────────────────────────────────
497
+ // DataHub 行情数据类型
498
+ // ─────────────────────────────────────────────────────────────
499
+
500
+ /** 市场类型 */
501
+ export type MarketType = "crypto" | "equity";
502
+
503
+ /** OHLCV K线数据 */
504
+ export interface OHLCV {
505
+ timestamp: number;
506
+ open: number;
507
+ high: number;
508
+ low: number;
509
+ close: number;
510
+ volume: number;
511
+ }
512
+
513
+ /** 行情快照 */
514
+ export interface Ticker {
515
+ symbol: string;
516
+ market: MarketType;
517
+ last: number;
518
+ volume24h?: number;
519
+ timestamp: number;
520
+ }
521
+
522
+ // ─────────────────────────────────────────────────────────────
523
+ // 统一插件配置
524
+ // ─────────────────────────────────────────────────────────────
525
+
526
+ /** 统一插件配置 */
527
+ export interface UnifiedPluginConfig {
528
+ /** API Key (用于 Hub 和 DataHub) */
529
+ apiKey: string | undefined;
530
+ /** Hub API URL */
531
+ hubApiUrl: string;
532
+ /** DataHub Gateway URL */
533
+ datahubGatewayUrl: string;
534
+ /** 请求超时(毫秒) */
535
+ requestTimeoutMs: number;
536
+ }
package/LICENSE DELETED
@@ -1,21 +0,0 @@
1
- MIT License
2
-
3
- Copyright (c) 2025 Peter Steinberger
4
-
5
- Permission is hereby granted, free of charge, to any person obtaining a copy
6
- of this software and associated documentation files (the "Software"), to deal
7
- in the Software without restriction, including without limitation the rights
8
- to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
- copies of the Software, and to permit persons to whom the Software is
10
- furnished to do so, subject to the following conditions:
11
-
12
- The above copyright notice and this permission notice shall be included in all
13
- copies or substantial portions of the Software.
14
-
15
- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
- SOFTWARE.
@@ -1,109 +0,0 @@
1
- import { describe, expect, it } from "vitest";
2
- import {
3
- slugifyName,
4
- extractShortId,
5
- generateForkDirName,
6
- generateCreatedDirName,
7
- parseStrategyId,
8
- formatDate,
9
- } from "./strategy-storage.js";
10
-
11
- describe("slugifyName", () => {
12
- it("converts to lowercase", () => {
13
- expect(slugifyName("BTC Strategy")).toBe("btc-strategy");
14
- });
15
-
16
- it("replaces spaces with hyphens", () => {
17
- expect(slugifyName("my cool strategy")).toBe("my-cool-strategy");
18
- });
19
-
20
- it("replaces underscores with hyphens", () => {
21
- expect(slugifyName("my_cool_strategy")).toBe("my-cool-strategy");
22
- });
23
-
24
- it("removes special characters", () => {
25
- expect(slugifyName("BTC@Strategy#123!")).toBe("btcstrategy123");
26
- });
27
-
28
- it("limits length to 40 characters", () => {
29
- const longName = "a".repeat(50);
30
- expect(slugifyName(longName)).toHaveLength(40);
31
- });
32
-
33
- it("handles multiple consecutive spaces", () => {
34
- expect(slugifyName("my strategy")).toBe("my-strategy");
35
- });
36
-
37
- it("strips leading and trailing hyphens", () => {
38
- expect(slugifyName("-my-strategy-")).toBe("my-strategy");
39
- });
40
- });
41
-
42
- describe("extractShortId", () => {
43
- it("extracts first 8 chars from UUID", () => {
44
- expect(extractShortId("34a5792f-7d20-4a15-90f3-26f1c54fa4a6")).toBe("34a5792f");
45
- });
46
-
47
- it("handles short input", () => {
48
- expect(extractShortId("abc")).toBe("abc");
49
- });
50
-
51
- it("converts to lowercase", () => {
52
- expect(extractShortId("ABC12345-XXXX-XXXX-XXXX-XXXXXXXXXXXX")).toBe("abc12345");
53
- });
54
- });
55
-
56
- describe("generateForkDirName", () => {
57
- it("combines slug and short ID", () => {
58
- expect(generateForkDirName("BTC Strategy", "34a5792f-7d20-4a15-90f3-26f1c54fa4a6")).toBe(
59
- "btc-strategy-34a5792f",
60
- );
61
- });
62
-
63
- it("handles long names", () => {
64
- const longName = "A".repeat(50);
65
- const result = generateForkDirName(longName, "12345678-XXXX-XXXX-XXXX-XXXXXXXXXXXX");
66
- expect(result).toMatch(/^a+-12345678$/);
67
- expect(result.length).toBeLessThanOrEqual(49); // 40 + 1 + 8
68
- });
69
- });
70
-
71
- describe("generateCreatedDirName", () => {
72
- it("returns slugified name", () => {
73
- expect(generateCreatedDirName("My New Strategy")).toBe("my-new-strategy");
74
- });
75
- });
76
-
77
- describe("parseStrategyId", () => {
78
- it("extracts ID from Hub URL", () => {
79
- expect(
80
- parseStrategyId("https://hub.openfinclaw.ai/strategy/34a5792f-7d20-4a15-90f3-26f1c54fa4a6"),
81
- ).toBe("34a5792f-7d20-4a15-90f3-26f1c54fa4a6");
82
- });
83
-
84
- it("normalizes full UUID to lowercase", () => {
85
- expect(parseStrategyId("34A5792F-7D20-4A15-90F3-26F1C54FA4A6")).toBe(
86
- "34a5792f-7d20-4a15-90f3-26f1c54fa4a6",
87
- );
88
- });
89
-
90
- it("returns short ID as-is (lowercase)", () => {
91
- expect(parseStrategyId("34A5792F")).toBe("34a5792f");
92
- });
93
-
94
- it("handles whitespace", () => {
95
- expect(parseStrategyId(" 34a5792f ")).toBe("34a5792f");
96
- });
97
- });
98
-
99
- describe("formatDate", () => {
100
- it("formats date as YYYY-MM-DD", () => {
101
- const date = new Date("2026-03-16T10:00:00Z");
102
- expect(formatDate(date)).toBe("2026-03-16");
103
- });
104
-
105
- it("pads month and day", () => {
106
- const date = new Date("2026-01-05T10:00:00Z");
107
- expect(formatDate(date)).toBe("2026-01-05");
108
- });
109
- });