@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.
Files changed (44) hide show
  1. package/LICENSE +75 -0
  2. package/lib/api/lib/index.d.ts +461 -0
  3. package/lib/custom-fs.d.ts +5 -0
  4. package/lib/custom-fs.js +72 -0
  5. package/lib/custom.d.ts +28 -0
  6. package/lib/custom.js +16 -0
  7. package/lib/engine.d.ts +6 -0
  8. package/lib/engine.js +165 -0
  9. package/lib/index.d.ts +14 -0
  10. package/lib/index.js +12 -0
  11. package/lib/paradigms/bollinger-reversion.d.ts +5 -0
  12. package/lib/paradigms/bollinger-reversion.js +77 -0
  13. package/lib/paradigms/donchian-breakout.d.ts +5 -0
  14. package/lib/paradigms/donchian-breakout.js +71 -0
  15. package/lib/paradigms/ema-crossover.d.ts +5 -0
  16. package/lib/paradigms/ema-crossover.js +83 -0
  17. package/lib/paradigms/index.d.ts +12 -0
  18. package/lib/paradigms/index.js +23 -0
  19. package/lib/paradigms/momentum-12m.d.ts +5 -0
  20. package/lib/paradigms/momentum-12m.js +73 -0
  21. package/lib/paradigms/rsi-reversion.d.ts +5 -0
  22. package/lib/paradigms/rsi-reversion.js +89 -0
  23. package/lib/paradigms/sma-baseline.d.ts +5 -0
  24. package/lib/paradigms/sma-baseline.js +70 -0
  25. package/lib/plugin.d.ts +44 -0
  26. package/lib/plugin.js +214 -0
  27. package/lib/screeners/above-ma.d.ts +1 -0
  28. package/lib/screeners/above-ma.js +66 -0
  29. package/lib/screeners/index.d.ts +11 -0
  30. package/lib/screeners/index.js +18 -0
  31. package/lib/screeners/ma-bull-align.d.ts +1 -0
  32. package/lib/screeners/ma-bull-align.js +68 -0
  33. package/lib/screeners/near-high.d.ts +1 -0
  34. package/lib/screeners/near-high.js +48 -0
  35. package/lib/screeners/rsi-oversold.d.ts +1 -0
  36. package/lib/screeners/rsi-oversold.js +50 -0
  37. package/lib/screeners/types.d.ts +38 -0
  38. package/lib/screeners/volume-breakout.d.ts +1 -0
  39. package/lib/screeners/volume-breakout.js +63 -0
  40. package/lib/types.d.ts +81 -0
  41. package/lib/validate-node.js +40 -0
  42. package/lib/validate.d.ts +28 -0
  43. package/lib/validate.js +264 -0
  44. package/package.json +40 -0
