@dshtrading/strategies 0.1.4 → 0.1.6
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/lib/builtin-tombstones-fs.d.ts +5 -0
- package/lib/builtin-tombstones-fs.js +75 -0
- package/lib/builtin-tombstones.d.ts +24 -0
- package/lib/builtin-tombstones.js +16 -0
- package/lib/custom-fs.js +15 -3
- package/lib/custom-screener-fs.d.ts +5 -0
- package/lib/custom-screener-fs.js +85 -0
- package/lib/custom-screener.d.ts +31 -0
- package/lib/custom-screener.js +16 -0
- package/lib/custom.d.ts +2 -1
- package/lib/index.d.ts +7 -2
- package/lib/index.js +8 -3
- package/lib/management.d.ts +46 -0
- package/lib/management.js +135 -0
- package/lib/paradigms/bollinger-reversion.js +46 -14
- package/lib/paradigms/donchian-breakout.js +12 -6
- package/lib/paradigms/ema-crossover.js +28 -15
- package/lib/paradigms/momentum-12m.js +22 -13
- package/lib/paradigms/rsi-reversion.js +22 -9
- package/lib/paradigms/sma-baseline.js +24 -15
- package/lib/plugin.d.ts +100 -6
- package/lib/plugin.js +370 -29
- package/lib/price-format.d.ts +16 -0
- package/lib/price-format.js +25 -0
- package/lib/screeners/above-ma.js +12 -7
- package/lib/screeners/ma-bull-align.js +14 -8
- package/lib/screeners/rsi-oversold.js +22 -6
- package/lib/screeners/volume-breakout.js +12 -7
- package/lib/validate-node.d.ts +10 -0
- package/lib/validate-node.js +19 -2
- package/lib/validate.d.ts +22 -1
- package/lib/validate.js +292 -76
- package/package.json +4 -3
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import { dirname } from "node:path";
|
|
2
|
+
import { mkdir, readFile, rename, unlink, writeFile } from "node:fs/promises";
|
|
3
|
+
//#region src/builtin-tombstones-fs.ts
|
|
4
|
+
/**
|
|
5
|
+
* 文件持久化版内置策略/选股器墓碑存储(Node.js 宿主侧使用,
|
|
6
|
+
* 落 ~/.dsh/strategies/builtin-tombstones.json,形状 { deleted: string[] })。
|
|
7
|
+
*
|
|
8
|
+
* 与 custom-fs.ts 同款 tmp + rename 原子写入模式;旧宿主从未写过该文件,
|
|
9
|
+
* 无迁移问题(ENOENT 视为空表)。
|
|
10
|
+
*/
|
|
11
|
+
function createFileBuiltinTombstonesStore(filePath) {
|
|
12
|
+
let cache = null;
|
|
13
|
+
async function load() {
|
|
14
|
+
if (cache !== null) return cache;
|
|
15
|
+
try {
|
|
16
|
+
const content = await readFile(filePath, "utf8");
|
|
17
|
+
const parsed = JSON.parse(content);
|
|
18
|
+
const set = /* @__PURE__ */ new Set();
|
|
19
|
+
if (Array.isArray(parsed?.deleted)) {
|
|
20
|
+
for (const id of parsed.deleted) if (typeof id === "string" && id) set.add(id);
|
|
21
|
+
}
|
|
22
|
+
cache = set;
|
|
23
|
+
return set;
|
|
24
|
+
} catch (err) {
|
|
25
|
+
if (err?.code !== "ENOENT") console.error(`[dsh-trading/strategies] failed to read builtin tombstones from ${filePath}:`, err);
|
|
26
|
+
cache = /* @__PURE__ */ new Set();
|
|
27
|
+
return cache;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
async function flush(set) {
|
|
31
|
+
const dir = dirname(filePath);
|
|
32
|
+
const tmpPath = `${filePath}.tmp.${Date.now()}.${Math.random().toString(36).slice(2, 8)}`;
|
|
33
|
+
const data = JSON.stringify({ deleted: [...set] }, null, 2);
|
|
34
|
+
try {
|
|
35
|
+
await mkdir(dir, { recursive: true });
|
|
36
|
+
await writeFile(tmpPath, data, "utf8");
|
|
37
|
+
let lastError;
|
|
38
|
+
for (let attempt = 0; attempt < 3; attempt++) try {
|
|
39
|
+
await rename(tmpPath, filePath);
|
|
40
|
+
return;
|
|
41
|
+
} catch (err) {
|
|
42
|
+
if (err?.code !== "EPERM" && err?.code !== "EBUSY") throw err;
|
|
43
|
+
lastError = err;
|
|
44
|
+
await new Promise((resolve) => setTimeout(resolve, 25 * (attempt + 1)));
|
|
45
|
+
}
|
|
46
|
+
throw lastError;
|
|
47
|
+
} catch (error) {
|
|
48
|
+
console.error(`[dsh-trading/strategies] failed to atomic flush builtin tombstones to ${filePath}:`, error);
|
|
49
|
+
await unlink(tmpPath).catch(() => {});
|
|
50
|
+
throw error;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
return {
|
|
54
|
+
async list() {
|
|
55
|
+
return [...await load()];
|
|
56
|
+
},
|
|
57
|
+
async add(id) {
|
|
58
|
+
const set = await load();
|
|
59
|
+
const fresh = !set.has(id);
|
|
60
|
+
if (fresh) {
|
|
61
|
+
set.add(id);
|
|
62
|
+
await flush(set);
|
|
63
|
+
}
|
|
64
|
+
return fresh;
|
|
65
|
+
},
|
|
66
|
+
async remove(id) {
|
|
67
|
+
const set = await load();
|
|
68
|
+
const existed = set.delete(id);
|
|
69
|
+
if (existed) await flush(set);
|
|
70
|
+
return existed;
|
|
71
|
+
}
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
//#endregion
|
|
75
|
+
export { createFileBuiltinTombstonesStore };
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
//#region src/builtin-tombstones.d.ts
|
|
2
|
+
/**
|
|
3
|
+
* 内置策略/选股器墓碑存储(策略管理,2026-09-07)。
|
|
4
|
+
*
|
|
5
|
+
* 语义(覆盖 + 墓碑模型):内置策略与选股器保持代码常量作为出厂默认;
|
|
6
|
+
* 用户「删除内置」不删代码,只落一条 id 墓碑——名册合成时剔除,恢复默认
|
|
7
|
+
* 即移除墓碑。策略(paradigm id)与选股器('scr.' 前缀 id)id 空间天然
|
|
8
|
+
* 隔离,共用同一墓碑表。覆盖(修改内置)不用墓碑——覆盖记录落在
|
|
9
|
+
* CustomStrategyStore(同 id 记录替换内置),此处只承载「删除」。
|
|
10
|
+
*
|
|
11
|
+
* 纯类型 + 内存版,零 Node 依赖,浏览器安全;file 版在 ./builtin-tombstones-fs.ts。
|
|
12
|
+
*/
|
|
13
|
+
interface BuiltinTombstonesStore {
|
|
14
|
+
/** 全部墓碑 id(策略与选股器混合,顺序 = 删除顺序)。 */
|
|
15
|
+
list(): Promise<string[]>;
|
|
16
|
+
/** 新增墓碑;返回是否为新删(重复删除幂等返回 false)。 */
|
|
17
|
+
add(id: string): Promise<boolean>;
|
|
18
|
+
/** 移除墓碑(恢复出厂);返回是否存在。 */
|
|
19
|
+
remove(id: string): Promise<boolean>;
|
|
20
|
+
}
|
|
21
|
+
/** 内存版墓碑存储(纯浏览器与单测用)。 */
|
|
22
|
+
declare function createMemoryBuiltinTombstonesStore(initial?: readonly string[]): BuiltinTombstonesStore;
|
|
23
|
+
//#endregion
|
|
24
|
+
export { BuiltinTombstonesStore, createMemoryBuiltinTombstonesStore };
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
//#region src/builtin-tombstones.ts
|
|
2
|
+
/** 内存版墓碑存储(纯浏览器与单测用)。 */
|
|
3
|
+
function createMemoryBuiltinTombstonesStore(initial = []) {
|
|
4
|
+
const tombstones = new Set(initial);
|
|
5
|
+
return {
|
|
6
|
+
list: async () => [...tombstones],
|
|
7
|
+
add: async (id) => {
|
|
8
|
+
const fresh = !tombstones.has(id);
|
|
9
|
+
tombstones.add(id);
|
|
10
|
+
return fresh;
|
|
11
|
+
},
|
|
12
|
+
remove: async (id) => tombstones.delete(id)
|
|
13
|
+
};
|
|
14
|
+
}
|
|
15
|
+
//#endregion
|
|
16
|
+
export { createMemoryBuiltinTombstonesStore };
|
package/lib/custom-fs.js
CHANGED
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
import { dirname } from "node:path";
|
|
2
|
-
import { mkdir, readFile, rename, unlink, writeFile } from "node:fs/promises";
|
|
2
|
+
import { appendFile, mkdir, readFile, rename, unlink, writeFile } from "node:fs/promises";
|
|
3
3
|
//#region src/custom-fs.ts
|
|
4
4
|
/**
|
|
5
5
|
* 文件持久化版自定义策略存储(Node.js 宿主侧使用,落 ~/.dsh/strategies/custom.json)。
|
|
6
6
|
*
|
|
7
7
|
* 与 indicators/src/custom-fs.ts 同款 tmp + rename 原子写入模式(issue #31 规格)。
|
|
8
|
+
* remove(archive=true) 会先把被删记录归档到 sidecar 文件(<path>.archive.jsonl,
|
|
9
|
+
* JSONL 追加)——策略管理(覆盖 + 墓碑)丢弃用户 override 时可找回(2026-09-07)。
|
|
8
10
|
*/
|
|
9
11
|
function createFileCustomStrategyStore(filePath) {
|
|
10
12
|
let cache = null;
|
|
@@ -60,10 +62,20 @@ function createFileCustomStrategyStore(filePath) {
|
|
|
60
62
|
map.set(record.id, { ...record });
|
|
61
63
|
await flush(map);
|
|
62
64
|
},
|
|
63
|
-
async remove(id) {
|
|
65
|
+
async remove(id, archive = false) {
|
|
64
66
|
const map = await load();
|
|
67
|
+
const removedRecord = map.get(id);
|
|
65
68
|
const existed = map.delete(id);
|
|
66
|
-
if (existed)
|
|
69
|
+
if (existed) {
|
|
70
|
+
if (archive && removedRecord !== void 0) try {
|
|
71
|
+
const archivePath = `${filePath}.archive.jsonl`;
|
|
72
|
+
await mkdir(dirname(filePath), { recursive: true });
|
|
73
|
+
await appendFile(archivePath, `${JSON.stringify(removedRecord)}\n`);
|
|
74
|
+
} catch (error) {
|
|
75
|
+
console.error("[dsh-trading/strategies] failed to archive removed strategy record:", error);
|
|
76
|
+
}
|
|
77
|
+
await flush(map);
|
|
78
|
+
}
|
|
67
79
|
return existed;
|
|
68
80
|
}
|
|
69
81
|
};
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import { dirname } from "node:path";
|
|
2
|
+
import { appendFile, mkdir, readFile, rename, unlink, writeFile } from "node:fs/promises";
|
|
3
|
+
//#region src/custom-screener-fs.ts
|
|
4
|
+
/**
|
|
5
|
+
* 文件持久化版自定义选股器存储(Node.js 宿主侧使用,
|
|
6
|
+
* 落 ~/.dsh/strategies/custom-screeners.json)。
|
|
7
|
+
*
|
|
8
|
+
* 与 custom-fs.ts 同款 tmp + rename 原子写入模式。
|
|
9
|
+
* remove(archive=true) 先把被删记录归档到 sidecar(<path>.archive.jsonl,JSONL
|
|
10
|
+
* 追加)——选股器管理丢弃用户 override 时可找回(2026-09-07 审查补强)。
|
|
11
|
+
*/
|
|
12
|
+
function createFileCustomScreenerStore(filePath) {
|
|
13
|
+
let cache = null;
|
|
14
|
+
async function load() {
|
|
15
|
+
if (cache !== null) return cache;
|
|
16
|
+
try {
|
|
17
|
+
const content = await readFile(filePath, "utf8");
|
|
18
|
+
const parsed = JSON.parse(content);
|
|
19
|
+
const map = /* @__PURE__ */ new Map();
|
|
20
|
+
if (Array.isArray(parsed)) {
|
|
21
|
+
for (const item of parsed) if (item && typeof item.id === "string") map.set(item.id, item);
|
|
22
|
+
}
|
|
23
|
+
cache = map;
|
|
24
|
+
return map;
|
|
25
|
+
} catch (err) {
|
|
26
|
+
if (err?.code !== "ENOENT") console.error(`[dsh-trading/strategies] failed to read custom screeners from ${filePath}:`, err);
|
|
27
|
+
cache = /* @__PURE__ */ new Map();
|
|
28
|
+
return cache;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
async function flush(map) {
|
|
32
|
+
const dir = dirname(filePath);
|
|
33
|
+
const tmpPath = `${filePath}.tmp.${Date.now()}.${Math.random().toString(36).slice(2, 8)}`;
|
|
34
|
+
const data = JSON.stringify([...map.values()], null, 2);
|
|
35
|
+
try {
|
|
36
|
+
await mkdir(dir, { recursive: true });
|
|
37
|
+
await writeFile(tmpPath, data, "utf8");
|
|
38
|
+
let lastError;
|
|
39
|
+
for (let attempt = 0; attempt < 3; attempt++) try {
|
|
40
|
+
await rename(tmpPath, filePath);
|
|
41
|
+
return;
|
|
42
|
+
} catch (err) {
|
|
43
|
+
if (err?.code !== "EPERM" && err?.code !== "EBUSY") throw err;
|
|
44
|
+
lastError = err;
|
|
45
|
+
await new Promise((resolve) => setTimeout(resolve, 25 * (attempt + 1)));
|
|
46
|
+
}
|
|
47
|
+
throw lastError;
|
|
48
|
+
} catch (error) {
|
|
49
|
+
console.error(`[dsh-trading/strategies] failed to atomic flush custom screeners to ${filePath}:`, error);
|
|
50
|
+
await unlink(tmpPath).catch(() => {});
|
|
51
|
+
throw error;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
return {
|
|
55
|
+
async list() {
|
|
56
|
+
return [...(await load()).values()];
|
|
57
|
+
},
|
|
58
|
+
async get(id) {
|
|
59
|
+
return (await load()).get(id);
|
|
60
|
+
},
|
|
61
|
+
async save(record) {
|
|
62
|
+
const map = await load();
|
|
63
|
+
map.set(record.id, { ...record });
|
|
64
|
+
await flush(map);
|
|
65
|
+
},
|
|
66
|
+
async remove(id, archive = false) {
|
|
67
|
+
const map = await load();
|
|
68
|
+
const removedRecord = map.get(id);
|
|
69
|
+
const existed = map.delete(id);
|
|
70
|
+
if (existed) {
|
|
71
|
+
if (archive && removedRecord !== void 0) try {
|
|
72
|
+
const archivePath = `${filePath}.archive.jsonl`;
|
|
73
|
+
await mkdir(dirname(filePath), { recursive: true });
|
|
74
|
+
await appendFile(archivePath, `${JSON.stringify(removedRecord)}\n`);
|
|
75
|
+
} catch (error) {
|
|
76
|
+
console.error("[dsh-trading/strategies] failed to archive removed screener record:", error);
|
|
77
|
+
}
|
|
78
|
+
await flush(map);
|
|
79
|
+
}
|
|
80
|
+
return existed;
|
|
81
|
+
}
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
//#endregion
|
|
85
|
+
export { createFileCustomScreenerStore };
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { StrategyHorizon } from "./types.js";
|
|
2
|
+
//#region src/custom-screener.d.ts
|
|
3
|
+
interface CustomScreenerRecord {
|
|
4
|
+
/** 选股器唯一 ID,强制 'scr.' 前缀(如 'scr.custom-momentum') */
|
|
5
|
+
id: string;
|
|
6
|
+
/** 展示名,如 '量价双确认' */
|
|
7
|
+
title: string;
|
|
8
|
+
/** 预留与策略记录同构的期限词汇(选股器当前不分组,恒 'swing') */
|
|
9
|
+
horizon: StrategyHorizon;
|
|
10
|
+
/** 一句话思路 */
|
|
11
|
+
summary: string;
|
|
12
|
+
/** 参数规格数组(StrategyParamSpec[])的 JSON 字符串 */
|
|
13
|
+
paramsJson: string;
|
|
14
|
+
/** 结果动态列(ScreenerColumnSpec[])的 JSON 字符串 */
|
|
15
|
+
columnsJson: string;
|
|
16
|
+
/** JavaScript 纯函数源码,接收 (bars, params) 返回 ScreenerMatch | null */
|
|
17
|
+
evaluateSource: string;
|
|
18
|
+
/** 创建时间戳 */
|
|
19
|
+
createdAt: number;
|
|
20
|
+
}
|
|
21
|
+
interface CustomScreenerStore {
|
|
22
|
+
list(): Promise<CustomScreenerRecord[]>;
|
|
23
|
+
get(id: string): Promise<CustomScreenerRecord | undefined>;
|
|
24
|
+
save(record: CustomScreenerRecord): Promise<void>;
|
|
25
|
+
/** archive = 删除前把记录归档(file 版落 .archive.jsonl;memory 版忽略)。 */
|
|
26
|
+
remove(id: string, archive?: boolean): Promise<boolean>;
|
|
27
|
+
}
|
|
28
|
+
/** 内存版自定义选股器存储(纯浏览器与单测用)。 */
|
|
29
|
+
declare function createMemoryCustomScreenerStore(initial?: CustomScreenerRecord[]): CustomScreenerStore;
|
|
30
|
+
//#endregion
|
|
31
|
+
export { CustomScreenerRecord, CustomScreenerStore, createMemoryCustomScreenerStore };
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
//#region src/custom-screener.ts
|
|
2
|
+
/** 内存版自定义选股器存储(纯浏览器与单测用)。 */
|
|
3
|
+
function createMemoryCustomScreenerStore(initial = []) {
|
|
4
|
+
const map = /* @__PURE__ */ new Map();
|
|
5
|
+
for (const item of initial) map.set(item.id, item);
|
|
6
|
+
return {
|
|
7
|
+
list: async () => [...map.values()],
|
|
8
|
+
get: async (id) => map.get(id),
|
|
9
|
+
save: async (record) => {
|
|
10
|
+
map.set(record.id, { ...record });
|
|
11
|
+
},
|
|
12
|
+
remove: async (id) => map.delete(id)
|
|
13
|
+
};
|
|
14
|
+
}
|
|
15
|
+
//#endregion
|
|
16
|
+
export { createMemoryCustomScreenerStore };
|
package/lib/custom.d.ts
CHANGED
|
@@ -20,7 +20,8 @@ interface CustomStrategyStore {
|
|
|
20
20
|
list(): Promise<CustomStrategyRecord[]>;
|
|
21
21
|
get(id: string): Promise<CustomStrategyRecord | undefined>;
|
|
22
22
|
save(record: CustomStrategyRecord): Promise<void>;
|
|
23
|
-
|
|
23
|
+
/** archive = 删除前把记录归档(file 版落 .archive.jsonl;memory 版忽略)。 */
|
|
24
|
+
remove(id: string, archive?: boolean): Promise<boolean>;
|
|
24
25
|
}
|
|
25
26
|
/** 内存版自定义策略存储(纯浏览器与单测用)。 */
|
|
26
27
|
declare function createMemoryCustomStrategyStore(initial?: CustomStrategyRecord[]): CustomStrategyStore;
|
package/lib/index.d.ts
CHANGED
|
@@ -1,8 +1,12 @@
|
|
|
1
1
|
import { BacktestMetrics, BacktestOptions, BacktestResult, EquityPoint, Kline, SignalAction, StrategyDefinition, StrategyHorizon, StrategyParamSpec, StrategySignal, TradeRecord } from "./types.js";
|
|
2
2
|
import { run } from "./engine.js";
|
|
3
|
+
import { fmtPrice, priceDigits } from "./price-format.js";
|
|
3
4
|
import { CustomStrategyRecord, CustomStrategyStore, createMemoryCustomStrategyStore } from "./custom.js";
|
|
4
|
-
import { StrategyValidationResult, compileStrategySource, validateCustomStrategy, validateSignalSequence } from "./validate.js";
|
|
5
5
|
import { ScreenerColumnSpec, ScreenerDefinition, ScreenerMatch } from "./screeners/types.js";
|
|
6
|
+
import { CustomScreenerRecord, CustomScreenerStore, createMemoryCustomScreenerStore } from "./custom-screener.js";
|
|
7
|
+
import { ScreenerValidationResult, StrategyValidationResult, compileStrategySource, validateCustomScreener, validateCustomStrategy, validateSignalSequence } from "./validate.js";
|
|
8
|
+
import { BUILTIN_SCREENER_IDS, BUILTIN_STRATEGY_IDS, applyScreenerManagement, applyStrategyManagement, builtinScreenerRecord, builtinScreenerSource, builtinStrategyRecord, builtinStrategySource, isBuiltinScreenerId, isBuiltinStrategyId } from "./management.js";
|
|
9
|
+
import { BuiltinTombstonesStore, createMemoryBuiltinTombstonesStore } from "./builtin-tombstones.js";
|
|
6
10
|
import { getScreenerById, screenerParadigms } from "./screeners/index.js";
|
|
7
11
|
import { donchianBreakoutStrategy } from "./paradigms/donchian-breakout.js";
|
|
8
12
|
import { rsiReversionStrategy } from "./paradigms/rsi-reversion.js";
|
|
@@ -11,4 +15,5 @@ import { bollingerReversionStrategy } from "./paradigms/bollinger-reversion.js";
|
|
|
11
15
|
import { smaBaselineStrategy } from "./paradigms/sma-baseline.js";
|
|
12
16
|
import { momentum12mStrategy } from "./paradigms/momentum-12m.js";
|
|
13
17
|
import { getStrategyById, strategyParadigms } from "./paradigms/index.js";
|
|
14
|
-
|
|
18
|
+
import { workerComputeRunner } from "@dshtrading/indicators";
|
|
19
|
+
export { BUILTIN_SCREENER_IDS, BUILTIN_STRATEGY_IDS, type BacktestMetrics, type BacktestOptions, type BacktestResult, type BuiltinTombstonesStore, type CustomScreenerRecord, type CustomScreenerStore, type CustomStrategyRecord, type CustomStrategyStore, type EquityPoint, type Kline, type ScreenerColumnSpec, type ScreenerDefinition, type ScreenerMatch, type ScreenerValidationResult, type SignalAction, type StrategyDefinition, type StrategyHorizon, type StrategyParamSpec, type StrategySignal, type StrategyValidationResult, type TradeRecord, applyScreenerManagement, applyStrategyManagement, bollingerReversionStrategy, builtinScreenerRecord, builtinScreenerSource, builtinStrategyRecord, builtinStrategySource, compileStrategySource, createMemoryBuiltinTombstonesStore, createMemoryCustomScreenerStore, createMemoryCustomStrategyStore, donchianBreakoutStrategy, emaCrossoverStrategy, fmtPrice, getScreenerById, getStrategyById, isBuiltinScreenerId, isBuiltinStrategyId, momentum12mStrategy, priceDigits, rsiReversionStrategy, run, screenerParadigms, smaBaselineStrategy, strategyParadigms, validateCustomScreener, validateCustomStrategy, validateSignalSequence, workerComputeRunner };
|
package/lib/index.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { run } from "./engine.js";
|
|
2
|
+
import { fmtPrice, priceDigits } from "./price-format.js";
|
|
2
3
|
import { createMemoryCustomStrategyStore } from "./custom.js";
|
|
3
|
-
import { compileStrategySource, validateCustomStrategy, validateSignalSequence } from "./validate.js";
|
|
4
|
-
import { getScreenerById, screenerParadigms } from "./screeners/index.js";
|
|
4
|
+
import { compileStrategySource, validateCustomScreener, validateCustomStrategy, validateSignalSequence } from "./validate.js";
|
|
5
5
|
import { donchianBreakoutStrategy } from "./paradigms/donchian-breakout.js";
|
|
6
6
|
import { rsiReversionStrategy } from "./paradigms/rsi-reversion.js";
|
|
7
7
|
import { emaCrossoverStrategy } from "./paradigms/ema-crossover.js";
|
|
@@ -9,4 +9,9 @@ import { bollingerReversionStrategy } from "./paradigms/bollinger-reversion.js";
|
|
|
9
9
|
import { smaBaselineStrategy } from "./paradigms/sma-baseline.js";
|
|
10
10
|
import { momentum12mStrategy } from "./paradigms/momentum-12m.js";
|
|
11
11
|
import { getStrategyById, strategyParadigms } from "./paradigms/index.js";
|
|
12
|
-
|
|
12
|
+
import { getScreenerById, screenerParadigms } from "./screeners/index.js";
|
|
13
|
+
import { BUILTIN_SCREENER_IDS, BUILTIN_STRATEGY_IDS, applyScreenerManagement, applyStrategyManagement, builtinScreenerRecord, builtinScreenerSource, builtinStrategyRecord, builtinStrategySource, isBuiltinScreenerId, isBuiltinStrategyId } from "./management.js";
|
|
14
|
+
import { createMemoryBuiltinTombstonesStore } from "./builtin-tombstones.js";
|
|
15
|
+
import { createMemoryCustomScreenerStore } from "./custom-screener.js";
|
|
16
|
+
import { workerComputeRunner } from "@dshtrading/indicators";
|
|
17
|
+
export { BUILTIN_SCREENER_IDS, BUILTIN_STRATEGY_IDS, applyScreenerManagement, applyStrategyManagement, bollingerReversionStrategy, builtinScreenerRecord, builtinScreenerSource, builtinStrategyRecord, builtinStrategySource, compileStrategySource, createMemoryBuiltinTombstonesStore, createMemoryCustomScreenerStore, createMemoryCustomStrategyStore, donchianBreakoutStrategy, emaCrossoverStrategy, fmtPrice, getScreenerById, getStrategyById, isBuiltinScreenerId, isBuiltinStrategyId, momentum12mStrategy, priceDigits, rsiReversionStrategy, run, screenerParadigms, smaBaselineStrategy, strategyParadigms, validateCustomScreener, validateCustomStrategy, validateSignalSequence, workerComputeRunner };
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { StrategyDefinition } from "./types.js";
|
|
2
|
+
import { CustomStrategyRecord } from "./custom.js";
|
|
3
|
+
import { ScreenerDefinition } from "./screeners/types.js";
|
|
4
|
+
import { CustomScreenerRecord } from "./custom-screener.js";
|
|
5
|
+
//#region src/management.d.ts
|
|
6
|
+
/** 内置范式策略 id 集合(稳定词汇;选股器 id 带 'scr.' 前缀,不在此列)。 */
|
|
7
|
+
declare const BUILTIN_STRATEGY_IDS: ReadonlySet<string>;
|
|
8
|
+
/** 内置选股器 id 集合('scr.' 前缀词汇)。 */
|
|
9
|
+
declare const BUILTIN_SCREENER_IDS: ReadonlySet<string>;
|
|
10
|
+
declare function isBuiltinStrategyId(id: string): boolean;
|
|
11
|
+
declare function isBuiltinScreenerId(id: string): boolean;
|
|
12
|
+
/**
|
|
13
|
+
* 名册合成:内置 − 墓碑,覆盖记录原位替换同 id 内置,自定义按传入顺序追加。
|
|
14
|
+
* @param paradigms 出厂默认名册(生产传 strategyParadigms,单测可传定制名册)
|
|
15
|
+
* @param defs 自定义 store 校验后的定义(含覆盖内置的同 id 记录)
|
|
16
|
+
* @param deletedIds 墓碑 id(内置删除标记;命中者从名册剔除)
|
|
17
|
+
*/
|
|
18
|
+
declare function applyStrategyManagement(paradigms: readonly StrategyDefinition[], defs: readonly StrategyDefinition[], deletedIds: readonly string[]): StrategyDefinition[];
|
|
19
|
+
/**
|
|
20
|
+
* 内置策略 compute 源码导出(GUI「编辑内置」预填用)。
|
|
21
|
+
*
|
|
22
|
+
* 前提:内置 compute 一律自包含(指标数学内联,不引用外部闭包),方法速记
|
|
23
|
+
* 形态 `compute(bars, params) { ... }` 归一为箭头形态 `(bars, params) => { ... }`
|
|
24
|
+
* ——后者是 compileStrategySource 直接接受的形状。打包器若改写为箭头/function
|
|
25
|
+
* 形态则原样透传(同样被 compileStrategySource 支持)。
|
|
26
|
+
*/
|
|
27
|
+
declare function builtinStrategySource(def: StrategyDefinition): string;
|
|
28
|
+
/**
|
|
29
|
+
* 内置策略 → 可编辑记录预填(GUI 编辑器初值)。params 序列化保留 step,
|
|
30
|
+
* 让覆盖保存后参数步进与出厂一致(校验器接受合法 step)。
|
|
31
|
+
*/
|
|
32
|
+
declare function builtinStrategyRecord(def: StrategyDefinition): CustomStrategyRecord;
|
|
33
|
+
/**
|
|
34
|
+
* 选股器名册合成(覆盖 + 墓碑,与 applyStrategyManagement 同模型):
|
|
35
|
+
* 内置 − 墓碑,覆盖记录原位替换同 id 内置,自定义按传入顺序追加。
|
|
36
|
+
*/
|
|
37
|
+
declare function applyScreenerManagement(builtins: readonly ScreenerDefinition[], defs: readonly ScreenerDefinition[], deletedIds: readonly string[]): ScreenerDefinition[];
|
|
38
|
+
/**
|
|
39
|
+
* 内置选股器 evaluate 源码导出(GUI「编辑内置」预填用),归一化规则同
|
|
40
|
+
* builtinStrategySource:方法速记 `evaluate(bars, params) { ... }` → 箭头形态。
|
|
41
|
+
*/
|
|
42
|
+
declare function builtinScreenerSource(def: ScreenerDefinition): string;
|
|
43
|
+
/** 内置选股器 → 可编辑记录预填(columns 序列化保真)。 */
|
|
44
|
+
declare function builtinScreenerRecord(def: ScreenerDefinition): CustomScreenerRecord;
|
|
45
|
+
//#endregion
|
|
46
|
+
export { BUILTIN_SCREENER_IDS, BUILTIN_STRATEGY_IDS, applyScreenerManagement, applyStrategyManagement, builtinScreenerRecord, builtinScreenerSource, builtinStrategyRecord, builtinStrategySource, isBuiltinScreenerId, isBuiltinStrategyId };
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
import { strategyParadigms } from "./paradigms/index.js";
|
|
2
|
+
import { screenerParadigms } from "./screeners/index.js";
|
|
3
|
+
//#region src/management.ts
|
|
4
|
+
/**
|
|
5
|
+
* 策略管理名册合成与内置源码导出(策略管理,2026-09-07)。
|
|
6
|
+
*
|
|
7
|
+
* 覆盖 + 墓碑模型的唯一合成逻辑:内置范式(代码常量)为出厂默认层,
|
|
8
|
+
* 自定义 store 中同 id 的记录是覆盖层,墓碑表是删除层。agent 侧
|
|
9
|
+
* (resolveStrategyDefinition)与 GUI 侧(StrategyView)共用本模块,
|
|
10
|
+
* 保证两侧名册一致。纯函数、零 Node 依赖,浏览器安全。
|
|
11
|
+
*/
|
|
12
|
+
/** 内置范式策略 id 集合(稳定词汇;选股器 id 带 'scr.' 前缀,不在此列)。 */
|
|
13
|
+
const BUILTIN_STRATEGY_IDS = new Set(strategyParadigms.map((d) => d.id));
|
|
14
|
+
/** 内置选股器 id 集合('scr.' 前缀词汇)。 */
|
|
15
|
+
const BUILTIN_SCREENER_IDS = new Set(screenerParadigms.map((d) => d.id));
|
|
16
|
+
function isBuiltinStrategyId(id) {
|
|
17
|
+
return BUILTIN_STRATEGY_IDS.has(id);
|
|
18
|
+
}
|
|
19
|
+
function isBuiltinScreenerId(id) {
|
|
20
|
+
return BUILTIN_SCREENER_IDS.has(id);
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* 名册合成:内置 − 墓碑,覆盖记录原位替换同 id 内置,自定义按传入顺序追加。
|
|
24
|
+
* @param paradigms 出厂默认名册(生产传 strategyParadigms,单测可传定制名册)
|
|
25
|
+
* @param defs 自定义 store 校验后的定义(含覆盖内置的同 id 记录)
|
|
26
|
+
* @param deletedIds 墓碑 id(内置删除标记;命中者从名册剔除)
|
|
27
|
+
*/
|
|
28
|
+
function applyStrategyManagement(paradigms, defs, deletedIds) {
|
|
29
|
+
const deleted = new Set(deletedIds);
|
|
30
|
+
const overrides = /* @__PURE__ */ new Map();
|
|
31
|
+
const customs = [];
|
|
32
|
+
for (const def of defs) if (BUILTIN_STRATEGY_IDS.has(def.id)) overrides.set(def.id, def);
|
|
33
|
+
else customs.push(def);
|
|
34
|
+
const roster = [];
|
|
35
|
+
for (const builtin of paradigms) {
|
|
36
|
+
if (deleted.has(builtin.id)) continue;
|
|
37
|
+
roster.push(overrides.get(builtin.id) ?? builtin);
|
|
38
|
+
}
|
|
39
|
+
return [...roster, ...customs];
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* 内置策略 compute 源码导出(GUI「编辑内置」预填用)。
|
|
43
|
+
*
|
|
44
|
+
* 前提:内置 compute 一律自包含(指标数学内联,不引用外部闭包),方法速记
|
|
45
|
+
* 形态 `compute(bars, params) { ... }` 归一为箭头形态 `(bars, params) => { ... }`
|
|
46
|
+
* ——后者是 compileStrategySource 直接接受的形状。打包器若改写为箭头/function
|
|
47
|
+
* 形态则原样透传(同样被 compileStrategySource 支持)。
|
|
48
|
+
*/
|
|
49
|
+
function builtinStrategySource(def) {
|
|
50
|
+
const raw = def.compute.toString().trim();
|
|
51
|
+
const method = raw.match(/^compute\s*\(([^)]*)\)\s*\{([\s\S]*)\}$/);
|
|
52
|
+
if (method !== null) return `(${method[1]}) => {${method[2]}}`;
|
|
53
|
+
return raw;
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* 内置策略 → 可编辑记录预填(GUI 编辑器初值)。params 序列化保留 step,
|
|
57
|
+
* 让覆盖保存后参数步进与出厂一致(校验器接受合法 step)。
|
|
58
|
+
*/
|
|
59
|
+
function builtinStrategyRecord(def) {
|
|
60
|
+
const params = def.params.map((p) => ({
|
|
61
|
+
key: p.key,
|
|
62
|
+
label: p.label,
|
|
63
|
+
default: p.default,
|
|
64
|
+
min: p.min,
|
|
65
|
+
max: p.max,
|
|
66
|
+
step: p.step
|
|
67
|
+
}));
|
|
68
|
+
return {
|
|
69
|
+
id: def.id,
|
|
70
|
+
title: def.name,
|
|
71
|
+
horizon: def.horizon,
|
|
72
|
+
summary: def.summary,
|
|
73
|
+
paramsJson: JSON.stringify(params),
|
|
74
|
+
computeSource: builtinStrategySource(def),
|
|
75
|
+
createdAt: Date.now()
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* 选股器名册合成(覆盖 + 墓碑,与 applyStrategyManagement 同模型):
|
|
80
|
+
* 内置 − 墓碑,覆盖记录原位替换同 id 内置,自定义按传入顺序追加。
|
|
81
|
+
*/
|
|
82
|
+
function applyScreenerManagement(builtins, defs, deletedIds) {
|
|
83
|
+
const deleted = new Set(deletedIds);
|
|
84
|
+
const overrides = /* @__PURE__ */ new Map();
|
|
85
|
+
const customs = [];
|
|
86
|
+
for (const def of defs) if (BUILTIN_SCREENER_IDS.has(def.id)) overrides.set(def.id, def);
|
|
87
|
+
else customs.push(def);
|
|
88
|
+
const roster = [];
|
|
89
|
+
for (const builtin of builtins) {
|
|
90
|
+
if (deleted.has(builtin.id)) continue;
|
|
91
|
+
roster.push(overrides.get(builtin.id) ?? builtin);
|
|
92
|
+
}
|
|
93
|
+
return [...roster, ...customs];
|
|
94
|
+
}
|
|
95
|
+
/**
|
|
96
|
+
* 内置选股器 evaluate 源码导出(GUI「编辑内置」预填用),归一化规则同
|
|
97
|
+
* builtinStrategySource:方法速记 `evaluate(bars, params) { ... }` → 箭头形态。
|
|
98
|
+
*/
|
|
99
|
+
function builtinScreenerSource(def) {
|
|
100
|
+
const raw = def.evaluate.toString().trim();
|
|
101
|
+
const method = raw.match(/^evaluate\s*\(([^)]*)\)\s*\{([\s\S]*)\}$/);
|
|
102
|
+
if (method !== null) return `(${method[1]}) => {${method[2]}}`;
|
|
103
|
+
return raw;
|
|
104
|
+
}
|
|
105
|
+
/** 内置选股器 → 可编辑记录预填(columns 序列化保真)。 */
|
|
106
|
+
function builtinScreenerRecord(def) {
|
|
107
|
+
const params = def.params.map((p) => ({
|
|
108
|
+
key: p.key,
|
|
109
|
+
label: p.label,
|
|
110
|
+
default: p.default,
|
|
111
|
+
min: p.min,
|
|
112
|
+
max: p.max,
|
|
113
|
+
step: p.step
|
|
114
|
+
}));
|
|
115
|
+
const columns = def.columns.map((c) => c.format === void 0 ? {
|
|
116
|
+
key: c.key,
|
|
117
|
+
label: c.label
|
|
118
|
+
} : {
|
|
119
|
+
key: c.key,
|
|
120
|
+
label: c.label,
|
|
121
|
+
format: c.format
|
|
122
|
+
});
|
|
123
|
+
return {
|
|
124
|
+
id: def.id,
|
|
125
|
+
title: def.name,
|
|
126
|
+
horizon: "swing",
|
|
127
|
+
summary: def.summary,
|
|
128
|
+
paramsJson: JSON.stringify(params),
|
|
129
|
+
columnsJson: JSON.stringify(columns),
|
|
130
|
+
evaluateSource: builtinScreenerSource(def),
|
|
131
|
+
createdAt: Date.now()
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
//#endregion
|
|
135
|
+
export { BUILTIN_SCREENER_IDS, BUILTIN_STRATEGY_IDS, applyScreenerManagement, applyStrategyManagement, builtinScreenerRecord, builtinScreenerSource, builtinStrategyRecord, builtinStrategySource, isBuiltinScreenerId, isBuiltinStrategyId };
|
|
@@ -1,11 +1,4 @@
|
|
|
1
|
-
import { bollinger } from "@dshtrading/indicators";
|
|
2
1
|
//#region src/paradigms/bollinger-reversion.ts
|
|
3
|
-
/**
|
|
4
|
-
* 布林带下轨均值回归策略(波段)。
|
|
5
|
-
*
|
|
6
|
-
* 入场:收盘价跌破布林线下轨(超卖错杀)。
|
|
7
|
-
* 出场:收盘价回归至布林带中轨(均线目标位平仓)。
|
|
8
|
-
*/
|
|
9
2
|
const bollingerReversionStrategy = {
|
|
10
3
|
id: "bollinger-reversion",
|
|
11
4
|
horizon: "swing",
|
|
@@ -29,8 +22,47 @@ const bollingerReversionStrategy = {
|
|
|
29
22
|
compute(bars, params) {
|
|
30
23
|
const period = Math.max(5, Math.round(params.period ?? 20));
|
|
31
24
|
const k = Number(params.multiplier ?? 2);
|
|
25
|
+
const fmtPrice = (value) => {
|
|
26
|
+
if (!Number.isFinite(value)) return "—";
|
|
27
|
+
const abs = Math.abs(value);
|
|
28
|
+
if (abs < 1) return value.toFixed(abs >= .01 ? 4 : 6);
|
|
29
|
+
return value.toFixed(Math.abs(Number(value.toFixed(2)) - value) < 1e-9 ? 2 : 3);
|
|
30
|
+
};
|
|
31
|
+
const smaOf = (values, period) => {
|
|
32
|
+
const out = new Array(values.length).fill(void 0);
|
|
33
|
+
if (!Number.isFinite(period) || period < 1 || values.length < period) return out;
|
|
34
|
+
let sum = 0;
|
|
35
|
+
for (let index = 0; index < values.length; index++) {
|
|
36
|
+
sum += values[index];
|
|
37
|
+
if (index >= period) sum -= values[index - period];
|
|
38
|
+
if (index >= period - 1) out[index] = sum / period;
|
|
39
|
+
}
|
|
40
|
+
return out;
|
|
41
|
+
};
|
|
42
|
+
const stdevOf = (values, period) => {
|
|
43
|
+
const out = new Array(values.length).fill(void 0);
|
|
44
|
+
if (!Number.isFinite(period) || period < 1 || values.length < period) return out;
|
|
45
|
+
for (let index = period - 1; index < values.length; index++) {
|
|
46
|
+
let mean = 0;
|
|
47
|
+
for (let offset = 0; offset < period; offset++) mean += values[index - offset];
|
|
48
|
+
mean /= period;
|
|
49
|
+
let variance = 0;
|
|
50
|
+
for (let offset = 0; offset < period; offset++) {
|
|
51
|
+
const delta = values[index - offset] - mean;
|
|
52
|
+
variance += delta * delta;
|
|
53
|
+
}
|
|
54
|
+
out[index] = Math.sqrt(variance / period);
|
|
55
|
+
}
|
|
56
|
+
return out;
|
|
57
|
+
};
|
|
32
58
|
const closes = bars.map((b) => b.close);
|
|
33
|
-
const
|
|
59
|
+
const mid = smaOf(closes, period);
|
|
60
|
+
const sd = stdevOf(closes, period);
|
|
61
|
+
const lower = closes.map((_, index) => {
|
|
62
|
+
const base = mid[index];
|
|
63
|
+
const dev = sd[index];
|
|
64
|
+
return base === void 0 || dev === void 0 ? void 0 : base - k * dev;
|
|
65
|
+
});
|
|
34
66
|
const signals = [];
|
|
35
67
|
let inPosition = false;
|
|
36
68
|
for (let i = period - 1; i < bars.length; i++) {
|
|
@@ -45,11 +77,11 @@ const bollingerReversionStrategy = {
|
|
|
45
77
|
action: "entry",
|
|
46
78
|
direction: "long",
|
|
47
79
|
price: currentClose,
|
|
48
|
-
reason: `收盘价 (${currentClose
|
|
80
|
+
reason: `收盘价 (${fmtPrice(currentClose)}) 跌破布林下轨 (${fmtPrice(currentLower)}),触发波段均值回归`,
|
|
49
81
|
reasonKey: "strat.bollinger-reversion.reason.entry",
|
|
50
82
|
reasonParams: {
|
|
51
|
-
close: currentClose
|
|
52
|
-
band: currentLower
|
|
83
|
+
close: fmtPrice(currentClose),
|
|
84
|
+
band: fmtPrice(currentLower)
|
|
53
85
|
}
|
|
54
86
|
});
|
|
55
87
|
inPosition = true;
|
|
@@ -60,11 +92,11 @@ const bollingerReversionStrategy = {
|
|
|
60
92
|
action: "exit",
|
|
61
93
|
direction: "flat",
|
|
62
94
|
price: currentClose,
|
|
63
|
-
reason: `收盘价 (${currentClose
|
|
95
|
+
reason: `收盘价 (${fmtPrice(currentClose)}) 成功回归至布林中轨 (${fmtPrice(currentMid)}),完成目标止盈`,
|
|
64
96
|
reasonKey: "strat.bollinger-reversion.reason.exit",
|
|
65
97
|
reasonParams: {
|
|
66
|
-
close: currentClose
|
|
67
|
-
mid: currentMid
|
|
98
|
+
close: fmtPrice(currentClose),
|
|
99
|
+
mid: fmtPrice(currentMid)
|
|
68
100
|
}
|
|
69
101
|
});
|
|
70
102
|
inPosition = false;
|