bitbank-lab-mcp 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 (147) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +388 -0
  3. package/assets/lightweight-charts.standalone.js +7 -0
  4. package/bin/bitbank-lab-mcp.js +20 -0
  5. package/lib/cache.ts +70 -0
  6. package/lib/candle-utils.ts +48 -0
  7. package/lib/candle-validate.ts +434 -0
  8. package/lib/conversions.ts +25 -0
  9. package/lib/datetime.ts +157 -0
  10. package/lib/depth-analysis.ts +51 -0
  11. package/lib/error.ts +15 -0
  12. package/lib/formatter.ts +296 -0
  13. package/lib/get-depth.ts +111 -0
  14. package/lib/http.ts +132 -0
  15. package/lib/indicator-config.ts +39 -0
  16. package/lib/indicator_buffer.ts +41 -0
  17. package/lib/indicators.ts +579 -0
  18. package/lib/logger.ts +120 -0
  19. package/lib/ma-snapshot-utils.ts +277 -0
  20. package/lib/math.ts +89 -0
  21. package/lib/pattern-diagrams.ts +562 -0
  22. package/lib/result.ts +104 -0
  23. package/lib/validate.ts +154 -0
  24. package/lib/volatility.ts +132 -0
  25. package/package.json +79 -0
  26. package/src/env.ts +4 -0
  27. package/src/handlers/analyzeCandlePatternsHandler.ts +383 -0
  28. package/src/handlers/analyzeFibonacciHandler.ts +54 -0
  29. package/src/handlers/analyzeIndicatorsHandler.ts +682 -0
  30. package/src/handlers/analyzeMarketSignalHandler.ts +272 -0
  31. package/src/handlers/analyzeMyPortfolioHandler.ts +800 -0
  32. package/src/handlers/detectPatternsHandler.ts +77 -0
  33. package/src/handlers/detectPatternsViewsHandler.ts +518 -0
  34. package/src/handlers/getTickersJpyHandler.ts +145 -0
  35. package/src/handlers/getVolatilityMetricsHandler.ts +234 -0
  36. package/src/handlers/portfolio/calc.ts +549 -0
  37. package/src/handlers/portfolio/fetch.ts +318 -0
  38. package/src/handlers/portfolio/types.ts +170 -0
  39. package/src/handlers/renderChartSvgHandler.ts +69 -0
  40. package/src/handlers/runBacktestHandler.ts +70 -0
  41. package/src/http.ts +107 -0
  42. package/src/private/auth.ts +104 -0
  43. package/src/private/client.ts +298 -0
  44. package/src/private/config.ts +25 -0
  45. package/src/private/confirmation.ts +185 -0
  46. package/src/private/schemas.ts +866 -0
  47. package/src/prompts.ts +2296 -0
  48. package/src/resources/app-resources.ts +79 -0
  49. package/src/schema/analysis.ts +942 -0
  50. package/src/schema/backtest.ts +100 -0
  51. package/src/schema/base.ts +88 -0
  52. package/src/schema/candle-validate.ts +135 -0
  53. package/src/schema/chart.ts +399 -0
  54. package/src/schema/index.ts +11 -0
  55. package/src/schema/indicators.ts +125 -0
  56. package/src/schema/market-data.ts +298 -0
  57. package/src/schema/patterns.ts +382 -0
  58. package/src/schema/types.ts +97 -0
  59. package/src/schemas.d.ts +37 -0
  60. package/src/schemas.ts +7 -0
  61. package/src/server.ts +405 -0
  62. package/src/tool-definition.ts +44 -0
  63. package/src/tool-registry.ts +174 -0
  64. package/src/types/express-shim.d.ts +9 -0
  65. package/src/types/schemas.generated.d.ts +23 -0
  66. package/tools/analyze_bb_snapshot.ts +385 -0
  67. package/tools/analyze_candle_patterns.ts +810 -0
  68. package/tools/analyze_currency_strength.ts +273 -0
  69. package/tools/analyze_ema_snapshot.ts +183 -0
  70. package/tools/analyze_fibonacci.ts +530 -0
  71. package/tools/analyze_ichimoku_snapshot.ts +606 -0
  72. package/tools/analyze_indicators.ts +691 -0
  73. package/tools/analyze_market_signal.ts +665 -0
  74. package/tools/analyze_mtf_fibonacci.ts +273 -0
  75. package/tools/analyze_mtf_sma.ts +175 -0
  76. package/tools/analyze_sma_snapshot.ts +146 -0
  77. package/tools/analyze_stoch_snapshot.ts +276 -0
  78. package/tools/analyze_support_resistance.ts +817 -0
  79. package/tools/analyze_volume_profile.ts +546 -0
  80. package/tools/chart/ichimoku-cloud.ts +113 -0
  81. package/tools/chart/render-depth.ts +139 -0
  82. package/tools/chart/render-sub-panels.ts +208 -0
  83. package/tools/chart/svg-utils.ts +102 -0
  84. package/tools/detect_macd_cross.ts +691 -0
  85. package/tools/detect_patterns.ts +424 -0
  86. package/tools/detect_whale_events.ts +181 -0
  87. package/tools/get_candles.ts +487 -0
  88. package/tools/get_flow_metrics.ts +596 -0
  89. package/tools/get_orderbook.ts +540 -0
  90. package/tools/get_ticker.ts +132 -0
  91. package/tools/get_tickers_jpy.ts +240 -0
  92. package/tools/get_transactions.ts +209 -0
  93. package/tools/get_volatility_metrics.ts +302 -0
  94. package/tools/patterns/aftermath.ts +212 -0
  95. package/tools/patterns/config.ts +151 -0
  96. package/tools/patterns/detect_doubles.ts +650 -0
  97. package/tools/patterns/detect_hs.ts +635 -0
  98. package/tools/patterns/detect_pennants.ts +373 -0
  99. package/tools/patterns/detect_triangles.ts +820 -0
  100. package/tools/patterns/detect_triples.ts +633 -0
  101. package/tools/patterns/detect_wedges.ts +1072 -0
  102. package/tools/patterns/helpers.ts +517 -0
  103. package/tools/patterns/index.ts +40 -0
  104. package/tools/patterns/regression.ts +153 -0
  105. package/tools/patterns/smoothing.ts +168 -0
  106. package/tools/patterns/swing.ts +91 -0
  107. package/tools/patterns/types.ts +193 -0
  108. package/tools/prepare_chart_data.ts +294 -0
  109. package/tools/prepare_depth_data.ts +189 -0
  110. package/tools/private/analyze_my_portfolio.ts +21 -0
  111. package/tools/private/cancel_order.ts +127 -0
  112. package/tools/private/cancel_orders.ts +121 -0
  113. package/tools/private/create_order.ts +236 -0
  114. package/tools/private/get_margin_positions.ts +134 -0
  115. package/tools/private/get_margin_status.ts +155 -0
  116. package/tools/private/get_margin_trade_history.ts +156 -0
  117. package/tools/private/get_my_assets.ts +207 -0
  118. package/tools/private/get_my_deposit_withdrawal.ts +500 -0
  119. package/tools/private/get_my_orders.ts +157 -0
  120. package/tools/private/get_my_trade_history.ts +229 -0
  121. package/tools/private/get_order.ts +95 -0
  122. package/tools/private/get_orders_info.ts +90 -0
  123. package/tools/private/preview_cancel_order.ts +172 -0
  124. package/tools/private/preview_cancel_orders.ts +137 -0
  125. package/tools/private/preview_order.ts +292 -0
  126. package/tools/render_candle_pattern_diagram.ts +389 -0
  127. package/tools/render_chart_svg.ts +799 -0
  128. package/tools/render_depth_svg.ts +274 -0
  129. package/tools/trading_process/index.ts +7 -0
  130. package/tools/trading_process/lib/backtest_engine.ts +252 -0
  131. package/tools/trading_process/lib/equity.ts +131 -0
  132. package/tools/trading_process/lib/fetch_candles.ts +181 -0
  133. package/tools/trading_process/lib/sma.ts +62 -0
  134. package/tools/trading_process/lib/strategies/bb_breakout.ts +141 -0
  135. package/tools/trading_process/lib/strategies/index.ts +52 -0
  136. package/tools/trading_process/lib/strategies/macd_cross.ts +256 -0
  137. package/tools/trading_process/lib/strategies/rsi.ts +133 -0
  138. package/tools/trading_process/lib/strategies/sma_cross.ts +214 -0
  139. package/tools/trading_process/lib/strategies/types.ts +118 -0
  140. package/tools/trading_process/lib/svg_to_png.ts +64 -0
  141. package/tools/trading_process/render_backtest_chart_generic.ts +729 -0
  142. package/tools/trading_process/run_backtest.ts +243 -0
  143. package/tools/trading_process/types.ts +85 -0
  144. package/tools/validate_candle_data.ts +260 -0
  145. package/tsconfig.json +17 -0
  146. package/ui/cancel-confirm/dist/cancel-confirm.html +99 -0
  147. package/ui/order-confirm/dist/order-confirm.html +99 -0
