@dshtrading/strategies 0.1.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/LICENSE +75 -0
- package/lib/api/lib/index.d.ts +461 -0
- package/lib/custom-fs.d.ts +5 -0
- package/lib/custom-fs.js +72 -0
- package/lib/custom.d.ts +28 -0
- package/lib/custom.js +16 -0
- package/lib/engine.d.ts +6 -0
- package/lib/engine.js +165 -0
- package/lib/index.d.ts +14 -0
- package/lib/index.js +12 -0
- package/lib/paradigms/bollinger-reversion.d.ts +5 -0
- package/lib/paradigms/bollinger-reversion.js +77 -0
- package/lib/paradigms/donchian-breakout.d.ts +5 -0
- package/lib/paradigms/donchian-breakout.js +71 -0
- package/lib/paradigms/ema-crossover.d.ts +5 -0
- package/lib/paradigms/ema-crossover.js +83 -0
- package/lib/paradigms/index.d.ts +12 -0
- package/lib/paradigms/index.js +23 -0
- package/lib/paradigms/momentum-12m.d.ts +5 -0
- package/lib/paradigms/momentum-12m.js +73 -0
- package/lib/paradigms/rsi-reversion.d.ts +5 -0
- package/lib/paradigms/rsi-reversion.js +89 -0
- package/lib/paradigms/sma-baseline.d.ts +5 -0
- package/lib/paradigms/sma-baseline.js +70 -0
- package/lib/plugin.d.ts +44 -0
- package/lib/plugin.js +214 -0
- package/lib/screeners/above-ma.d.ts +1 -0
- package/lib/screeners/above-ma.js +66 -0
- package/lib/screeners/index.d.ts +11 -0
- package/lib/screeners/index.js +18 -0
- package/lib/screeners/ma-bull-align.d.ts +1 -0
- package/lib/screeners/ma-bull-align.js +68 -0
- package/lib/screeners/near-high.d.ts +1 -0
- package/lib/screeners/near-high.js +48 -0
- package/lib/screeners/rsi-oversold.d.ts +1 -0
- package/lib/screeners/rsi-oversold.js +50 -0
- package/lib/screeners/types.d.ts +38 -0
- package/lib/screeners/volume-breakout.d.ts +1 -0
- package/lib/screeners/volume-breakout.js +63 -0
- package/lib/types.d.ts +81 -0
- package/lib/validate-node.js +40 -0
- package/lib/validate.d.ts +28 -0
- package/lib/validate.js +264 -0
- package/package.json +40 -0
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { sma } from "@dshtrading/indicators";
|
|
2
|
+
//#region src/paradigms/sma-baseline.ts
|
|
3
|
+
/**
|
|
4
|
+
* 200 日均线择时基线策略(长线,Meb Faber GTAA 经典)。
|
|
5
|
+
*
|
|
6
|
+
* 入场:收盘价站上 SMA200 牛市生命线,全仓持有。
|
|
7
|
+
* 出场:收盘价跌破 SMA200 熊市防线,空仓避险。
|
|
8
|
+
*/
|
|
9
|
+
const smaBaselineStrategy = {
|
|
10
|
+
id: "sma-baseline",
|
|
11
|
+
horizon: "long",
|
|
12
|
+
name: "200 日均线牛熊择时基线",
|
|
13
|
+
summary: "收盘价站上 SMA200 均线做多,跌破均线空仓避险(长线资产配置经典基线)",
|
|
14
|
+
params: [{
|
|
15
|
+
key: "period",
|
|
16
|
+
label: "长期均线周期",
|
|
17
|
+
default: 200,
|
|
18
|
+
min: 50,
|
|
19
|
+
max: 300,
|
|
20
|
+
step: 10
|
|
21
|
+
}],
|
|
22
|
+
compute(bars, params) {
|
|
23
|
+
const period = Math.max(10, Math.round(params.period ?? 200));
|
|
24
|
+
const closes = bars.map((b) => b.close);
|
|
25
|
+
const smaValues = sma(closes, period);
|
|
26
|
+
const signals = [];
|
|
27
|
+
let inPosition = false;
|
|
28
|
+
for (let i = period - 1; i < bars.length; i++) {
|
|
29
|
+
const currentClose = bars[i].close;
|
|
30
|
+
const currentSma = smaValues[i];
|
|
31
|
+
if (currentSma === void 0) continue;
|
|
32
|
+
if (!inPosition && currentClose > currentSma) {
|
|
33
|
+
signals.push({
|
|
34
|
+
index: i,
|
|
35
|
+
time: bars[i].openTime,
|
|
36
|
+
action: "entry",
|
|
37
|
+
direction: "long",
|
|
38
|
+
price: currentClose,
|
|
39
|
+
reason: `收盘价 (${currentClose.toFixed(2)}) 站上长期基线 SMA(${period}) (${currentSma.toFixed(2)}),确立多头趋势`,
|
|
40
|
+
reasonKey: "strat.sma-baseline.reason.entry",
|
|
41
|
+
reasonParams: {
|
|
42
|
+
close: currentClose.toFixed(2),
|
|
43
|
+
period,
|
|
44
|
+
sma: currentSma.toFixed(2)
|
|
45
|
+
}
|
|
46
|
+
});
|
|
47
|
+
inPosition = true;
|
|
48
|
+
} else if (inPosition && currentClose < currentSma) {
|
|
49
|
+
signals.push({
|
|
50
|
+
index: i,
|
|
51
|
+
time: bars[i].openTime,
|
|
52
|
+
action: "exit",
|
|
53
|
+
direction: "flat",
|
|
54
|
+
price: currentClose,
|
|
55
|
+
reason: `收盘价 (${currentClose.toFixed(2)}) 跌破长期基线 SMA(${period}) (${currentSma.toFixed(2)}),转入防御避险`,
|
|
56
|
+
reasonKey: "strat.sma-baseline.reason.exit",
|
|
57
|
+
reasonParams: {
|
|
58
|
+
close: currentClose.toFixed(2),
|
|
59
|
+
period,
|
|
60
|
+
sma: currentSma.toFixed(2)
|
|
61
|
+
}
|
|
62
|
+
});
|
|
63
|
+
inPosition = false;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
return signals;
|
|
67
|
+
}
|
|
68
|
+
};
|
|
69
|
+
//#endregion
|
|
70
|
+
export { smaBaselineStrategy };
|
package/lib/plugin.d.ts
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { StrategyDefinition } from "./types.js";
|
|
2
|
+
import { CustomStrategyRecord, CustomStrategyStore } from "./custom.js";
|
|
3
|
+
import { MarketDataService } from "./api/lib/index.js";
|
|
4
|
+
import { createFileCustomStrategyStore } from "./custom-fs.js";
|
|
5
|
+
import { Context } from "@deepseek-ai/cordis";
|
|
6
|
+
//#region src/plugin.d.ts
|
|
7
|
+
/** Cordis 插件名 = patch 行 id(TEMPLATES §8),市场无关共享行命名空间。 */
|
|
8
|
+
declare const name = "dsh-trading-strategies";
|
|
9
|
+
/** 本插件不硬依赖任何服务(headless 宿主零要求);tools/注册表经 ctx.inject 声明。 */
|
|
10
|
+
declare const inject: string[];
|
|
11
|
+
/** 默认存储路径:~/.dsh/strategies/custom.json。 */
|
|
12
|
+
declare function defaultStorePath(): string;
|
|
13
|
+
/** tradingEvents 的最小发布面(鸭式,不定死接口;总线缺席时静默降级)。 */
|
|
14
|
+
interface TradingEventsPublisher {
|
|
15
|
+
emit(store: 'strategies'): void;
|
|
16
|
+
}
|
|
17
|
+
/** 注册表 + 老部署回退的行情解析面(与桥同款 registry-first 语义)。 */
|
|
18
|
+
interface StrategyMarketDataResolver {
|
|
19
|
+
(market: string): MarketDataService | undefined;
|
|
20
|
+
}
|
|
21
|
+
declare function createMarketDataResolver(ctx: Context): StrategyMarketDataResolver;
|
|
22
|
+
interface StrategyAuthorToolOptions {
|
|
23
|
+
store: CustomStrategyStore;
|
|
24
|
+
/** 可选:策略成功落盘后的回调(issue #30:事件总线 emit('strategies') 接线点)。 */
|
|
25
|
+
onWritten?: (record: CustomStrategyRecord) => void;
|
|
26
|
+
}
|
|
27
|
+
/** strategy_author 工厂(独立导出便于单测)。 */
|
|
28
|
+
declare function createStrategyAuthorTool(options: StrategyAuthorToolOptions): import("@deepseek-ai/dsh-tools").ToolDefinition;
|
|
29
|
+
interface StrategyBacktestToolOptions {
|
|
30
|
+
store: CustomStrategyStore;
|
|
31
|
+
marketData: StrategyMarketDataResolver;
|
|
32
|
+
}
|
|
33
|
+
/** 自定义或范式策略 → 回测用 StrategyDefinition(自定义 compute 经编译落定)。 */
|
|
34
|
+
declare function resolveStrategyDefinition(store: CustomStrategyStore, strategyId: string): Promise<StrategyDefinition | undefined>;
|
|
35
|
+
interface StrategyBacktestToolDeps {
|
|
36
|
+
store: CustomStrategyStore;
|
|
37
|
+
marketData: StrategyMarketDataResolver;
|
|
38
|
+
}
|
|
39
|
+
/** strategy_backtest 工厂(独立导出便于单测)。 */
|
|
40
|
+
declare function createStrategyBacktestTool(deps: StrategyBacktestToolDeps): import("@deepseek-ai/dsh-tools").ToolDefinition;
|
|
41
|
+
/** Host plugin body:注册 strategy_author / strategy_backtest(host 平面,全会话可见)。 */
|
|
42
|
+
declare function apply(ctx: Context): void;
|
|
43
|
+
//#endregion
|
|
44
|
+
export { StrategyAuthorToolOptions, StrategyBacktestToolDeps, StrategyBacktestToolOptions, StrategyMarketDataResolver, TradingEventsPublisher, apply, createFileCustomStrategyStore, createMarketDataResolver, createStrategyAuthorTool, createStrategyBacktestTool, defaultStorePath, inject, name, resolveStrategyDefinition };
|
package/lib/plugin.js
ADDED
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
import { run } from "./engine.js";
|
|
2
|
+
import { compileStrategySource } from "./validate.js";
|
|
3
|
+
import { getStrategyById } from "./paradigms/index.js";
|
|
4
|
+
import "./index.js";
|
|
5
|
+
import { createFileCustomStrategyStore } from "./custom-fs.js";
|
|
6
|
+
import { validateCustomStrategyNode } from "./validate-node.js";
|
|
7
|
+
import { defineTool } from "@deepseek-ai/dsh-tools";
|
|
8
|
+
import os from "node:os";
|
|
9
|
+
import path from "node:path";
|
|
10
|
+
//#region src/plugin.ts
|
|
11
|
+
/** Cordis 插件名 = patch 行 id(TEMPLATES §8),市场无关共享行命名空间。 */
|
|
12
|
+
const name = "dsh-trading-strategies";
|
|
13
|
+
/** 本插件不硬依赖任何服务(headless 宿主零要求);tools/注册表经 ctx.inject 声明。 */
|
|
14
|
+
const inject = [];
|
|
15
|
+
/** 行情服务键映射(与 client-ui-trading/bridge 的 MARKET_SERVICE_KEYS 同词汇;本地副本避免跨包依赖)。 */
|
|
16
|
+
const MARKET_SERVICE_KEYS = {
|
|
17
|
+
crypto: "tradingCryptoMarketData",
|
|
18
|
+
us: "tradingUsMarketData",
|
|
19
|
+
cn: "tradingCnMarketData",
|
|
20
|
+
hk: "tradingHkMarketData"
|
|
21
|
+
};
|
|
22
|
+
/** 默认存储路径:~/.dsh/strategies/custom.json。 */
|
|
23
|
+
function defaultStorePath() {
|
|
24
|
+
return path.join(os.homedir(), ".dsh", "strategies", "custom.json");
|
|
25
|
+
}
|
|
26
|
+
function eventsOf(ctx) {
|
|
27
|
+
return ctx.get?.("tradingEvents", false);
|
|
28
|
+
}
|
|
29
|
+
function createMarketDataResolver(ctx) {
|
|
30
|
+
return (market) => {
|
|
31
|
+
const registry = ctx.get?.("tradingMarketDataRegistry", false);
|
|
32
|
+
if (registry !== void 0) return registry.active(market)?.service;
|
|
33
|
+
const key = MARKET_SERVICE_KEYS[market];
|
|
34
|
+
return key === void 0 ? void 0 : ctx.get?.(key);
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
/** strategy_author 工厂(独立导出便于单测)。 */
|
|
38
|
+
function createStrategyAuthorTool(options) {
|
|
39
|
+
const { store, onWritten } = options;
|
|
40
|
+
return defineTool({
|
|
41
|
+
name: "strategy_author",
|
|
42
|
+
description: "Author, validate, and persist a custom trading strategy from JavaScript compute source. compute(bars, params) must return StrategySignal[] (entry/exit at bar close, filled at next bar open by the backtest engine). The validator runs sandbox trial calculations across multiple kline scenarios and replays the signal sequence for engine-replayability. If valid, the strategy is persisted and immediately available for backtesting and the strategy roster.",
|
|
43
|
+
parameters: {
|
|
44
|
+
id: {
|
|
45
|
+
type: "string",
|
|
46
|
+
required: true,
|
|
47
|
+
description: "Unique strategy id (2-32 chars: lowercase letters/digits/underscore/hyphen, e.g. \"ema-stop-takeprofit\"); the 6 built-in paradigm ids are reserved"
|
|
48
|
+
},
|
|
49
|
+
title: {
|
|
50
|
+
type: "string",
|
|
51
|
+
required: true,
|
|
52
|
+
description: "Display name (1-32 chars), e.g. \"双均线止损止盈\""
|
|
53
|
+
},
|
|
54
|
+
horizon: {
|
|
55
|
+
type: "string",
|
|
56
|
+
required: true,
|
|
57
|
+
description: "Strategy horizon: \"short\" (短线), \"swing\" (波段), or \"long\" (长线)"
|
|
58
|
+
},
|
|
59
|
+
summary: {
|
|
60
|
+
type: "string",
|
|
61
|
+
required: true,
|
|
62
|
+
description: "One-sentence idea summary (≤120 chars), shown in the roster and chat card"
|
|
63
|
+
},
|
|
64
|
+
paramsJson: {
|
|
65
|
+
type: "string",
|
|
66
|
+
description: "JSON string of StrategyParamSpec[] (optional, default []). Each spec: { key, label, default, min, max } with numeric default/min/max and min < max. JSON example: [{\"key\":\"fast\",\"label\":\"fast EMA\",\"default\":20,\"min\":2,\"max\":120}]"
|
|
67
|
+
},
|
|
68
|
+
computeSource: {
|
|
69
|
+
type: "string",
|
|
70
|
+
required: true,
|
|
71
|
+
description: "JavaScript pure function source, signature (bars, params) => StrategySignal[]. bars has { openTime, open, high, low, close, volume }. Each signal: { index, time, action: \"entry\"|\"exit\", direction: \"long\"|\"flat\", price: bars[index].close, reason }. Signals must be strictly index-increasing, start with entry, and alternate entry/exit."
|
|
72
|
+
}
|
|
73
|
+
},
|
|
74
|
+
output: {
|
|
75
|
+
schema: { type: "string" },
|
|
76
|
+
render: (_args, value) => [{
|
|
77
|
+
type: "text",
|
|
78
|
+
text: value
|
|
79
|
+
}]
|
|
80
|
+
},
|
|
81
|
+
async execute(raw) {
|
|
82
|
+
const args = raw ?? {};
|
|
83
|
+
const candidate = {
|
|
84
|
+
id: typeof args.id === "string" ? args.id : "",
|
|
85
|
+
title: typeof args.title === "string" ? args.title : "",
|
|
86
|
+
horizon: typeof args.horizon === "string" ? args.horizon : "",
|
|
87
|
+
summary: typeof args.summary === "string" ? args.summary : "",
|
|
88
|
+
paramsJson: typeof args.paramsJson === "string" && args.paramsJson.trim() ? args.paramsJson.trim() : "[]",
|
|
89
|
+
computeSource: typeof args.computeSource === "string" ? args.computeSource : "",
|
|
90
|
+
createdAt: Date.now()
|
|
91
|
+
};
|
|
92
|
+
const result = await validateCustomStrategyNode(candidate);
|
|
93
|
+
if (!result.ok) return `[strategy_author] Validation failed: ${result.reason}\nReview the requirements: compute(bars, params) returns StrategySignal[]; each signal confirms at bar close (price === bars[index].close, time === bars[index].openTime), indices strictly increase, the sequence starts with entry and strictly alternates entry/exit (the engine fills at the next bar's open).`;
|
|
94
|
+
await store.save(result.record);
|
|
95
|
+
onWritten?.(result.record);
|
|
96
|
+
const specSummary = result.definition.params.map((p) => `${p.key}=${p.default}`).join(", ");
|
|
97
|
+
return `[strategy_author] Successfully authored strategy "${result.record.title}" (id: ${result.record.id}, horizon: ${result.record.horizon}${specSummary ? `, params: ${specSummary}` : ""}). The strategy passed sandbox trials across 5 kline scenarios with engine-replayable signal sequences and is now persisted — call strategy_backtest with this id to backtest it.`;
|
|
98
|
+
}
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
/** 自定义或范式策略 → 回测用 StrategyDefinition(自定义 compute 经编译落定)。 */
|
|
102
|
+
async function resolveStrategyDefinition(store, strategyId) {
|
|
103
|
+
const record = await store.get(strategyId);
|
|
104
|
+
if (record !== void 0) {
|
|
105
|
+
let params = [];
|
|
106
|
+
try {
|
|
107
|
+
const parsed = JSON.parse(record.paramsJson);
|
|
108
|
+
if (Array.isArray(parsed)) params = parsed;
|
|
109
|
+
} catch {
|
|
110
|
+
params = [];
|
|
111
|
+
}
|
|
112
|
+
return {
|
|
113
|
+
id: record.id,
|
|
114
|
+
horizon: record.horizon,
|
|
115
|
+
name: record.title,
|
|
116
|
+
summary: record.summary,
|
|
117
|
+
params,
|
|
118
|
+
compute: compileStrategySource(record.computeSource)
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
return getStrategyById(strategyId);
|
|
122
|
+
}
|
|
123
|
+
/** strategy_backtest 工厂(独立导出便于单测)。 */
|
|
124
|
+
function createStrategyBacktestTool(deps) {
|
|
125
|
+
return defineTool({
|
|
126
|
+
name: "strategy_backtest",
|
|
127
|
+
description: "Backtest a strategy (custom authored via strategy_author, or a built-in paradigm like ema-crossover / donchian-breakout / rsi-reversion / bollinger-reversion / sma-baseline / momentum-12m) on a symbol and interval using the pure-function engine. Returns 8 metrics (totalReturn, cagr, maxDrawdown, sharpe, winRate, profitFactor, tradeCount, exposure), the trade list, and the equity curve. Signals confirm at bar close and fill at the next bar open with fee/slippage modeling; this is simulation only — it never places orders.",
|
|
128
|
+
parameters: {
|
|
129
|
+
strategyId: {
|
|
130
|
+
type: "string",
|
|
131
|
+
required: true,
|
|
132
|
+
description: "Strategy id — a custom id from strategy_author or a built-in paradigm id"
|
|
133
|
+
},
|
|
134
|
+
market: {
|
|
135
|
+
type: "string",
|
|
136
|
+
required: true,
|
|
137
|
+
description: "Market vocabulary: crypto | us | cn | hk"
|
|
138
|
+
},
|
|
139
|
+
symbol: {
|
|
140
|
+
type: "string",
|
|
141
|
+
required: true,
|
|
142
|
+
description: "Market-canonical symbol, e.g. BTCUSDT (crypto), AAPL (us), 600519.SH (cn), 00700.HK (hk)"
|
|
143
|
+
},
|
|
144
|
+
interval: {
|
|
145
|
+
type: "string",
|
|
146
|
+
description: "Kline interval (default \"1d\"), e.g. 1m/5m/15m/1h/4h/1d/1w/1M — subject to the market data provider vocabulary"
|
|
147
|
+
},
|
|
148
|
+
limit: {
|
|
149
|
+
type: "number",
|
|
150
|
+
description: "Kline count to backtest (default 200, capped by the provider)"
|
|
151
|
+
}
|
|
152
|
+
},
|
|
153
|
+
output: {
|
|
154
|
+
schema: { type: "string" },
|
|
155
|
+
render: (_args, value) => [{
|
|
156
|
+
type: "text",
|
|
157
|
+
text: value
|
|
158
|
+
}]
|
|
159
|
+
},
|
|
160
|
+
async execute(raw) {
|
|
161
|
+
const args = raw ?? {};
|
|
162
|
+
const strategyId = typeof args.strategyId === "string" ? args.strategyId.trim() : "";
|
|
163
|
+
const market = typeof args.market === "string" ? args.market.trim() : "";
|
|
164
|
+
const symbol = typeof args.symbol === "string" ? args.symbol.trim() : "";
|
|
165
|
+
const interval = typeof args.interval === "string" && args.interval.trim() ? args.interval.trim() : "1d";
|
|
166
|
+
const limit = typeof args.limit === "number" && Number.isFinite(args.limit) && args.limit > 0 ? Math.min(Math.floor(args.limit), 1e3) : 200;
|
|
167
|
+
if (!strategyId || !market || !symbol) throw new Error("strategy_backtest: strategyId, market and symbol are required");
|
|
168
|
+
const definition = await resolveStrategyDefinition(deps.store, strategyId);
|
|
169
|
+
if (definition === void 0) throw new Error(`strategy_backtest: unknown strategyId "${strategyId}" — author one with strategy_author first, or use a built-in paradigm id (donchian-breakout, rsi-reversion, ema-crossover, bollinger-reversion, sma-baseline, momentum-12m)`);
|
|
170
|
+
const service = deps.marketData(market);
|
|
171
|
+
if (service === void 0) throw new Error(`strategy_backtest: no market data service for market "${market}" — install/activate a market connector first`);
|
|
172
|
+
const bars = await service.getKlines(symbol, interval, limit);
|
|
173
|
+
if (!Array.isArray(bars) || bars.length === 0) throw new Error(`strategy_backtest: no klines returned for ${symbol} (${market}, ${interval}) — check the symbol/interval vocabulary`);
|
|
174
|
+
const result = run(bars, definition);
|
|
175
|
+
return JSON.stringify({
|
|
176
|
+
ok: true,
|
|
177
|
+
strategy: {
|
|
178
|
+
id: definition.id,
|
|
179
|
+
name: definition.name,
|
|
180
|
+
horizon: definition.horizon
|
|
181
|
+
},
|
|
182
|
+
market,
|
|
183
|
+
symbol,
|
|
184
|
+
interval,
|
|
185
|
+
barsTested: bars.length,
|
|
186
|
+
metrics: result.metrics,
|
|
187
|
+
trades: result.trades,
|
|
188
|
+
equity: result.equity,
|
|
189
|
+
initialCapital: result.initialCapital,
|
|
190
|
+
finalCapital: result.finalCapital
|
|
191
|
+
});
|
|
192
|
+
}
|
|
193
|
+
});
|
|
194
|
+
}
|
|
195
|
+
/** Host plugin body:注册 strategy_author / strategy_backtest(host 平面,全会话可见)。 */
|
|
196
|
+
function apply(ctx) {
|
|
197
|
+
const store = createFileCustomStrategyStore(defaultStorePath());
|
|
198
|
+
ctx.inject(["tools"], (toolCtx) => {
|
|
199
|
+
const tools = toolCtx.tools;
|
|
200
|
+
if (!tools || typeof tools.register !== "function") return;
|
|
201
|
+
const authorTool = createStrategyAuthorTool({
|
|
202
|
+
store,
|
|
203
|
+
onWritten: () => eventsOf(ctx)?.emit("strategies")
|
|
204
|
+
});
|
|
205
|
+
if (tools.get(authorTool.name) === void 0) tools.register(authorTool);
|
|
206
|
+
const backtestTool = createStrategyBacktestTool({
|
|
207
|
+
store,
|
|
208
|
+
marketData: createMarketDataResolver(ctx)
|
|
209
|
+
});
|
|
210
|
+
if (tools.get(backtestTool.name) === void 0) tools.register(backtestTool);
|
|
211
|
+
});
|
|
212
|
+
}
|
|
213
|
+
//#endregion
|
|
214
|
+
export { apply, createFileCustomStrategyStore, createMarketDataResolver, createStrategyAuthorTool, createStrategyBacktestTool, defaultStorePath, inject, name, resolveStrategyDefinition };
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import "./types.js";
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { sma } from "@dshtrading/indicators";
|
|
2
|
+
//#region src/screeners/above-ma.ts
|
|
3
|
+
/**
|
|
4
|
+
* 牛熊线之上:现价站上长期均线且均线自身斜率向上——与量化「200 日均线
|
|
5
|
+
* 牛熊择时基线」同源的长线标的筛选(Faber GTAA 的截面版)。
|
|
6
|
+
*/
|
|
7
|
+
const aboveMaScreener = {
|
|
8
|
+
id: "scr.above-ma",
|
|
9
|
+
name: "站上牛熊线",
|
|
10
|
+
summary: "现价站上长期均线且均线斜率向上,筛选长线多头环境的标的",
|
|
11
|
+
params: [{
|
|
12
|
+
key: "period",
|
|
13
|
+
label: "牛熊线周期",
|
|
14
|
+
default: 200,
|
|
15
|
+
min: 50,
|
|
16
|
+
max: 300,
|
|
17
|
+
step: 10
|
|
18
|
+
}, {
|
|
19
|
+
key: "slopeBars",
|
|
20
|
+
label: "斜率窗口(日)",
|
|
21
|
+
default: 20,
|
|
22
|
+
min: 5,
|
|
23
|
+
max: 60,
|
|
24
|
+
step: 5
|
|
25
|
+
}],
|
|
26
|
+
columns: [{
|
|
27
|
+
key: "aboveMaPct",
|
|
28
|
+
label: "高于均线",
|
|
29
|
+
format: "percent"
|
|
30
|
+
}, {
|
|
31
|
+
key: "maSlopePct",
|
|
32
|
+
label: "均线斜率",
|
|
33
|
+
format: "percent"
|
|
34
|
+
}],
|
|
35
|
+
evaluate(bars, params) {
|
|
36
|
+
const period = Math.max(50, Math.round(params.period ?? 200));
|
|
37
|
+
const slopeBars = Math.max(5, Math.round(params.slopeBars ?? 20));
|
|
38
|
+
const i = bars.length - 1;
|
|
39
|
+
if (i < period + slopeBars - 1) return null;
|
|
40
|
+
const closes = bars.map((b) => b.close);
|
|
41
|
+
const maSeries = sma(closes, period);
|
|
42
|
+
const ma = maSeries[i];
|
|
43
|
+
const maPrev = maSeries[i - slopeBars];
|
|
44
|
+
if (ma === void 0 || maPrev === void 0 || !(maPrev > 0)) return null;
|
|
45
|
+
const close = bars[i].close;
|
|
46
|
+
if (!(close > ma) || !(ma > maPrev)) return null;
|
|
47
|
+
const aboveMaPct = (close - ma) / ma * 100;
|
|
48
|
+
const maSlopePct = (ma - maPrev) / maPrev * 100;
|
|
49
|
+
return {
|
|
50
|
+
metrics: {
|
|
51
|
+
aboveMaPct,
|
|
52
|
+
maSlopePct
|
|
53
|
+
},
|
|
54
|
+
reason: `现价高于 SMA(${period}) ${aboveMaPct.toFixed(2)}%,均线 ${slopeBars} 日斜率 +${maSlopePct.toFixed(2)}%`,
|
|
55
|
+
reasonKey: "scr.above-ma.reason",
|
|
56
|
+
reasonParams: {
|
|
57
|
+
period,
|
|
58
|
+
above: aboveMaPct.toFixed(2),
|
|
59
|
+
slope: slopeBars,
|
|
60
|
+
slopePct: maSlopePct.toFixed(2)
|
|
61
|
+
}
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
};
|
|
65
|
+
//#endregion
|
|
66
|
+
export { aboveMaScreener };
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { ScreenerColumnSpec, ScreenerDefinition, ScreenerMatch } from "./types.js";
|
|
2
|
+
import "./ma-bull-align.js";
|
|
3
|
+
import "./volume-breakout.js";
|
|
4
|
+
import "./rsi-oversold.js";
|
|
5
|
+
import "./near-high.js";
|
|
6
|
+
import "./above-ma.js";
|
|
7
|
+
//#region src/screeners/index.d.ts
|
|
8
|
+
declare const screenerParadigms: readonly ScreenerDefinition[];
|
|
9
|
+
declare function getScreenerById(id: string): ScreenerDefinition | undefined;
|
|
10
|
+
//#endregion
|
|
11
|
+
export { type ScreenerColumnSpec, type ScreenerDefinition, type ScreenerMatch, getScreenerById, screenerParadigms };
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { maBullAlignScreener } from "./ma-bull-align.js";
|
|
2
|
+
import { volumeBreakoutScreener } from "./volume-breakout.js";
|
|
3
|
+
import { rsiOversoldScreener } from "./rsi-oversold.js";
|
|
4
|
+
import { nearHighScreener } from "./near-high.js";
|
|
5
|
+
import { aboveMaScreener } from "./above-ma.js";
|
|
6
|
+
//#region src/screeners/index.ts
|
|
7
|
+
const screenerParadigms = [
|
|
8
|
+
maBullAlignScreener,
|
|
9
|
+
volumeBreakoutScreener,
|
|
10
|
+
rsiOversoldScreener,
|
|
11
|
+
nearHighScreener,
|
|
12
|
+
aboveMaScreener
|
|
13
|
+
];
|
|
14
|
+
function getScreenerById(id) {
|
|
15
|
+
return screenerParadigms.find((s) => s.id === id);
|
|
16
|
+
}
|
|
17
|
+
//#endregion
|
|
18
|
+
export { getScreenerById, screenerParadigms };
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import "./types.js";
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import { sma } from "@dshtrading/indicators";
|
|
2
|
+
//#region src/screeners/ma-bull-align.ts
|
|
3
|
+
/**
|
|
4
|
+
* 均线多头排列:现价站上三线,且短中期均线自上而下依次压制
|
|
5
|
+
* (SMA(短) > SMA(中) > SMA(长)),趋势结构完整的顺势筛选。
|
|
6
|
+
*/
|
|
7
|
+
const maBullAlignScreener = {
|
|
8
|
+
id: "scr.ma-bull-align",
|
|
9
|
+
name: "均线多头排列",
|
|
10
|
+
summary: "现价站上三线且 SMA(短) > SMA(中) > SMA(长),筛选趋势结构完整的标的",
|
|
11
|
+
params: [
|
|
12
|
+
{
|
|
13
|
+
key: "n1",
|
|
14
|
+
label: "短期均线",
|
|
15
|
+
default: 20,
|
|
16
|
+
min: 5,
|
|
17
|
+
max: 120,
|
|
18
|
+
step: 5
|
|
19
|
+
},
|
|
20
|
+
{
|
|
21
|
+
key: "n2",
|
|
22
|
+
label: "中期均线",
|
|
23
|
+
default: 60,
|
|
24
|
+
min: 20,
|
|
25
|
+
max: 250,
|
|
26
|
+
step: 10
|
|
27
|
+
},
|
|
28
|
+
{
|
|
29
|
+
key: "n3",
|
|
30
|
+
label: "长期均线",
|
|
31
|
+
default: 120,
|
|
32
|
+
min: 50,
|
|
33
|
+
max: 300,
|
|
34
|
+
step: 10
|
|
35
|
+
}
|
|
36
|
+
],
|
|
37
|
+
columns: [{
|
|
38
|
+
key: "distLongPct",
|
|
39
|
+
label: "距长期均线",
|
|
40
|
+
format: "percent"
|
|
41
|
+
}],
|
|
42
|
+
evaluate(bars, params) {
|
|
43
|
+
const n1 = Math.max(5, Math.round(params.n1 ?? 20));
|
|
44
|
+
const n2 = Math.max(n1, Math.round(params.n2 ?? 60));
|
|
45
|
+
const n3 = Math.max(n2, Math.round(params.n3 ?? 120));
|
|
46
|
+
const i = bars.length - 1;
|
|
47
|
+
if (i + 1 < n3) return null;
|
|
48
|
+
const closes = bars.map((b) => b.close);
|
|
49
|
+
const s1 = sma(closes, n1)[i];
|
|
50
|
+
const s2 = sma(closes, n2)[i];
|
|
51
|
+
const s3 = sma(closes, n3)[i];
|
|
52
|
+
if (s1 === void 0 || s2 === void 0 || s3 === void 0) return null;
|
|
53
|
+
const close = bars[i].close;
|
|
54
|
+
if (!(close > s1 && s1 > s2 && s2 > s3)) return null;
|
|
55
|
+
return {
|
|
56
|
+
metrics: { distLongPct: (close - s3) / s3 * 100 },
|
|
57
|
+
reason: `现价站上三线且 SMA(${n1}) > SMA(${n2}) > SMA(${n3}),多头排列`,
|
|
58
|
+
reasonKey: "scr.ma-bull-align.reason",
|
|
59
|
+
reasonParams: {
|
|
60
|
+
n1,
|
|
61
|
+
n2,
|
|
62
|
+
n3
|
|
63
|
+
}
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
};
|
|
67
|
+
//#endregion
|
|
68
|
+
export { maBullAlignScreener };
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import "./types.js";
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
//#region src/screeners/near-high.ts
|
|
2
|
+
const nearHighScreener = {
|
|
3
|
+
id: "scr.near-high",
|
|
4
|
+
name: "接近一年新高",
|
|
5
|
+
summary: "现价距 N 日最高价不超过 X%,强势整理/突破前夜的动量筛选",
|
|
6
|
+
params: [{
|
|
7
|
+
key: "window",
|
|
8
|
+
label: "窗口(日)",
|
|
9
|
+
default: 250,
|
|
10
|
+
min: 60,
|
|
11
|
+
max: 500,
|
|
12
|
+
step: 10
|
|
13
|
+
}, {
|
|
14
|
+
key: "withinPct",
|
|
15
|
+
label: "接近阈值(%)",
|
|
16
|
+
default: 5,
|
|
17
|
+
min: 1,
|
|
18
|
+
max: 30,
|
|
19
|
+
step: 1
|
|
20
|
+
}],
|
|
21
|
+
columns: [{
|
|
22
|
+
key: "offHighPct",
|
|
23
|
+
label: "距高点",
|
|
24
|
+
format: "percent"
|
|
25
|
+
}],
|
|
26
|
+
evaluate(bars, params) {
|
|
27
|
+
const window = Math.max(60, Math.round(params.window ?? 250));
|
|
28
|
+
const withinPct = Math.max(1, params.withinPct ?? 5);
|
|
29
|
+
const i = bars.length - 1;
|
|
30
|
+
if (i + 1 < window) return null;
|
|
31
|
+
const windowBars = bars.slice(i + 1 - window);
|
|
32
|
+
const highMax = Math.max(...windowBars.map((b) => b.high));
|
|
33
|
+
if (!(highMax > 0)) return null;
|
|
34
|
+
const offHighPct = (highMax - bars[i].close) / highMax * 100;
|
|
35
|
+
if (!(offHighPct >= 0 && offHighPct <= withinPct)) return null;
|
|
36
|
+
return {
|
|
37
|
+
metrics: { offHighPct },
|
|
38
|
+
reason: `距 ${window} 日高点仅 ${offHighPct.toFixed(2)}%,处于突破前夜`,
|
|
39
|
+
reasonKey: "scr.near-high.reason",
|
|
40
|
+
reasonParams: {
|
|
41
|
+
n: window,
|
|
42
|
+
pct: offHighPct.toFixed(2)
|
|
43
|
+
}
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
};
|
|
47
|
+
//#endregion
|
|
48
|
+
export { nearHighScreener };
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import "./types.js";
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { rsi } from "@dshtrading/indicators";
|
|
2
|
+
//#region src/screeners/rsi-oversold.ts
|
|
3
|
+
/**
|
|
4
|
+
* RSI 超卖:RSI(period) 跌入阈值以下的逆势关注筛选——超卖≠见底,
|
|
5
|
+
* 定位是「值得盯的反转候选池」,命中理由里明示是逆势信号。
|
|
6
|
+
*/
|
|
7
|
+
const rsiOversoldScreener = {
|
|
8
|
+
id: "scr.rsi-oversold",
|
|
9
|
+
name: "RSI 超卖",
|
|
10
|
+
summary: "RSI 跌入超卖区(默认 <30),筛选值得盯的反转候选(逆势信号)",
|
|
11
|
+
params: [{
|
|
12
|
+
key: "period",
|
|
13
|
+
label: "RSI 周期",
|
|
14
|
+
default: 14,
|
|
15
|
+
min: 5,
|
|
16
|
+
max: 30,
|
|
17
|
+
step: 1
|
|
18
|
+
}, {
|
|
19
|
+
key: "threshold",
|
|
20
|
+
label: "超卖阈值",
|
|
21
|
+
default: 30,
|
|
22
|
+
min: 10,
|
|
23
|
+
max: 50,
|
|
24
|
+
step: 5
|
|
25
|
+
}],
|
|
26
|
+
columns: [{
|
|
27
|
+
key: "rsi",
|
|
28
|
+
label: "RSI"
|
|
29
|
+
}],
|
|
30
|
+
evaluate(bars, params) {
|
|
31
|
+
const period = Math.max(5, Math.round(params.period ?? 14));
|
|
32
|
+
const threshold = Math.min(50, Math.max(10, params.threshold ?? 30));
|
|
33
|
+
const i = bars.length - 1;
|
|
34
|
+
if (i < period) return null;
|
|
35
|
+
const value = rsi(bars.map((b) => b.close), period)[i];
|
|
36
|
+
if (value === void 0) return null;
|
|
37
|
+
if (!(value < threshold)) return null;
|
|
38
|
+
return {
|
|
39
|
+
metrics: { rsi: value },
|
|
40
|
+
reason: `RSI(${period}) = ${value.toFixed(1)},进入超卖区(逆势信号)`,
|
|
41
|
+
reasonKey: "scr.rsi-oversold.reason",
|
|
42
|
+
reasonParams: {
|
|
43
|
+
period,
|
|
44
|
+
val: value.toFixed(1)
|
|
45
|
+
}
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
};
|
|
49
|
+
//#endregion
|
|
50
|
+
export { rsiOversoldScreener };
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { StrategyParamSpec } from "../types.js";
|
|
2
|
+
import { Kline as Kline$1 } from "@dshtrading/indicators";
|
|
3
|
+
//#region src/screeners/types.d.ts
|
|
4
|
+
/** 结果表动态列(每只标的命中后展示的指标值列)。 */
|
|
5
|
+
interface ScreenerColumnSpec {
|
|
6
|
+
readonly key: string;
|
|
7
|
+
readonly label: string;
|
|
8
|
+
/** percent = 按 % 渲染;缺省按普通数字渲染 */
|
|
9
|
+
readonly format?: 'percent' | 'number';
|
|
10
|
+
}
|
|
11
|
+
/** 单标的命中结果(未命中由 evaluate 返回 null 表达,不进结果集)。 */
|
|
12
|
+
interface ScreenerMatch {
|
|
13
|
+
/** 动态指标值(key 对应 columns 声明) */
|
|
14
|
+
readonly metrics: Readonly<Record<string, number>>;
|
|
15
|
+
/** 人话解释,UI 直接展示(zh 单语原文,回退用) */
|
|
16
|
+
readonly reason: string;
|
|
17
|
+
/** reason 的词典键(client-ui-strategies 词典约定 scr.<id>.reason),
|
|
18
|
+
* 视图按当前语言渲染 t(reasonKey, reasonParams);缺省回退 reason 原文。 */
|
|
19
|
+
readonly reasonKey?: string;
|
|
20
|
+
/** reasonKey 的 {placeholder} 插值参数。 */
|
|
21
|
+
readonly reasonParams?: Readonly<Record<string, string | number>>;
|
|
22
|
+
}
|
|
23
|
+
interface ScreenerDefinition {
|
|
24
|
+
/** 稳定词汇,加 'scr.' 前缀与范式策略 id 空间隔离(如 'scr.ma-bull-align') */
|
|
25
|
+
readonly id: string;
|
|
26
|
+
readonly name: string;
|
|
27
|
+
readonly summary: string;
|
|
28
|
+
readonly params: readonly StrategyParamSpec[];
|
|
29
|
+
readonly columns: readonly ScreenerColumnSpec[];
|
|
30
|
+
/**
|
|
31
|
+
* 纯函数:无 IO/随机/全局态;同一输入必须同一输出。
|
|
32
|
+
* 数据不足(无法计算所需指标)返回 null——既不算命中也不算错误,
|
|
33
|
+
* 新上市标的窗口不够时静默跳过。
|
|
34
|
+
*/
|
|
35
|
+
evaluate(bars: readonly Kline$1[], params: Readonly<Record<string, number>>): ScreenerMatch | null;
|
|
36
|
+
}
|
|
37
|
+
//#endregion
|
|
38
|
+
export { type Kline$1 as Kline, ScreenerColumnSpec, ScreenerDefinition, ScreenerMatch };
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import "./types.js";
|