@dshtrading/strategies 0.1.5 → 0.2.0
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 +55 -0
- package/lib/builtin-tombstones.d.ts +24 -0
- package/lib/builtin-tombstones.js +16 -0
- package/lib/custom-fs.js +17 -24
- package/lib/custom-screener-fs.d.ts +5 -0
- package/lib/custom-screener-fs.js +66 -0
- package/lib/custom-screener.d.ts +31 -0
- package/lib/custom-screener.js +8 -0
- package/lib/custom.d.ts +3 -2
- package/lib/custom.js +3 -11
- 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/memory-store.js +15 -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 +163 -7
- package/lib/plugin.js +816 -32
- 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 +17 -31
- package/lib/validate.d.ts +28 -3
- package/lib/validate.js +299 -83
- package/package.json +4 -3
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
//#region src/price-format.d.ts
|
|
2
|
+
/**
|
|
3
|
+
* 标的价格显示小数位(自适应,全局显示约定单一来源):
|
|
4
|
+
* ≥1 默认 2 位;当 2 位舍入会丢失第 3 位有效小数(如港股 0.001 tick、
|
|
5
|
+
* 低价权证/债券)时升到 3 位;<1 按量级 4/6 位(低价加密对)。
|
|
6
|
+
* 1e-9 容差只滤浮点表示噪声——常规价位下它比第 3 位小数量级低多个数量级。
|
|
7
|
+
*
|
|
8
|
+
* client-ui-trading / client-ui-strategies 经本包复用同一规则;范式策略的
|
|
9
|
+
* compute 因需自包含(compute.toString() 导出可编译源码)内联同式实现,
|
|
10
|
+
* 两处改动必须同步。
|
|
11
|
+
*/
|
|
12
|
+
declare function priceDigits(value: number | undefined): number;
|
|
13
|
+
/** 价格文本:undefined/NaN → '—',否则按 priceDigits 定点舍入。 */
|
|
14
|
+
declare function fmtPrice(value: number | undefined): string;
|
|
15
|
+
//#endregion
|
|
16
|
+
export { fmtPrice, priceDigits };
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
//#region src/price-format.ts
|
|
2
|
+
/**
|
|
3
|
+
* 标的价格显示小数位(自适应,全局显示约定单一来源):
|
|
4
|
+
* ≥1 默认 2 位;当 2 位舍入会丢失第 3 位有效小数(如港股 0.001 tick、
|
|
5
|
+
* 低价权证/债券)时升到 3 位;<1 按量级 4/6 位(低价加密对)。
|
|
6
|
+
* 1e-9 容差只滤浮点表示噪声——常规价位下它比第 3 位小数量级低多个数量级。
|
|
7
|
+
*
|
|
8
|
+
* client-ui-trading / client-ui-strategies 经本包复用同一规则;范式策略的
|
|
9
|
+
* compute 因需自包含(compute.toString() 导出可编译源码)内联同式实现,
|
|
10
|
+
* 两处改动必须同步。
|
|
11
|
+
*/
|
|
12
|
+
function priceDigits(value) {
|
|
13
|
+
if (value === void 0 || !Number.isFinite(value)) return 2;
|
|
14
|
+
const abs = Math.abs(value);
|
|
15
|
+
if (abs < 1) return abs >= .01 ? 4 : 6;
|
|
16
|
+
const rounded = Number(value.toFixed(2));
|
|
17
|
+
return Math.abs(rounded - value) < 1e-9 ? 2 : 3;
|
|
18
|
+
}
|
|
19
|
+
/** 价格文本:undefined/NaN → '—',否则按 priceDigits 定点舍入。 */
|
|
20
|
+
function fmtPrice(value) {
|
|
21
|
+
if (value === void 0 || !Number.isFinite(value)) return "—";
|
|
22
|
+
return value.toFixed(priceDigits(value));
|
|
23
|
+
}
|
|
24
|
+
//#endregion
|
|
25
|
+
export { fmtPrice, priceDigits };
|
|
@@ -1,9 +1,4 @@
|
|
|
1
|
-
import { sma } from "@dshtrading/indicators";
|
|
2
1
|
//#region src/screeners/above-ma.ts
|
|
3
|
-
/**
|
|
4
|
-
* 牛熊线之上:现价站上长期均线且均线自身斜率向上——与量化「200 日均线
|
|
5
|
-
* 牛熊择时基线」同源的长线标的筛选(Faber GTAA 的截面版)。
|
|
6
|
-
*/
|
|
7
2
|
const aboveMaScreener = {
|
|
8
3
|
id: "scr.above-ma",
|
|
9
4
|
name: "站上牛熊线",
|
|
@@ -37,8 +32,18 @@ const aboveMaScreener = {
|
|
|
37
32
|
const slopeBars = Math.max(5, Math.round(params.slopeBars ?? 20));
|
|
38
33
|
const i = bars.length - 1;
|
|
39
34
|
if (i < period + slopeBars - 1) return null;
|
|
40
|
-
const
|
|
41
|
-
|
|
35
|
+
const smaOf = (values, period) => {
|
|
36
|
+
const out = new Array(values.length).fill(void 0);
|
|
37
|
+
if (!Number.isFinite(period) || period < 1 || values.length < period) return out;
|
|
38
|
+
let sum = 0;
|
|
39
|
+
for (let index = 0; index < values.length; index++) {
|
|
40
|
+
sum += values[index];
|
|
41
|
+
if (index >= period) sum -= values[index - period];
|
|
42
|
+
if (index >= period - 1) out[index] = sum / period;
|
|
43
|
+
}
|
|
44
|
+
return out;
|
|
45
|
+
};
|
|
46
|
+
const maSeries = smaOf(bars.map((b) => b.close), period);
|
|
42
47
|
const ma = maSeries[i];
|
|
43
48
|
const maPrev = maSeries[i - slopeBars];
|
|
44
49
|
if (ma === void 0 || maPrev === void 0 || !(maPrev > 0)) return null;
|
|
@@ -1,9 +1,4 @@
|
|
|
1
|
-
import { sma } from "@dshtrading/indicators";
|
|
2
1
|
//#region src/screeners/ma-bull-align.ts
|
|
3
|
-
/**
|
|
4
|
-
* 均线多头排列:现价站上三线,且短中期均线自上而下依次压制
|
|
5
|
-
* (SMA(短) > SMA(中) > SMA(长)),趋势结构完整的顺势筛选。
|
|
6
|
-
*/
|
|
7
2
|
const maBullAlignScreener = {
|
|
8
3
|
id: "scr.ma-bull-align",
|
|
9
4
|
name: "均线多头排列",
|
|
@@ -45,10 +40,21 @@ const maBullAlignScreener = {
|
|
|
45
40
|
const n3 = Math.max(n2, Math.round(params.n3 ?? 120));
|
|
46
41
|
const i = bars.length - 1;
|
|
47
42
|
if (i + 1 < n3) return null;
|
|
43
|
+
const smaOf = (values, period) => {
|
|
44
|
+
const out = new Array(values.length).fill(void 0);
|
|
45
|
+
if (!Number.isFinite(period) || period < 1 || values.length < period) return out;
|
|
46
|
+
let sum = 0;
|
|
47
|
+
for (let index = 0; index < values.length; index++) {
|
|
48
|
+
sum += values[index];
|
|
49
|
+
if (index >= period) sum -= values[index - period];
|
|
50
|
+
if (index >= period - 1) out[index] = sum / period;
|
|
51
|
+
}
|
|
52
|
+
return out;
|
|
53
|
+
};
|
|
48
54
|
const closes = bars.map((b) => b.close);
|
|
49
|
-
const s1 =
|
|
50
|
-
const s2 =
|
|
51
|
-
const s3 =
|
|
55
|
+
const s1 = smaOf(closes, n1)[i];
|
|
56
|
+
const s2 = smaOf(closes, n2)[i];
|
|
57
|
+
const s3 = smaOf(closes, n3)[i];
|
|
52
58
|
if (s1 === void 0 || s2 === void 0 || s3 === void 0) return null;
|
|
53
59
|
const close = bars[i].close;
|
|
54
60
|
if (!(close > s1 && s1 > s2 && s2 > s3)) return null;
|
|
@@ -1,9 +1,4 @@
|
|
|
1
|
-
import { rsi } from "@dshtrading/indicators";
|
|
2
1
|
//#region src/screeners/rsi-oversold.ts
|
|
3
|
-
/**
|
|
4
|
-
* RSI 超卖:RSI(period) 跌入阈值以下的逆势关注筛选——超卖≠见底,
|
|
5
|
-
* 定位是「值得盯的反转候选池」,命中理由里明示是逆势信号。
|
|
6
|
-
*/
|
|
7
2
|
const rsiOversoldScreener = {
|
|
8
3
|
id: "scr.rsi-oversold",
|
|
9
4
|
name: "RSI 超卖",
|
|
@@ -32,7 +27,28 @@ const rsiOversoldScreener = {
|
|
|
32
27
|
const threshold = Math.min(50, Math.max(10, params.threshold ?? 30));
|
|
33
28
|
const i = bars.length - 1;
|
|
34
29
|
if (i < period) return null;
|
|
35
|
-
const
|
|
30
|
+
const rsiOf = (values, period) => {
|
|
31
|
+
const out = new Array(values.length).fill(void 0);
|
|
32
|
+
if (!Number.isFinite(period) || period < 1 || values.length < period) return out;
|
|
33
|
+
let gain = 0;
|
|
34
|
+
let loss = 0;
|
|
35
|
+
for (let index = 1; index <= period; index++) {
|
|
36
|
+
const delta = values[index] - values[index - 1];
|
|
37
|
+
if (delta >= 0) gain += delta;
|
|
38
|
+
else loss -= delta;
|
|
39
|
+
}
|
|
40
|
+
gain /= period;
|
|
41
|
+
loss /= period;
|
|
42
|
+
out[period] = loss === 0 ? 100 : 100 - 100 / (1 + gain / loss);
|
|
43
|
+
for (let index = period + 1; index < values.length; index++) {
|
|
44
|
+
const delta = values[index] - values[index - 1];
|
|
45
|
+
gain = (gain * (period - 1) + Math.max(delta, 0)) / period;
|
|
46
|
+
loss = (loss * (period - 1) + Math.max(-delta, 0)) / period;
|
|
47
|
+
out[index] = loss === 0 ? 100 : 100 - 100 / (1 + gain / loss);
|
|
48
|
+
}
|
|
49
|
+
return out;
|
|
50
|
+
};
|
|
51
|
+
const value = rsiOf(bars.map((b) => b.close), period)[i];
|
|
36
52
|
if (value === void 0) return null;
|
|
37
53
|
if (!(value < threshold)) return null;
|
|
38
54
|
return {
|
|
@@ -1,9 +1,4 @@
|
|
|
1
|
-
import { sma } from "@dshtrading/indicators";
|
|
2
1
|
//#region src/screeners/volume-breakout.ts
|
|
3
|
-
/**
|
|
4
|
-
* 放量突破:收盘价创 lookback 日新高,且成交量 ≥ 均量的 volMultiple 倍——
|
|
5
|
-
* 突破有效性的量能确认(无量突破不入选)。量比口径跨市场可比(比值无量纲)。
|
|
6
|
-
*/
|
|
7
2
|
const volumeBreakoutScreener = {
|
|
8
3
|
id: "scr.volume-breakout",
|
|
9
4
|
name: "放量突破",
|
|
@@ -36,10 +31,20 @@ const volumeBreakoutScreener = {
|
|
|
36
31
|
const volMultiple = Math.max(1, params.volMultiple ?? 2);
|
|
37
32
|
const i = bars.length - 1;
|
|
38
33
|
if (i < lookback) return null;
|
|
34
|
+
const smaOf = (values, period) => {
|
|
35
|
+
const out = new Array(values.length).fill(void 0);
|
|
36
|
+
if (!Number.isFinite(period) || period < 1 || values.length < period) return out;
|
|
37
|
+
let sum = 0;
|
|
38
|
+
for (let index = 0; index < values.length; index++) {
|
|
39
|
+
sum += values[index];
|
|
40
|
+
if (index >= period) sum -= values[index - period];
|
|
41
|
+
if (index >= period - 1) out[index] = sum / period;
|
|
42
|
+
}
|
|
43
|
+
return out;
|
|
44
|
+
};
|
|
39
45
|
const window = bars.slice(i - lookback, i);
|
|
40
46
|
const priorHigh = Math.max(...window.map((b) => b.high));
|
|
41
|
-
const
|
|
42
|
-
const avgVolume = sma(volumes, lookback)[i];
|
|
47
|
+
const avgVolume = smaOf(bars.map((b) => b.volume), lookback)[i];
|
|
43
48
|
if (avgVolume === void 0 || avgVolume <= 0) return null;
|
|
44
49
|
const close = bars[i].close;
|
|
45
50
|
const volume = bars[i].volume;
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import "./types.js";
|
|
2
|
+
import { ScreenerValidationResult, StrategyValidationResult } from "./validate.js";
|
|
3
|
+
import { Kline } from "@dshtrading/indicators";
|
|
4
|
+
//#region src/validate-node.d.ts
|
|
5
|
+
/** Node.js 宿主端策略校验器:自动启用 node:vm 超时熔断保护。 */
|
|
6
|
+
declare function validateCustomStrategyNode(raw: unknown): Promise<StrategyValidationResult>;
|
|
7
|
+
/** Node.js 宿主端选股器校验器:vm 熔断 runner 注入。 */
|
|
8
|
+
declare function validateCustomScreenerNode(raw: unknown): Promise<ScreenerValidationResult>;
|
|
9
|
+
//#endregion
|
|
10
|
+
export { validateCustomScreenerNode, validateCustomStrategyNode };
|
package/lib/validate-node.js
CHANGED
|
@@ -1,40 +1,26 @@
|
|
|
1
|
-
import { validateCustomStrategy } from "./validate.js";
|
|
2
|
-
import
|
|
1
|
+
import { validateCustomScreener, validateCustomStrategy } from "./validate.js";
|
|
2
|
+
import { runSourceInVmSandbox } from "@dshtrading/indicators/tool";
|
|
3
3
|
//#region src/validate-node.ts
|
|
4
|
+
const nodeStrategyComputeRunner = (computeSource, bars, params, timeoutMs = 100) => runSourceInVmSandbox(computeSource, bars, params, timeoutMs, "策略试算");
|
|
5
|
+
/** Node.js 宿主端策略校验器:自动启用 node:vm 超时熔断保护。 */
|
|
6
|
+
function validateCustomStrategyNode(raw) {
|
|
7
|
+
return validateCustomStrategy(raw, { runner: nodeStrategyComputeRunner });
|
|
8
|
+
}
|
|
4
9
|
/**
|
|
5
|
-
* Node
|
|
6
|
-
* 的
|
|
10
|
+
* Node 宿主端选股器 evaluate 试算 runner(同一 vm 沙箱形态;返回值形状由
|
|
11
|
+
* validateCustomScreener 的 ScreenerMatch 校验把关,此处不约束类型)。
|
|
12
|
+
* Promise 形态:抛错转拒绝,超时文案经 label 归位为「选股器试算」。
|
|
7
13
|
*/
|
|
8
|
-
const
|
|
9
|
-
const sandbox = {
|
|
10
|
-
bars,
|
|
11
|
-
params,
|
|
12
|
-
result: null,
|
|
13
|
-
Math,
|
|
14
|
-
Array,
|
|
15
|
-
Object,
|
|
16
|
-
Number,
|
|
17
|
-
String,
|
|
18
|
-
Boolean,
|
|
19
|
-
Date
|
|
20
|
-
};
|
|
21
|
-
const trimmed = computeSource.trim();
|
|
22
|
-
let code;
|
|
23
|
-
if (/^(?:\([a-zA-Z0-9_,\s]*\)|[a-zA-Z0-9_]+)\s*=>/.test(trimmed) || /^function\b/.test(trimmed)) code = `"use strict"; const fn = (${trimmed}); result = fn(bars, params);`;
|
|
24
|
-
else code = `"use strict"; const fn = (function(bars, params) { ${trimmed} }); result = fn(bars, params);`;
|
|
25
|
-
const script = new vm.Script(code);
|
|
26
|
-
const context = vm.createContext(sandbox);
|
|
14
|
+
const nodeScreenerEvaluateRunner = (evaluateSource, bars, params, timeoutMs = 100) => {
|
|
27
15
|
try {
|
|
28
|
-
|
|
29
|
-
return sandbox.result;
|
|
16
|
+
return Promise.resolve(runSourceInVmSandbox(evaluateSource, bars, params, timeoutMs, "选股器试算"));
|
|
30
17
|
} catch (error) {
|
|
31
|
-
|
|
32
|
-
throw error;
|
|
18
|
+
return Promise.reject(error);
|
|
33
19
|
}
|
|
34
20
|
};
|
|
35
|
-
/** Node.js
|
|
36
|
-
function
|
|
37
|
-
return
|
|
21
|
+
/** Node.js 宿主端选股器校验器:vm 熔断 runner 注入。 */
|
|
22
|
+
function validateCustomScreenerNode(raw) {
|
|
23
|
+
return validateCustomScreener(raw, { runner: nodeScreenerEvaluateRunner });
|
|
38
24
|
}
|
|
39
25
|
//#endregion
|
|
40
|
-
export { nodeStrategyComputeRunner, validateCustomStrategyNode };
|
|
26
|
+
export { nodeScreenerEvaluateRunner, nodeStrategyComputeRunner, validateCustomScreenerNode, validateCustomStrategyNode };
|
package/lib/validate.d.ts
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import { StrategyDefinition, StrategySignal } from "./types.js";
|
|
2
2
|
import { CustomStrategyRecord } from "./custom.js";
|
|
3
|
+
import { ScreenerDefinition } from "./screeners/types.js";
|
|
4
|
+
import { CustomScreenerRecord } from "./custom-screener.js";
|
|
3
5
|
import { AsyncComputeRunner, Kline } from "@dshtrading/indicators";
|
|
4
6
|
//#region src/validate.d.ts
|
|
5
7
|
type StrategyValidationResult = {
|
|
@@ -10,8 +12,12 @@ type StrategyValidationResult = {
|
|
|
10
12
|
ok: false;
|
|
11
13
|
reason: string;
|
|
12
14
|
};
|
|
13
|
-
/**
|
|
14
|
-
|
|
15
|
+
/**
|
|
16
|
+
* 将策略源码解析为可执行纯函数(浏览器端 new Function;Node 侧走 vm runner 试算)。
|
|
17
|
+
* 编译语义(形态嗅探 + new Function 双形态)的单一来源是 @dshtrading/indicators 的
|
|
18
|
+
* compileComputeSource——此处仅按策略信号类型收窄返回值(2026-09-09 收敛同体副本)。
|
|
19
|
+
*/
|
|
20
|
+
declare const compileStrategySource: (source: string) => (bars: readonly Kline[], params: Readonly<Record<string, number>>) => StrategySignal[];
|
|
15
21
|
/**
|
|
16
22
|
* 信号序列专用校验(引擎语义对齐:i 收盘确认、i+1 开盘成交可复算)。
|
|
17
23
|
* 返回 undefined = 通过;否则返回人话诊断(模型与 UI 直接可读)。
|
|
@@ -24,5 +30,24 @@ declare function validateSignalSequence(signals: readonly unknown[], bars: reado
|
|
|
24
30
|
declare function validateCustomStrategy(raw: unknown, options?: {
|
|
25
31
|
runner?: AsyncComputeRunner;
|
|
26
32
|
}): Promise<StrategyValidationResult>;
|
|
33
|
+
type ScreenerValidationResult = {
|
|
34
|
+
ok: true;
|
|
35
|
+
definition: ScreenerDefinition;
|
|
36
|
+
record: CustomScreenerRecord;
|
|
37
|
+
} | {
|
|
38
|
+
ok: false;
|
|
39
|
+
reason: string;
|
|
40
|
+
};
|
|
41
|
+
/**
|
|
42
|
+
* 自定义选股器校验(异步——试算走可等待 runner,默认浏览器 Worker 超时熔断;
|
|
43
|
+
* Node 宿主侧传 validate-node.ts 的 vm 熔断 runner)。
|
|
44
|
+
*
|
|
45
|
+
* 与策略校验的差异:evaluate 是「单时点截面判断」——无信号序列语义,
|
|
46
|
+
* 返回 null(未命中/数据不足跳过)或 ScreenerMatch(metrics 键 ⊆ columns 声明、
|
|
47
|
+
* 数值有限、reason 非空)。内置选股器 id 可覆盖(不再保留名)。
|
|
48
|
+
*/
|
|
49
|
+
declare function validateCustomScreener(raw: unknown, options?: {
|
|
50
|
+
runner?: AsyncComputeRunner;
|
|
51
|
+
}): Promise<ScreenerValidationResult>;
|
|
27
52
|
//#endregion
|
|
28
|
-
export { StrategyValidationResult, compileStrategySource, validateCustomStrategy, validateSignalSequence };
|
|
53
|
+
export { ScreenerValidationResult, StrategyValidationResult, compileStrategySource, validateCustomScreener, validateCustomStrategy, validateSignalSequence };
|