@@ -0,0 +1,181 @@
1
+ /**
2
+ * lib/fetch_candles.ts - バックテスト用ローソク足取得
3
+ *
4
+ * 【データ品質保証】
5
+ * - time でソート(古い順)
6
+ * - time をキーに重複排除
7
+ * - 数値 NaN / time欠損は除外
8
+ */
9
+
10
+ import { dayjs } from '../../../lib/datetime.js';
11
+ import getCandles from '../../get_candles.js';
12
+ import type { Candle, Period, Timeframe } from '../types.js';
13
+
14
+ // 期間 → 必要本数のマッピング(バックテスト対象期間)
15
+ // 1D: 日足 → 1M=30日, 3M=90日, 6M=180日
16
+ // 4H: 4時間足 → 1M=180本(30日×6), 3M=540本, 6M=1080本
17
+ // 1H: 1時間足 → 1M=720本(30日×24), 3M=2160本, 6M=4320本
18
+ const PERIOD_TO_BARS: Record<Timeframe, Record<Period, number>> = {
19
+ '1D': { '1M': 30, '3M': 90, '6M': 180 },
20
+ '4H': { '1M': 180, '3M': 540, '6M': 1080 },
21
+ '1H': { '1M': 720, '3M': 2160, '6M': 4320 },
22
+ };
23
+
24
+ // timeframe → get_candles の type マッピング
25
+ const TIMEFRAME_TO_CANDLE_TYPE: Record<Timeframe, string> = {
26
+ '1D': '1day',
27
+ '4H': '4hour',
28
+ '1H': '1hour',
29
+ };
30
+
31
+ /**
32
+ * 期間に対応するバックテスト対象本数を取得
33
+ */
34
+ export function getPeriodBars(timeframe: Timeframe, period: Period): number {
35
+ return PERIOD_TO_BARS[timeframe]?.[period] ?? 90;
36
+ }
37
+
38
+ /**
39
+ * ローソク足データのバリデーション
40
+ *
41
+ * @param candle 検証対象
42
+ * @returns 有効なデータの場合 true
43
+ */
44
+ function isValidCandle(candle: Candle): boolean {
45
+ // time が空でないこと
46
+ if (!candle.time || candle.time.trim() === '') {
47
+ return false;
48
+ }
49
+
50
+ // time が有効な日付であること
51
+ const timestamp = dayjs(candle.time).valueOf();
52
+ if (Number.isNaN(timestamp)) {
53
+ return false;
54
+ }
55
+
56
+ // 数値が NaN でないこと
57
+ if (
58
+ Number.isNaN(candle.open) ||
59
+ Number.isNaN(candle.high) ||
60
+ Number.isNaN(candle.low) ||
61
+ Number.isNaN(candle.close)
62
+ ) {
63
+ return false;
64
+ }
65
+
66
+ // 価格が 0 以上であること
67
+ if (candle.open <= 0 || candle.high <= 0 || candle.low <= 0 || candle.close <= 0) {
68
+ return false;
69
+ }
70
+
71
+ return true;
72
+ }
73
+
74
+ /**
75
+ * バックテスト用にローソク足を取得
76
+ *
77
+ * 返すデータ構成:
78
+ * - 直近 `periodBars` 本 = バックテスト対象期間
79
+ * - その前に `smaLong` 本 = SMA計算用ウォームアップ期間
80
+ * - 合計: `periodBars + smaLong + buffer` 本
81
+ *
82
+ * 【データ品質保証】
83
+ * - time でソート(古い順)
84
+ * - time をキーに重複排除
85
+ * - 数値 NaN / time欠損は除外
86
+ *
87
+ * @param pair 通貨ペア
88
+ * @param timeframe 時間軸
89
+ * @param period 期間
90
+ * @param smaLong 長期SMAの期間(ウォームアップ用)
91
+ * @returns ローソク足配列(古い順、ウォームアップ込み)
92
+ */
93
+ export async function fetchCandlesForBacktest(
94
+ pair: string,
95
+ timeframe: Timeframe,
96
+ period: Period,
97
+ smaLong: number,
98
+ ): Promise<Candle[]> {
99
+ const periodBars = PERIOD_TO_BARS[timeframe]?.[period];
100
+ if (!periodBars) {
101
+ throw new Error(`Unsupported timeframe/period: ${timeframe}/${period}`);
102
+ }
103
+
104
+ // 必要な本数: バックテスト期間 + SMAウォームアップ + バッファ
105
+ const neededBars = periodBars + smaLong + 10;
106
+
107
+ // 日足の場合は複数年取得を発動させるため最低400を指定
108
+ // 時間足の場合はそのまま必要本数を指定
109
+ const fetchLimit = timeframe === '1D' ? Math.max(neededBars, 400) : neededBars;
110
+
111
+ const candleType = TIMEFRAME_TO_CANDLE_TYPE[timeframe];
112
+ if (!candleType) {
113
+ throw new Error(`Unsupported timeframe: ${timeframe}`);
114
+ }
115
+
116
+ const result = await getCandles(pair, candleType, undefined, fetchLimit);
117
+
118
+ if (!result.ok) {
119
+ throw new Error(`Failed to fetch candles: ${result.summary}`);
120
+ }
121
+
122
+ const normalized = result.data?.normalized;
123
+ if (!normalized || !Array.isArray(normalized) || normalized.length === 0) {
124
+ throw new Error('No candle data returned');
125
+ }
126
+
127
+ // 全データをCandle形式に変換
128
+ const rawCandles: Candle[] = normalized.map(
129
+ (c: {
130
+ isoTime?: string | null;
131
+ open: number;
132
+ high: number;
133
+ low: number;
134
+ close: number;
135
+ volume?: number | null;
136
+ }) => ({
137
+ time: c.isoTime || '',
138
+ open: Number(c.open),
139
+ high: Number(c.high),
140
+ low: Number(c.low),
141
+ close: Number(c.close),
142
+ volume: c.volume != null ? Number(c.volume) : undefined,
143
+ }),
144
+ );
145
+
146
+ // 1. バリデーション(無効データを除外)
147
+ const validCandles = rawCandles.filter(isValidCandle);
148
+
149
+ if (validCandles.length === 0) {
150
+ throw new Error('No valid candle data after filtering');
151
+ }
152
+
153
+ // 2. time でソート(古い順)
154
+ validCandles.sort((a, b) => {
155
+ const timeA = dayjs(a.time).valueOf();
156
+ const timeB = dayjs(b.time).valueOf();
157
+ return timeA - timeB;
158
+ });
159
+
160
+ // 3. 重複排除(time をキーに、後の方を優先)
161
+ const uniqueMap = new Map<string, Candle>();
162
+ for (const candle of validCandles) {
163
+ uniqueMap.set(candle.time, candle);
164
+ }
165
+ const uniqueCandles = Array.from(uniqueMap.values());
166
+
167
+ // 4. 再ソート(Map は順序を保証するが念のため)
168
+ uniqueCandles.sort((a, b) => {
169
+ const timeA = dayjs(a.time).valueOf();
170
+ const timeB = dayjs(b.time).valueOf();
171
+ return timeA - timeB;
172
+ });
173
+
174
+ // 5. 直近の必要な本数だけ切り出す(古い順を維持)
175
+ if (uniqueCandles.length <= neededBars) {
176
+ return uniqueCandles;
177
+ }
178
+
179
+ const startIdx = uniqueCandles.length - neededBars;
180
+ return uniqueCandles.slice(startIdx);
181
+ }
@@ -0,0 +1,62 @@
1
+ /**
2
+ * lib/sma.ts - 単純移動平均計算(lib/indicators.ts への委譲)
3
+ *
4
+ * 【計算仕様】
5
+ * - 入力: prices[0..n-1](古い順)
6
+ * - 出力: sma[0..n-1]
7
+ * - sma[0..period-2] = NaN(データ不足)
8
+ * - sma[period-1] = 最初の有効なSMA値
9
+ * - sma[i] = prices[i-period+1..i] の平均(i >= period-1)
10
+ *
11
+ * 【売買ループでの使用法】
12
+ * - SMA(period) が有効なのは index >= period-1
13
+ * - クロスオーバー判定には sma[i-1] と sma[i] が必要
14
+ * - したがって startIdx = period(安全マージンを含む)から開始
15
+ */
16
+
17
+ import { sma } from '../../../lib/indicators.js';
18
+
19
+ /**
20
+ * 単純移動平均を計算
21
+ *
22
+ * @param prices 価格配列(古い順)
23
+ * @param period 期間(正の整数)
24
+ * @returns SMA配列
25
+ * - 先頭 period-1 個は NaN
26
+ * - result[period-1] が最初の有効なSMA値
27
+ * - result[i] (i >= period-1) は prices[i-period+1..i] の平均
28
+ *
29
+ * @example
30
+ * const prices = [100, 102, 104, 103, 105];
31
+ * const sma3 = calculateSMA(prices, 3);
32
+ * // sma3 = [NaN, NaN, 102, 103, 104]
33
+ * // sma3[2] = (100 + 102 + 104) / 3 = 102
34
+ * // sma3[3] = (102 + 104 + 103) / 3 = 103
35
+ * // sma3[4] = (104 + 103 + 105) / 3 = 104
36
+ */
37
+ export function calculateSMA(prices: number[], period: number): number[] {
38
+ return sma(prices, period);
39
+ }
40
+
41
+ /**
42
+ * SMAが有効になる最初のインデックスを返す
43
+ *
44
+ * @param period SMAの期間
45
+ * @returns 最初の有効インデックス(= period - 1)
46
+ */
47
+ export function getFirstValidIndex(period: number): number {
48
+ return period - 1;
49
+ }
50
+
51
+ /**
52
+ * 売買ループで安全に使用できる開始インデックスを返す
53
+ *
54
+ * クロスオーバー判定には sma[i-1] と sma[i] が必要なため、
55
+ * period から開始するのが安全(period-1 は有効だが、period-2 は NaN)
56
+ *
57
+ * @param period SMAの期間(通常は長期SMAの期間を使用)
58
+ * @returns 売買ループの開始インデックス(= period)
59
+ */
60
+ export function getSafeStartIndex(period: number): number {
61
+ return period;
62
+ }
@@ -0,0 +1,141 @@
1
+ /**
2
+ * strategies/bb_breakout.ts - ボリンジャーバンドブレイクアウト戦略
3
+ *
4
+ * エントリー: 価格が下部バンド(-stddev σ)を下回った後、中央線(SMA)を上抜け
5
+ * エグジット: 価格が上部バンド(+stddev σ)に到達
6
+ */
7
+
8
+ import { bollingerBands } from '../../../../lib/indicators.js';
9
+ import type { Candle } from '../../types.js';
10
+ import type { Overlay, ParamValidationResult, Signal, Strategy } from './types.js';
11
+
12
+ /**
13
+ * BB戦略のデフォルトパラメータ
14
+ */
15
+ const DEFAULT_PARAMS = {
16
+ period: 20,
17
+ stddev: 2,
18
+ };
19
+
20
+ /**
21
+ * パラメータのバリデーション
22
+ */
23
+ export function validateParams(params: Record<string, number>): ParamValidationResult {
24
+ const errors: string[] = [];
25
+ const normalized = { ...DEFAULT_PARAMS, ...params };
26
+
27
+ if (normalized.period < 5) {
28
+ errors.push('period must be at least 5');
29
+ }
30
+ if (normalized.stddev <= 0) {
31
+ errors.push('stddev must be positive');
32
+ }
33
+
34
+ return {
35
+ valid: errors.length === 0,
36
+ errors,
37
+ normalizedParams: normalized,
38
+ };
39
+ }
40
+
41
+ /**
42
+ * ボリンジャーバンドブレイクアウト戦略
43
+ */
44
+ export const bbBreakoutStrategy: Strategy = {
45
+ name: 'Bollinger Bands Breakout',
46
+ type: 'bb_breakout',
47
+ requiredBars: 25,
48
+ defaultParams: DEFAULT_PARAMS,
49
+
50
+ generate(candles: Candle[], params: Record<string, number>): Signal[] {
51
+ const { period, stddev } = { ...DEFAULT_PARAMS, ...params };
52
+ const closes = candles.map((c) => c.close);
53
+ const { middle, upper, lower } = bollingerBands(closes, period, stddev);
54
+
55
+ const signals: Signal[] = [];
56
+ const startIdx = period + 1;
57
+
58
+ // 下部バンドを下回ったかどうかを追跡
59
+ let belowLowerBand = false;
60
+
61
+ for (let i = 0; i < candles.length; i++) {
62
+ if (i < startIdx) {
63
+ signals.push({ time: candles[i].time, action: 'hold' });
64
+ continue;
65
+ }
66
+
67
+ const close = closes[i];
68
+ const prevClose = closes[i - 1];
69
+ const mid = middle[i];
70
+ const prevMid = middle[i - 1];
71
+ const up = upper[i];
72
+ const low = lower[i];
73
+ const prevLow = lower[i - 1];
74
+
75
+ if (
76
+ Number.isNaN(mid) ||
77
+ Number.isNaN(up) ||
78
+ Number.isNaN(low) ||
79
+ Number.isNaN(prevMid) ||
80
+ Number.isNaN(prevLow)
81
+ ) {
82
+ signals.push({ time: candles[i].time, action: 'hold' });
83
+ continue;
84
+ }
85
+
86
+ // 下部バンドを下回ったら追跡開始
87
+ if (prevClose <= prevLow) {
88
+ belowLowerBand = true;
89
+ }
90
+
91
+ // エントリー: 下部バンドを下回った後、中央線を上抜け
92
+ if (belowLowerBand && prevClose <= prevMid && close > mid) {
93
+ signals.push({
94
+ time: candles[i].time,
95
+ action: 'buy',
96
+ reason: `BB Breakout: Price crossed above middle band (${mid.toFixed(0)})`,
97
+ });
98
+ belowLowerBand = false; // リセット
99
+ }
100
+ // エグジット: 上部バンドに到達
101
+ else if (close >= up) {
102
+ signals.push({
103
+ time: candles[i].time,
104
+ action: 'sell',
105
+ reason: `BB Upper Band reached: ${up.toFixed(0)}`,
106
+ });
107
+ belowLowerBand = false; // リセット
108
+ }
109
+ // シグナルなし
110
+ else {
111
+ signals.push({ time: candles[i].time, action: 'hold' });
112
+ }
113
+ }
114
+
115
+ return signals;
116
+ },
117
+
118
+ getOverlays(candles: Candle[], params: Record<string, number>): Overlay[] {
119
+ const { period, stddev } = { ...DEFAULT_PARAMS, ...params };
120
+ const closes = candles.map((c) => c.close);
121
+ const { middle, upper, lower } = bollingerBands(closes, period, stddev);
122
+
123
+ return [
124
+ {
125
+ type: 'line',
126
+ name: `BB Middle(${period})`,
127
+ color: '#fbbf24', // yellow(Closeの青と区別)
128
+ data: middle,
129
+ },
130
+ {
131
+ type: 'band',
132
+ name: `BB ±${stddev}σ`,
133
+ color: '#a855f7', // purple
134
+ fillColor: 'rgba(168, 85, 247, 0.15)',
135
+ data: { upper, middle, lower },
136
+ },
137
+ ];
138
+ },
139
+ };
140
+
141
+ export default bbBreakoutStrategy;
@@ -0,0 +1,52 @@
1
+ /**
2
+ * strategies/index.ts - 戦略レジストリ
3
+ */
4
+
5
+ import { bbBreakoutStrategy } from './bb_breakout.js';
6
+ import { macdCrossStrategy } from './macd_cross.js';
7
+ import { rsiStrategy } from './rsi.js';
8
+ import { smaCrossStrategy } from './sma_cross.js';
9
+ import type { Strategy, StrategyRegistry, StrategyType } from './types.js';
10
+
11
+ /**
12
+ * 戦略レジストリ
13
+ */
14
+ const registry: StrategyRegistry = new Map<StrategyType, Strategy>();
15
+
16
+ // 戦略を登録
17
+ registry.set('sma_cross', smaCrossStrategy);
18
+ registry.set('rsi', rsiStrategy);
19
+ registry.set('macd_cross', macdCrossStrategy);
20
+ registry.set('bb_breakout', bbBreakoutStrategy);
21
+
22
+ /**
23
+ * 戦略を取得
24
+ */
25
+ export function getStrategy(type: StrategyType): Strategy | undefined {
26
+ return registry.get(type);
27
+ }
28
+
29
+ /**
30
+ * 利用可能な戦略タイプを取得
31
+ */
32
+ export function getAvailableStrategies(): StrategyType[] {
33
+ return Array.from(registry.keys());
34
+ }
35
+
36
+ /**
37
+ * 戦略を登録
38
+ */
39
+ export function registerStrategy(strategy: Strategy): void {
40
+ registry.set(strategy.type, strategy);
41
+ }
42
+
43
+ /**
44
+ * 戦略のデフォルトパラメータを取得
45
+ */
46
+ export function getStrategyDefaults(type: StrategyType): Record<string, number> | undefined {
47
+ const strategy = registry.get(type);
48
+ return strategy?.defaultParams;
49
+ }
50
+
51
+ export * from './types.js';
52
+ export { bbBreakoutStrategy, macdCrossStrategy, rsiStrategy, smaCrossStrategy };
@@ -0,0 +1,256 @@
1
+ /**
2
+ * strategies/macd_cross.ts - MACDクロスオーバー戦略
3
+ *
4
+ * エントリー: MACDラインがシグナルラインを上抜け(ゴールデンクロス)
5
+ * エグジット: MACDラインがシグナルラインを下抜け(デッドクロス)
6
+ *
7
+ * オプションフィルター:
8
+ * - sma_filter_period: SMAトレンドフィルター(例: 200)。価格がSMA上の場合のみ買い
9
+ * - zero_line_filter: ゼロラインフィルター(-1=ゼロ以下のみ, 0=なし, 1=ゼロ以上のみ)
10
+ * - rsi_filter_period: RSIフィルター期間(例: 14)。0で無効
11
+ * - rsi_filter_max: RSI上限(例: 70)。RSIがこの値未満の場合のみ買い
12
+ */
13
+
14
+ import { macd as sharedMacd } from '../../../../lib/indicators.js';
15
+ import type { Candle } from '../../types.js';
16
+ import { calculateSMA } from '../sma.js';
17
+ import { calculateRSI } from './rsi.js';
18
+ import type { Overlay, ParamValidationResult, Signal, Strategy } from './types.js';
19
+
20
+ /**
21
+ * MACD戦略のデフォルトパラメータ
22
+ */
23
+ const DEFAULT_PARAMS: Record<string, number> = {
24
+ fast: 12,
25
+ slow: 26,
26
+ signal: 9,
27
+ // フィルター(0 = 無効)
28
+ sma_filter_period: 0,
29
+ zero_line_filter: 0, // -1=below zero only, 0=none, 1=above zero only
30
+ rsi_filter_period: 0,
31
+ rsi_filter_max: 100, // RSI < この値 の場合のみ買い(100=フィルター無効)
32
+ };
33
+
34
+ /**
35
+ * MACDを計算(lib/indicators.ts への委譲)
36
+ */
37
+ function calculateMACD(
38
+ closes: number[],
39
+ fastPeriod: number,
40
+ slowPeriod: number,
41
+ signalPeriod: number,
42
+ ): { macd: number[]; signal: number[]; histogram: number[] } {
43
+ const result = sharedMacd(closes, fastPeriod, slowPeriod, signalPeriod);
44
+ return { macd: result.line, signal: result.signal, histogram: result.hist };
45
+ }
46
+
47
+ /**
48
+ * パラメータのバリデーション
49
+ */
50
+ export function validateParams(params: Record<string, number>): ParamValidationResult {
51
+ const errors: string[] = [];
52
+ const normalized = { ...DEFAULT_PARAMS, ...params };
53
+
54
+ if (normalized.fast >= normalized.slow) {
55
+ errors.push('fast period must be less than slow period');
56
+ }
57
+ if (normalized.fast < 2) {
58
+ errors.push('fast period must be at least 2');
59
+ }
60
+ if (normalized.signal < 2) {
61
+ errors.push('signal period must be at least 2');
62
+ }
63
+ if (normalized.sma_filter_period < 0) {
64
+ errors.push('sma_filter_period must be >= 0');
65
+ }
66
+ if (![-1, 0, 1].includes(normalized.zero_line_filter)) {
67
+ errors.push('zero_line_filter must be -1, 0, or 1');
68
+ }
69
+ if (normalized.rsi_filter_period < 0) {
70
+ errors.push('rsi_filter_period must be >= 0');
71
+ }
72
+ if (normalized.rsi_filter_max < 0 || normalized.rsi_filter_max > 100) {
73
+ errors.push('rsi_filter_max must be 0-100');
74
+ }
75
+
76
+ return {
77
+ valid: errors.length === 0,
78
+ errors,
79
+ normalizedParams: normalized,
80
+ };
81
+ }
82
+
83
+ /**
84
+ * フィルター条件の説明文を生成
85
+ */
86
+ function describeFilters(params: Record<string, number>): string {
87
+ const parts: string[] = [];
88
+ if (params.sma_filter_period > 0) {
89
+ parts.push(`SMA${params.sma_filter_period} trend filter`);
90
+ }
91
+ if (params.zero_line_filter === 1) {
92
+ parts.push('zero-line: above only');
93
+ } else if (params.zero_line_filter === -1) {
94
+ parts.push('zero-line: below only');
95
+ }
96
+ if (params.rsi_filter_period > 0 && params.rsi_filter_max < 100) {
97
+ parts.push(`RSI(${params.rsi_filter_period})<${params.rsi_filter_max}`);
98
+ }
99
+ return parts.length > 0 ? ` [${parts.join(', ')}]` : '';
100
+ }
101
+
102
+ /**
103
+ * MACDクロスオーバー戦略
104
+ */
105
+ export const macdCrossStrategy: Strategy = {
106
+ name: 'MACD Crossover',
107
+ type: 'macd_cross',
108
+ requiredBars: 35, // slow(26) + signal(9)
109
+ defaultParams: DEFAULT_PARAMS,
110
+
111
+ generate(candles: Candle[], params: Record<string, number>): Signal[] {
112
+ const p = { ...DEFAULT_PARAMS, ...params };
113
+ const { fast, slow, signal: signalPeriod } = p;
114
+ const closes = candles.map((c) => c.close);
115
+ const { macd, signal, histogram: _histogram } = calculateMACD(closes, fast, slow, signalPeriod);
116
+
117
+ // フィルター用の指標を事前計算
118
+ const sma = p.sma_filter_period > 0 ? calculateSMA(closes, p.sma_filter_period) : null;
119
+ const rsi = p.rsi_filter_period > 0 ? calculateRSI(closes, p.rsi_filter_period) : null;
120
+
121
+ const signals: Signal[] = [];
122
+ // SMAフィルターが有効な場合、開始インデックスをその分遅らせる
123
+ const baseStartIdx = slow + signalPeriod;
124
+ const startIdx = sma ? Math.max(baseStartIdx, p.sma_filter_period) : baseStartIdx;
125
+
126
+ for (let i = 0; i < candles.length; i++) {
127
+ if (i < startIdx) {
128
+ signals.push({ time: candles[i].time, action: 'hold' });
129
+ continue;
130
+ }
131
+
132
+ const prevMACD = macd[i - 1];
133
+ const prevSignal = signal[i - 1];
134
+ const currMACD = macd[i];
135
+ const currSignal = signal[i];
136
+
137
+ if (Number.isNaN(prevMACD) || Number.isNaN(prevSignal) || Number.isNaN(currMACD) || Number.isNaN(currSignal)) {
138
+ signals.push({ time: candles[i].time, action: 'hold' });
139
+ continue;
140
+ }
141
+
142
+ // ゴールデンクロス: MACDがシグナルを上抜け
143
+ if (prevMACD <= prevSignal && currMACD > currSignal) {
144
+ // フィルター適用(買いシグナルのみにフィルターを適用)
145
+ const filterReasons: string[] = [];
146
+ let filtered = false;
147
+
148
+ // SMAトレンドフィルター: 価格がSMA上の場合のみ
149
+ if (sma && !Number.isNaN(sma[i]) && closes[i] < sma[i]) {
150
+ filtered = true;
151
+ filterReasons.push(`price(${closes[i].toFixed(0)}) < SMA${p.sma_filter_period}(${sma[i].toFixed(0)})`);
152
+ }
153
+
154
+ // ゼロラインフィルター
155
+ if (p.zero_line_filter === 1 && currMACD < 0) {
156
+ filtered = true;
157
+ filterReasons.push(`MACD(${currMACD.toFixed(0)}) below zero`);
158
+ } else if (p.zero_line_filter === -1 && currMACD > 0) {
159
+ filtered = true;
160
+ filterReasons.push(`MACD(${currMACD.toFixed(0)}) above zero`);
161
+ }
162
+
163
+ // RSIフィルター
164
+ if (rsi && !Number.isNaN(rsi[i]) && rsi[i] >= p.rsi_filter_max) {
165
+ filtered = true;
166
+ filterReasons.push(`RSI(${rsi[i].toFixed(1)}) >= ${p.rsi_filter_max}`);
167
+ }
168
+
169
+ if (filtered) {
170
+ signals.push({ time: candles[i].time, action: 'hold' });
171
+ } else {
172
+ const filterDesc = describeFilters(p);
173
+ signals.push({
174
+ time: candles[i].time,
175
+ action: 'buy',
176
+ reason: `MACD Golden Cross: MACD(${currMACD.toFixed(0)}) > Signal(${currSignal.toFixed(0)})${filterDesc}`,
177
+ });
178
+ }
179
+ }
180
+ // デッドクロス: MACDがシグナルを下抜け(エグジットなのでフィルター適用しない)
181
+ else if (prevMACD >= prevSignal && currMACD < currSignal) {
182
+ signals.push({
183
+ time: candles[i].time,
184
+ action: 'sell',
185
+ reason: `MACD Dead Cross: MACD(${currMACD.toFixed(0)}) < Signal(${currSignal.toFixed(0)})`,
186
+ });
187
+ }
188
+ // シグナルなし
189
+ else {
190
+ signals.push({ time: candles[i].time, action: 'hold' });
191
+ }
192
+ }
193
+
194
+ return signals;
195
+ },
196
+
197
+ getOverlays(candles: Candle[], params: Record<string, number>): Overlay[] {
198
+ const p = { ...DEFAULT_PARAMS, ...params };
199
+ const { fast, slow, signal: signalPeriod } = p;
200
+ const closes = candles.map((c) => c.close);
201
+ const { macd, signal, histogram } = calculateMACD(closes, fast, slow, signalPeriod);
202
+
203
+ const overlays: Overlay[] = [
204
+ {
205
+ type: 'line' as const,
206
+ name: `MACD(${fast},${slow})`,
207
+ color: '#22c55e',
208
+ data: macd,
209
+ panel: 'indicator' as const,
210
+ },
211
+ {
212
+ type: 'line' as const,
213
+ name: `Signal(${signalPeriod})`,
214
+ color: '#f97316',
215
+ data: signal,
216
+ panel: 'indicator' as const,
217
+ },
218
+ {
219
+ type: 'histogram' as const,
220
+ name: 'Histogram',
221
+ positiveColor: 'rgba(34, 197, 94, 0.7)',
222
+ negativeColor: 'rgba(239, 68, 68, 0.7)',
223
+ data: histogram,
224
+ panel: 'indicator' as const,
225
+ },
226
+ ];
227
+
228
+ // SMAフィルターが有効な場合、SMAラインを価格チャートに表示
229
+ if (p.sma_filter_period > 0) {
230
+ const sma = calculateSMA(closes, p.sma_filter_period);
231
+ overlays.push({
232
+ type: 'line' as const,
233
+ name: `SMA${p.sma_filter_period} (filter)`,
234
+ color: '#facc15',
235
+ data: sma,
236
+ panel: 'price' as const,
237
+ });
238
+ }
239
+
240
+ // RSIフィルターが有効な場合、RSIラインをインジケータパネルに表示
241
+ if (p.rsi_filter_period > 0 && p.rsi_filter_max < 100) {
242
+ const rsi = calculateRSI(closes, p.rsi_filter_period);
243
+ overlays.push({
244
+ type: 'line' as const,
245
+ name: `RSI(${p.rsi_filter_period})`,
246
+ color: '#a78bfa',
247
+ data: rsi,
248
+ panel: 'indicator' as const,
249
+ });
250
+ }
251
+
252
+ return overlays;
253
+ },
254
+ };
255
+
256
+ export default macdCrossStrategy;