package/lib/engine.js ADDED
@@ -0,0 +1,165 @@
1
+ //#region src/engine.ts
2
+ const DEFAULT_INITIAL_CAPITAL = 1e5;
3
+ const DEFAULT_FEE_RATE = .001;
4
+ const MS_PER_YEAR = 315576e5;
5
+ /** 推断一年的 bar 根数(根据相邻 K 线平均时间间隔)。 */
6
+ function estimateBarsPerYear(bars) {
7
+ if (bars.length < 2) return 250;
8
+ const totalSpan = bars[bars.length - 1].openTime - bars[0].openTime;
9
+ if (totalSpan <= 0) return 250;
10
+ const avgInterval = totalSpan / (bars.length - 1);
11
+ if (avgInterval <= 0) return 250;
12
+ const barsPerYear = MS_PER_YEAR / avgInterval;
13
+ return Math.max(1, Math.min(barsPerYear, 525600));
14
+ }
15
+ function run(bars, strategy, paramsOverride = {}, options = {}) {
16
+ const initialCapital = options.initialCapital ?? DEFAULT_INITIAL_CAPITAL;
17
+ const feeRate = options.feeRate ?? DEFAULT_FEE_RATE;
18
+ const slippage = options.slippage ?? 0;
19
+ if (!bars || bars.length === 0) return {
20
+ signals: [],
21
+ trades: [],
22
+ equity: [],
23
+ metrics: {
24
+ totalReturn: 0,
25
+ cagr: 0,
26
+ maxDrawdown: 0,
27
+ sharpe: 0,
28
+ winRate: 0,
29
+ profitFactor: 0,
30
+ tradeCount: 0,
31
+ exposure: 0
32
+ },
33
+ initialCapital,
34
+ finalCapital: initialCapital
35
+ };
36
+ const resolvedParams = {};
37
+ for (const p of strategy.params) resolvedParams[p.key] = paramsOverride[p.key] ?? p.default;
38
+ const signals = strategy.compute(bars, resolvedParams);
39
+ const signalMap = /* @__PURE__ */ new Map();
40
+ for (const s of signals) if (s.index >= 0 && s.index < bars.length) signalMap.set(s.index, s);
41
+ let cash = initialCapital;
42
+ let shares = 0;
43
+ let position = "flat";
44
+ let entryIndex = -1;
45
+ let entryTime = 0;
46
+ let entryPrice = 0;
47
+ let entryCashCost = 0;
48
+ let totalHoldingBars = 0;
49
+ const trades = [];
50
+ const equityPoints = [];
51
+ let peakEquity = initialCapital;
52
+ let maxDrawdown = 0;
53
+ for (let i = 0; i < bars.length; i++) {
54
+ const currentBar = bars[i];
55
+ if (i > 0) {
56
+ const prevSignal = signalMap.get(i - 1);
57
+ if (prevSignal) {
58
+ if (prevSignal.action === "entry" && position === "flat") {
59
+ const executedBuyPrice = currentBar.open * (1 + slippage);
60
+ if (executedBuyPrice > 0) {
61
+ const costPerShare = executedBuyPrice * (1 + feeRate);
62
+ shares = cash / costPerShare;
63
+ entryCashCost = cash;
64
+ cash = 0;
65
+ position = "long";
66
+ entryIndex = i;
67
+ entryTime = currentBar.openTime;
68
+ entryPrice = executedBuyPrice;
69
+ }
70
+ } else if (prevSignal.action === "exit" && position === "long") {
71
+ const executedSellPrice = currentBar.open * (1 - slippage);
72
+ const netCash = shares * executedSellPrice * (1 - feeRate);
73
+ const profit = netCash - entryCashCost;
74
+ const returnPercent = (executedSellPrice / entryPrice * (1 - feeRate) / (1 + feeRate) - 1) * 100;
75
+ const holdingBars = i - entryIndex;
76
+ trades.push({
77
+ entryIndex,
78
+ entryTime,
79
+ entryPrice,
80
+ exitIndex: i,
81
+ exitTime: currentBar.openTime,
82
+ exitPrice: executedSellPrice,
83
+ returnPercent,
84
+ profit,
85
+ holdingBars,
86
+ exitReason: prevSignal.reason,
87
+ ...prevSignal.reasonKey !== void 0 ? { exitReasonKey: prevSignal.reasonKey } : {},
88
+ ...prevSignal.reasonParams !== void 0 ? { exitReasonParams: prevSignal.reasonParams } : {}
89
+ });
90
+ cash = netCash;
91
+ shares = 0;
92
+ position = "flat";
93
+ entryIndex = -1;
94
+ }
95
+ }
96
+ }
97
+ if (position === "long") totalHoldingBars += 1;
98
+ let currentEquity;
99
+ if (position === "long") currentEquity = shares * currentBar.close * (1 - feeRate);
100
+ else currentEquity = cash;
101
+ if (currentEquity > peakEquity) peakEquity = currentEquity;
102
+ const currentDrawdown = peakEquity > 0 ? (currentEquity - peakEquity) / peakEquity * 100 : 0;
103
+ if (currentDrawdown < maxDrawdown) maxDrawdown = currentDrawdown;
104
+ equityPoints.push({
105
+ time: currentBar.openTime,
106
+ equity: currentEquity,
107
+ drawdownPercent: currentDrawdown
108
+ });
109
+ }
110
+ const finalCapital = equityPoints[equityPoints.length - 1]?.equity ?? initialCapital;
111
+ const totalReturn = (finalCapital - initialCapital) / initialCapital * 100;
112
+ const tradeCount = trades.length;
113
+ const winningTrades = trades.filter((t) => t.profit > 0);
114
+ const winRate = tradeCount > 0 ? winningTrades.length / tradeCount * 100 : 0;
115
+ const totalWinAmount = trades.filter((t) => t.profit > 0).reduce((sum, t) => sum + t.profit, 0);
116
+ const totalLossAmount = trades.filter((t) => t.profit < 0).reduce((sum, t) => sum + Math.abs(t.profit), 0);
117
+ let profitFactor = 0;
118
+ if (totalLossAmount === 0) profitFactor = totalWinAmount > 0 ? Infinity : 0;
119
+ else profitFactor = totalWinAmount / totalLossAmount;
120
+ const exposure = bars.length > 0 ? totalHoldingBars / bars.length * 100 : 0;
121
+ let cagr = 0;
122
+ if (bars.length > 1) {
123
+ const years = (bars[bars.length - 1].openTime - bars[0].openTime) / MS_PER_YEAR;
124
+ if (years > .01 && finalCapital > 0) cagr = (Math.pow(finalCapital / initialCapital, 1 / years) - 1) * 100;
125
+ else if (finalCapital <= 0) cagr = -100;
126
+ }
127
+ let sharpe = 0;
128
+ if (equityPoints.length > 1) {
129
+ const returns = [];
130
+ for (let k = 1; k < equityPoints.length; k++) {
131
+ const prev = equityPoints[k - 1].equity;
132
+ const curr = equityPoints[k].equity;
133
+ if (prev > 0) returns.push((curr - prev) / prev);
134
+ else returns.push(0);
135
+ }
136
+ if (returns.length > 1) {
137
+ const mean = returns.reduce((sum, r) => sum + r, 0) / returns.length;
138
+ const variance = returns.reduce((sum, r) => sum + Math.pow(r - mean, 2), 0) / (returns.length - 1);
139
+ const stdev = Math.sqrt(variance);
140
+ if (stdev > 0) {
141
+ const annualFactor = Math.sqrt(estimateBarsPerYear(bars));
142
+ sharpe = mean / stdev * annualFactor;
143
+ }
144
+ }
145
+ }
146
+ return {
147
+ signals,
148
+ trades,
149
+ equity: equityPoints,
150
+ metrics: {
151
+ totalReturn,
152
+ cagr,
153
+ maxDrawdown,
154
+ sharpe,
155
+ winRate,
156
+ profitFactor,
157
+ tradeCount,
158
+ exposure
159
+ },
160
+ initialCapital,
161
+ finalCapital
162
+ };
163
+ }
164
+ //#endregion
165
+ export { run };
package/lib/index.d.ts ADDED
@@ -0,0 +1,14 @@
1
+ import { BacktestMetrics, BacktestOptions, BacktestResult, EquityPoint, Kline, SignalAction, StrategyDefinition, StrategyHorizon, StrategyParamSpec, StrategySignal, TradeRecord } from "./types.js";
2
+ import { run } from "./engine.js";
3
+ import { CustomStrategyRecord, CustomStrategyStore, createMemoryCustomStrategyStore } from "./custom.js";
4
+ import { StrategyValidationResult, compileStrategySource, validateCustomStrategy, validateSignalSequence } from "./validate.js";
5
+ import { ScreenerColumnSpec, ScreenerDefinition, ScreenerMatch } from "./screeners/types.js";
6
+ import { getScreenerById, screenerParadigms } from "./screeners/index.js";
7
+ import { donchianBreakoutStrategy } from "./paradigms/donchian-breakout.js";
8
+ import { rsiReversionStrategy } from "./paradigms/rsi-reversion.js";
9
+ import { emaCrossoverStrategy } from "./paradigms/ema-crossover.js";
10
+ import { bollingerReversionStrategy } from "./paradigms/bollinger-reversion.js";
11
+ import { smaBaselineStrategy } from "./paradigms/sma-baseline.js";
12
+ import { momentum12mStrategy } from "./paradigms/momentum-12m.js";
13
+ import { getStrategyById, strategyParadigms } from "./paradigms/index.js";
14
+ export { type BacktestMetrics, type BacktestOptions, type BacktestResult, type CustomStrategyRecord, type CustomStrategyStore, type EquityPoint, type Kline, type ScreenerColumnSpec, type ScreenerDefinition, type ScreenerMatch, type SignalAction, type StrategyDefinition, type StrategyHorizon, type StrategyParamSpec, type StrategySignal, type StrategyValidationResult, type TradeRecord, bollingerReversionStrategy, compileStrategySource, createMemoryCustomStrategyStore, donchianBreakoutStrategy, emaCrossoverStrategy, getScreenerById, getStrategyById, momentum12mStrategy, rsiReversionStrategy, run, screenerParadigms, smaBaselineStrategy, strategyParadigms, validateCustomStrategy, validateSignalSequence };
package/lib/index.js ADDED
@@ -0,0 +1,12 @@
1
+ import { run } from "./engine.js";
2
+ import { createMemoryCustomStrategyStore } from "./custom.js";
3
+ import { compileStrategySource, validateCustomStrategy, validateSignalSequence } from "./validate.js";
4
+ import { getScreenerById, screenerParadigms } from "./screeners/index.js";
5
+ import { donchianBreakoutStrategy } from "./paradigms/donchian-breakout.js";
6
+ import { rsiReversionStrategy } from "./paradigms/rsi-reversion.js";
7
+ import { emaCrossoverStrategy } from "./paradigms/ema-crossover.js";
8
+ import { bollingerReversionStrategy } from "./paradigms/bollinger-reversion.js";
9
+ import { smaBaselineStrategy } from "./paradigms/sma-baseline.js";
10
+ import { momentum12mStrategy } from "./paradigms/momentum-12m.js";
11
+ import { getStrategyById, strategyParadigms } from "./paradigms/index.js";
12
+ export { bollingerReversionStrategy, compileStrategySource, createMemoryCustomStrategyStore, donchianBreakoutStrategy, emaCrossoverStrategy, getScreenerById, getStrategyById, momentum12mStrategy, rsiReversionStrategy, run, screenerParadigms, smaBaselineStrategy, strategyParadigms, validateCustomStrategy, validateSignalSequence };
@@ -0,0 +1,5 @@
1
+ import { StrategyDefinition } from "../types.js";
2
+ //#region src/paradigms/bollinger-reversion.d.ts
3
+ declare const bollingerReversionStrategy: StrategyDefinition;
4
+ //#endregion
5
+ export { bollingerReversionStrategy };
@@ -0,0 +1,77 @@
1
+ import { bollinger } from "@dshtrading/indicators";
2
+ //#region src/paradigms/bollinger-reversion.ts
3
+ /**
4
+ * 布林带下轨均值回归策略(波段)。
5
+ *
6
+ * 入场:收盘价跌破布林线下轨(超卖错杀)。
7
+ * 出场:收盘价回归至布林带中轨(均线目标位平仓)。
8
+ */
9
+ const bollingerReversionStrategy = {
10
+ id: "bollinger-reversion",
11
+ horizon: "swing",
12
+ name: "布林带下轨均值回归",
13
+ summary: "价格跌破布林线下轨时介入抄底,反弹至中轨(基准均线)时平仓(波段通道回归)",
14
+ params: [{
15
+ key: "period",
16
+ label: "布林周期",
17
+ default: 20,
18
+ min: 5,
19
+ max: 50,
20
+ step: 1
21
+ }, {
22
+ key: "multiplier",
23
+ label: "标准差倍数 (k)",
24
+ default: 2,
25
+ min: 1,
26
+ max: 4,
27
+ step: .5
28
+ }],
29
+ compute(bars, params) {
30
+ const period = Math.max(5, Math.round(params.period ?? 20));
31
+ const k = Number(params.multiplier ?? 2);
32
+ const closes = bars.map((b) => b.close);
33
+ const { mid, lower } = bollinger(closes, period, k);
34
+ const signals = [];
35
+ let inPosition = false;
36
+ for (let i = period - 1; i < bars.length; i++) {
37
+ const currentClose = bars[i].close;
38
+ const currentLower = lower[i];
39
+ const currentMid = mid[i];
40
+ if (currentLower === void 0 || currentMid === void 0) continue;
41
+ if (!inPosition && currentClose < currentLower) {
42
+ signals.push({
43
+ index: i,
44
+ time: bars[i].openTime,
45
+ action: "entry",
46
+ direction: "long",
47
+ price: currentClose,
48
+ reason: `收盘价 (${currentClose.toFixed(2)}) 跌破布林下轨 (${currentLower.toFixed(2)}),触发波段均值回归`,
49
+ reasonKey: "strat.bollinger-reversion.reason.entry",
50
+ reasonParams: {
51
+ close: currentClose.toFixed(2),
52
+ band: currentLower.toFixed(2)
53
+ }
54
+ });
55
+ inPosition = true;
56
+ } else if (inPosition && currentClose >= currentMid) {
57
+ signals.push({
58
+ index: i,
59
+ time: bars[i].openTime,
60
+ action: "exit",
61
+ direction: "flat",
62
+ price: currentClose,
63
+ reason: `收盘价 (${currentClose.toFixed(2)}) 成功回归至布林中轨 (${currentMid.toFixed(2)}),完成目标止盈`,
64
+ reasonKey: "strat.bollinger-reversion.reason.exit",
65
+ reasonParams: {
66
+ close: currentClose.toFixed(2),
67
+ mid: currentMid.toFixed(2)
68
+ }
69
+ });
70
+ inPosition = false;
71
+ }
72
+ }
73
+ return signals;
74
+ }
75
+ };
76
+ //#endregion
77
+ export { bollingerReversionStrategy };
@@ -0,0 +1,5 @@
1
+ import { StrategyDefinition } from "../types.js";
2
+ //#region src/paradigms/donchian-breakout.d.ts
3
+ declare const donchianBreakoutStrategy: StrategyDefinition;
4
+ //#endregion
5
+ export { donchianBreakoutStrategy };
@@ -0,0 +1,71 @@
1
+ //#region src/paradigms/donchian-breakout.ts
2
+ const donchianBreakoutStrategy = {
3
+ id: "donchian-breakout",
4
+ horizon: "short",
5
+ name: "唐奇安通道突破",
6
+ summary: "收盘价突破前 N1 根最高价做多,跌破前 N2 根最低价离场(海龟经典简化版)",
7
+ params: [{
8
+ key: "lookbackEntry",
9
+ label: "突破周期 (N1)",
10
+ default: 20,
11
+ min: 5,
12
+ max: 100,
13
+ step: 1
14
+ }, {
15
+ key: "lookbackExit",
16
+ label: "离场周期 (N2)",
17
+ default: 10,
18
+ min: 2,
19
+ max: 50,
20
+ step: 1
21
+ }],
22
+ compute(bars, params) {
23
+ const n1 = Math.max(2, Math.round(params.lookbackEntry ?? 20));
24
+ const n2 = Math.max(1, Math.round(params.lookbackExit ?? 10));
25
+ const signals = [];
26
+ let inPosition = false;
27
+ for (let i = Math.max(n1, n2); i < bars.length; i++) {
28
+ const currentClose = bars[i].close;
29
+ let highestHigh = -Infinity;
30
+ for (let j = i - n1; j < i; j++) if (bars[j].high > highestHigh) highestHigh = bars[j].high;
31
+ let lowestLow = Infinity;
32
+ for (let j = i - n2; j < i; j++) if (bars[j].low < lowestLow) lowestLow = bars[j].low;
33
+ if (!inPosition && currentClose > highestHigh) {
34
+ signals.push({
35
+ index: i,
36
+ time: bars[i].openTime,
37
+ action: "entry",
38
+ direction: "long",
39
+ price: currentClose,
40
+ reason: `收盘价 (${currentClose.toFixed(2)}) 突破前 ${n1} 根最高价 (${highestHigh.toFixed(2)})`,
41
+ reasonKey: "strat.donchian-breakout.reason.entry",
42
+ reasonParams: {
43
+ close: currentClose.toFixed(2),
44
+ n: n1,
45
+ high: highestHigh.toFixed(2)
46
+ }
47
+ });
48
+ inPosition = true;
49
+ } else if (inPosition && currentClose < lowestLow) {
50
+ signals.push({
51
+ index: i,
52
+ time: bars[i].openTime,
53
+ action: "exit",
54
+ direction: "flat",
55
+ price: currentClose,
56
+ reason: `收盘价 (${currentClose.toFixed(2)}) 跌破前 ${n2} 根最低价 (${lowestLow.toFixed(2)})`,
57
+ reasonKey: "strat.donchian-breakout.reason.exit",
58
+ reasonParams: {
59
+ close: currentClose.toFixed(2),
60
+ n: n2,
61
+ low: lowestLow.toFixed(2)
62
+ }
63
+ });
64
+ inPosition = false;
65
+ }
66
+ }
67
+ return signals;
68
+ }
69
+ };
70
+ //#endregion
71
+ export { donchianBreakoutStrategy };
@@ -0,0 +1,5 @@
1
+ import { StrategyDefinition } from "../types.js";
2
+ //#region src/paradigms/ema-crossover.d.ts
3
+ declare const emaCrossoverStrategy: StrategyDefinition;
4
+ //#endregion
5
+ export { emaCrossoverStrategy };
@@ -0,0 +1,83 @@
1
+ import { ema } from "@dshtrading/indicators";
2
+ //#region src/paradigms/ema-crossover.ts
3
+ /**
4
+ * 双均线趋势跟踪策略(波段)。
5
+ *
6
+ * 入场:EMA(fast) 上穿 EMA(slow) 金叉做多。
7
+ * 出场:EMA(fast) 下穿 EMA(slow) 死叉离场。
8
+ */
9
+ const emaCrossoverStrategy = {
10
+ id: "ema-crossover",
11
+ horizon: "swing",
12
+ name: "EMA 双均线趋势跟踪",
13
+ summary: "快线 EMA 上穿慢线 EMA 金叉做多,死叉平仓(经典中线波段趋势策略)",
14
+ params: [{
15
+ key: "fastPeriod",
16
+ label: "快线周期 (Fast)",
17
+ default: 20,
18
+ min: 5,
19
+ max: 50,
20
+ step: 1
21
+ }, {
22
+ key: "slowPeriod",
23
+ label: "慢线周期 (Slow)",
24
+ default: 60,
25
+ min: 20,
26
+ max: 200,
27
+ step: 1
28
+ }],
29
+ compute(bars, params) {
30
+ const fastP = Math.max(2, Math.round(params.fastPeriod ?? 20));
31
+ const slowP = Math.max(fastP + 1, Math.round(params.slowPeriod ?? 60));
32
+ const closes = bars.map((b) => b.close);
33
+ const fastEma = ema(closes, fastP);
34
+ const slowEma = ema(closes, slowP);
35
+ const signals = [];
36
+ let inPosition = false;
37
+ for (let i = 1; i < bars.length; i++) {
38
+ const prevFast = fastEma[i - 1];
39
+ const prevSlow = slowEma[i - 1];
40
+ const currFast = fastEma[i];
41
+ const currSlow = slowEma[i];
42
+ if (prevFast === void 0 || prevSlow === void 0 || currFast === void 0 || currSlow === void 0) continue;
43
+ if (!inPosition && prevFast <= prevSlow && currFast > currSlow) {
44
+ signals.push({
45
+ index: i,
46
+ time: bars[i].openTime,
47
+ action: "entry",
48
+ direction: "long",
49
+ price: bars[i].close,
50
+ reason: `EMA(${fastP}) (${currFast.toFixed(2)}) 上穿 EMA(${slowP}) (${currSlow.toFixed(2)}) 形成金叉`,
51
+ reasonKey: "strat.ema-crossover.reason.entry",
52
+ reasonParams: {
53
+ fastP,
54
+ fast: currFast.toFixed(2),
55
+ slowP,
56
+ slow: currSlow.toFixed(2)
57
+ }
58
+ });
59
+ inPosition = true;
60
+ } else if (inPosition && prevFast >= prevSlow && currFast < currSlow) {
61
+ signals.push({
62
+ index: i,
63
+ time: bars[i].openTime,
64
+ action: "exit",
65
+ direction: "flat",
66
+ price: bars[i].close,
67
+ reason: `EMA(${fastP}) (${currFast.toFixed(2)}) 下穿 EMA(${slowP}) (${currSlow.toFixed(2)}) 形成死叉`,
68
+ reasonKey: "strat.ema-crossover.reason.exit",
69
+ reasonParams: {
70
+ fastP,
71
+ fast: currFast.toFixed(2),
72
+ slowP,
73
+ slow: currSlow.toFixed(2)
74
+ }
75
+ });
76
+ inPosition = false;
77
+ }
78
+ }
79
+ return signals;
80
+ }
81
+ };
82
+ //#endregion
83
+ export { emaCrossoverStrategy };
@@ -0,0 +1,12 @@
1
+ import { StrategyDefinition } from "../types.js";
2
+ import { donchianBreakoutStrategy } from "./donchian-breakout.js";
3
+ import { rsiReversionStrategy } from "./rsi-reversion.js";
4
+ import { emaCrossoverStrategy } from "./ema-crossover.js";
5
+ import { bollingerReversionStrategy } from "./bollinger-reversion.js";
6
+ import { smaBaselineStrategy } from "./sma-baseline.js";
7
+ import { momentum12mStrategy } from "./momentum-12m.js";
8
+ //#region src/paradigms/index.d.ts
9
+ declare const strategyParadigms: readonly StrategyDefinition[];
10
+ declare function getStrategyById(id: string): StrategyDefinition | undefined;
11
+ //#endregion
12
+ export { bollingerReversionStrategy, donchianBreakoutStrategy, emaCrossoverStrategy, getStrategyById, momentum12mStrategy, rsiReversionStrategy, smaBaselineStrategy, strategyParadigms };
@@ -0,0 +1,23 @@
1
+ import { donchianBreakoutStrategy } from "./donchian-breakout.js";
2
+ import { rsiReversionStrategy } from "./rsi-reversion.js";
3
+ import { emaCrossoverStrategy } from "./ema-crossover.js";
4
+ import { bollingerReversionStrategy } from "./bollinger-reversion.js";
5
+ import { smaBaselineStrategy } from "./sma-baseline.js";
6
+ import { momentum12mStrategy } from "./momentum-12m.js";
7
+ //#region src/paradigms/index.ts
8
+ /**
9
+ * 6 个经典策略参考范式汇聚。
10
+ */
11
+ const strategyParadigms = [
12
+ donchianBreakoutStrategy,
13
+ rsiReversionStrategy,
14
+ emaCrossoverStrategy,
15
+ bollingerReversionStrategy,
16
+ smaBaselineStrategy,
17
+ momentum12mStrategy
18
+ ];
19
+ function getStrategyById(id) {
20
+ return strategyParadigms.find((s) => s.id === id);
21
+ }
22
+ //#endregion
23
+ export { bollingerReversionStrategy, donchianBreakoutStrategy, emaCrossoverStrategy, getStrategyById, momentum12mStrategy, rsiReversionStrategy, smaBaselineStrategy, strategyParadigms };
@@ -0,0 +1,5 @@
1
+ import { StrategyDefinition } from "../types.js";
2
+ //#region src/paradigms/momentum-12m.d.ts
3
+ declare const momentum12mStrategy: StrategyDefinition;
4
+ //#endregion
5
+ export { momentum12mStrategy };
@@ -0,0 +1,73 @@
1
+ import { sma } from "@dshtrading/indicators";
2
+ //#region src/paradigms/momentum-12m.ts
3
+ /**
4
+ * 12 个月动量择时策略(长线,Fama-French 动量单标的经典化)。
5
+ *
6
+ * 入场:近 12 个月(约 250 根日 K)累计收益 > 0 且价格高于年线,强动量做多。
7
+ * 出场:动量转负或价格跌破年线,动量衰竭离场。
8
+ */
9
+ const momentum12mStrategy = {
10
+ id: "momentum-12m",
11
+ horizon: "long",
12
+ name: "12 个月动量择时",
13
+ summary: "近 12 个月动量为正且站上年线做多,动量转负或破位离场(低换手长线动量)",
14
+ params: [{
15
+ key: "lookbackBars",
16
+ label: "动量回溯周期 (K线)",
17
+ default: 250,
18
+ min: 50,
19
+ max: 500,
20
+ step: 10
21
+ }],
22
+ compute(bars, params) {
23
+ const lookback = Math.max(10, Math.round(params.lookbackBars ?? 250));
24
+ const closes = bars.map((b) => b.close);
25
+ const smaValues = sma(closes, lookback);
26
+ const signals = [];
27
+ let inPosition = false;
28
+ for (let i = lookback; i < bars.length; i++) {
29
+ const currentClose = bars[i].close;
30
+ const pastClose = bars[i - lookback].close;
31
+ const currentSma = smaValues[i];
32
+ if (pastClose <= 0 || currentSma === void 0) continue;
33
+ const momentumReturn = (currentClose - pastClose) / pastClose;
34
+ if (!inPosition && momentumReturn > 0 && currentClose > currentSma) {
35
+ signals.push({
36
+ index: i,
37
+ time: bars[i].openTime,
38
+ action: "entry",
39
+ direction: "long",
40
+ price: currentClose,
41
+ reason: `近 ${lookback} 周期动量为正 (+${(momentumReturn * 100).toFixed(1)}%) 且位于均线 (${currentSma.toFixed(2)}) 之上,确认强动量`,
42
+ reasonKey: "strat.momentum-12m.reason.entry",
43
+ reasonParams: {
44
+ n: lookback,
45
+ pct: (momentumReturn * 100).toFixed(1),
46
+ sma: currentSma.toFixed(2)
47
+ }
48
+ });
49
+ inPosition = true;
50
+ } else if (inPosition && (momentumReturn <= 0 || currentClose < currentSma)) {
51
+ const exitCause = momentumReturn <= 0 ? "动量转为负值" : "跌破基准均线";
52
+ signals.push({
53
+ index: i,
54
+ time: bars[i].openTime,
55
+ action: "exit",
56
+ direction: "flat",
57
+ price: currentClose,
58
+ reason: `${exitCause} (${(momentumReturn * 100).toFixed(1)}% / SMA ${currentSma.toFixed(2)}),动量衰减平仓`,
59
+ reasonKey: "strat.momentum-12m.reason.exit",
60
+ reasonParams: {
61
+ cause: momentumReturn <= 0 ? "momentumNegative" : "belowBaseline",
62
+ pct: (momentumReturn * 100).toFixed(1),
63
+ sma: currentSma.toFixed(2)
64
+ }
65
+ });
66
+ inPosition = false;
67
+ }
68
+ }
69
+ return signals;
70
+ }
71
+ };
72
+ //#endregion
73
+ export { momentum12mStrategy };
@@ -0,0 +1,5 @@
1
+ import { StrategyDefinition } from "../types.js";
2
+ //#region src/paradigms/rsi-reversion.d.ts
3
+ declare const rsiReversionStrategy: StrategyDefinition;
4
+ //#endregion
5
+ export { rsiReversionStrategy };
@@ -0,0 +1,89 @@
1
+ import { rsi } from "@dshtrading/indicators";
2
+ //#region src/paradigms/rsi-reversion.ts
3
+ /**
4
+ * 短周期 RSI 极值均值回归策略(短线,Connors 式)。
5
+ *
6
+ * 入场:RSI(2) < 10 极端超卖入场。
7
+ * 出场:RSI(2) > 60 快速反弹获利了结。
8
+ */
9
+ const rsiReversionStrategy = {
10
+ id: "rsi-reversion",
11
+ horizon: "short",
12
+ name: "RSI 短线极值回归",
13
+ summary: "短周期 RSI(2) 进入极端超卖区抄底,快速反弹后止盈(经典均值回归)",
14
+ params: [
15
+ {
16
+ key: "rsiPeriod",
17
+ label: "RSI 周期",
18
+ default: 2,
19
+ min: 2,
20
+ max: 14,
21
+ step: 1
22
+ },
23
+ {
24
+ key: "entryThreshold",
25
+ label: "入场超卖阈值",
26
+ default: 10,
27
+ min: 1,
28
+ max: 30,
29
+ step: 1
30
+ },
31
+ {
32
+ key: "exitThreshold",
33
+ label: "出场止盈阈值",
34
+ default: 60,
35
+ min: 50,
36
+ max: 95,
37
+ step: 1
38
+ }
39
+ ],
40
+ compute(bars, params) {
41
+ const period = Math.max(2, Math.round(params.rsiPeriod ?? 2));
42
+ const enterThresh = Number(params.entryThreshold ?? 10);
43
+ const exitThresh = Number(params.exitThreshold ?? 60);
44
+ const closes = bars.map((b) => b.close);
45
+ const rsiValues = rsi(closes, period);
46
+ const signals = [];
47
+ let inPosition = false;
48
+ for (let i = 0; i < bars.length; i++) {
49
+ const val = rsiValues[i];
50
+ if (val === void 0 || Number.isNaN(val)) continue;
51
+ if (!inPosition && val < enterThresh) {
52
+ signals.push({
53
+ index: i,
54
+ time: bars[i].openTime,
55
+ action: "entry",
56
+ direction: "long",
57
+ price: bars[i].close,
58
+ reason: `RSI(${period}) 达到极端超卖值 (${val.toFixed(1)} < ${enterThresh}),触发反弹买入`,
59
+ reasonKey: "strat.rsi-reversion.reason.entry",
60
+ reasonParams: {
61
+ period,
62
+ val: val.toFixed(1),
63
+ thresh: enterThresh
64
+ }
65
+ });
66
+ inPosition = true;
67
+ } else if (inPosition && val > exitThresh) {
68
+ signals.push({
69
+ index: i,
70
+ time: bars[i].openTime,
71
+ action: "exit",
72
+ direction: "flat",
73
+ price: bars[i].close,
74
+ reason: `RSI(${period}) 回升至目标位 (${val.toFixed(1)} > ${exitThresh}),止盈离场`,
75
+ reasonKey: "strat.rsi-reversion.reason.exit",
76
+ reasonParams: {
77
+ period,
78
+ val: val.toFixed(1),
79
+ thresh: exitThresh
80
+ }
81
+ });
82
+ inPosition = false;
83
+ }
84
+ }
85
+ return signals;
86
+ }
87
+ };
88
+ //#endregion
89
+ export { rsiReversionStrategy };
@@ -0,0 +1,5 @@
1
+ import { StrategyDefinition } from "../types.js";
2
+ //#region src/paradigms/sma-baseline.d.ts
3
+ declare const smaBaselineStrategy: StrategyDefinition;
4
+ //#endregion
5
+ export { smaBaselineStrategy };