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,810 @@
1
+ /**
2
+ * analyze_candle_patterns - ローソク足パターン検出(1〜3本足)
3
+ *
4
+ * 設計思想:
5
+ * - 目的: 指定ペアの直近5日間のローソク足から短期反転パターンを検出
6
+ * - 対象:
7
+ * - 1本足: ハンマー、シューティングスター、十字線
8
+ * - 2本足: 包み線、はらみ線、毛抜き、かぶせ線、切り込み線
9
+ * - 3本足: 明けの明星、宵の明星、赤三兵、黒三兵
10
+ * - 用途: 初心者向けの自然言語解説 + 過去統計付与
11
+ *
12
+ * 既存ツールとの違い:
13
+ * - detect_patterns: 数週間〜数ヶ月スケールの大型チャートパターン
14
+ * - 本ツール: 1〜3本足の短期反転パターンに特化
15
+ *
16
+ * 🚨 CRITICAL: 配列順序の明示
17
+ * candles配列の順序は常に [最古, ..., 最新] です
18
+ * - index 0: 最古(5日前)
19
+ * - index n-1: 最新(今日、未確定の可能性)
20
+ */
21
+
22
+ import {
23
+ bodyBottom,
24
+ bodySize,
25
+ bodyTop,
26
+ isBearish,
27
+ isBullish,
28
+ lowerShadow,
29
+ totalRange,
30
+ upperShadow,
31
+ } from '../lib/candle-utils.js';
32
+ import { dayjs, nowIso, today, toIsoTime } from '../lib/datetime.js';
33
+ import { fail, failFromError, failFromValidation } from '../lib/result.js';
34
+ import { createMeta, ensurePair } from '../lib/validate.js';
35
+ import type {
36
+ CandlePatternType,
37
+ DetectedCandlePattern,
38
+ HistoryHorizonStats,
39
+ HistoryStats,
40
+ WindowCandle,
41
+ } from '../src/handlers/analyzeCandlePatternsHandler.js';
42
+ import { generateContent, generateSummary } from '../src/handlers/analyzeCandlePatternsHandler.js';
43
+ import type { Candle } from '../src/schemas.js';
44
+ import { AnalyzeCandlePatternsInputSchema, AnalyzeCandlePatternsOutputSchema } from '../src/schemas.js';
45
+ import type { ToolDefinition } from '../src/tool-definition.js';
46
+ import getCandles from './get_candles.js';
47
+
48
+ // ----- パターンコンテキスト -----
49
+ interface PatternContext {
50
+ rangeHigh: number;
51
+ rangeLow: number;
52
+ avgBodySize: number;
53
+ }
54
+
55
+ // ----- 統一パターン定義 -----
56
+ interface PatternConfig {
57
+ span: 1 | 2 | 3;
58
+ direction: 'bullish' | 'bearish' | 'neutral';
59
+ jp_name: string;
60
+ detect: (candles: Candle[], context?: PatternContext) => { detected: boolean; strength: number };
61
+ }
62
+
63
+ // ----- ヘルパー関数 -----
64
+
65
+ /**
66
+ * トレンド判定(直前n本の終値で判定)
67
+ * CRITICAL: candles配列は [最古, ..., 最新] の順序
68
+ */
69
+ function detectTrendBefore(candles: Candle[], endIndex: number, lookbackCount: number = 3): 'up' | 'down' | 'neutral' {
70
+ if (endIndex < lookbackCount - 1) return 'neutral';
71
+
72
+ let upCount = 0;
73
+ let downCount = 0;
74
+
75
+ for (let i = endIndex - lookbackCount + 1; i <= endIndex; i++) {
76
+ if (i > 0 && candles[i].close > candles[i - 1].close) {
77
+ upCount++;
78
+ } else if (i > 0 && candles[i].close < candles[i - 1].close) {
79
+ downCount++;
80
+ }
81
+ }
82
+
83
+ const threshold = Math.ceil(lookbackCount * 0.6);
84
+ if (upCount >= threshold) return 'up';
85
+ if (downCount >= threshold) return 'down';
86
+ return 'neutral';
87
+ }
88
+
89
+ /**
90
+ * ボラティリティレベルの判定
91
+ */
92
+ function detectVolatilityLevel(
93
+ candles: Candle[],
94
+ endIndex: number,
95
+ lookbackCount: number = 5,
96
+ ): 'low' | 'medium' | 'high' {
97
+ if (endIndex < lookbackCount) return 'medium';
98
+
99
+ const recentCandles = candles.slice(Math.max(0, endIndex - lookbackCount + 1), endIndex + 1);
100
+ const avgPrice = recentCandles.reduce((sum, c) => sum + c.close, 0) / recentCandles.length;
101
+ const avgRange = recentCandles.reduce((sum, c) => sum + (c.high - c.low), 0) / recentCandles.length;
102
+ const rangePct = (avgRange / avgPrice) * 100;
103
+
104
+ if (rangePct < 1.5) return 'low';
105
+ if (rangePct > 3.0) return 'high';
106
+ return 'medium';
107
+ }
108
+
109
+ // =====================================================================
110
+ // パターン検出関数
111
+ // =====================================================================
112
+
113
+ // ----- 1本足パターン (Phase 3) -----
114
+
115
+ /**
116
+ * ハンマー (hammer) の検出
117
+ * 条件: 長い下ヒゲ、小さい実体(上部)、短い上ヒゲ
118
+ */
119
+ function detectHammer(candles: Candle[], _context?: PatternContext): { detected: boolean; strength: number } {
120
+ const c = candles[0];
121
+ const range = totalRange(c);
122
+ if (range === 0) return { detected: false, strength: 0 };
123
+
124
+ const body = bodySize(c);
125
+ const lower = lowerShadow(c);
126
+ const upper = upperShadow(c);
127
+ const bodyRatio = body / range;
128
+
129
+ // 実体はレンジの5%〜35%(dojiと区別 & 大きすぎない)
130
+ if (bodyRatio < 0.05 || bodyRatio > 0.35) return { detected: false, strength: 0 };
131
+ // 下ヒゲが実体の2倍以上
132
+ if (lower < body * 2) return { detected: false, strength: 0 };
133
+ // 上ヒゲがレンジの25%以下
134
+ if (upper / range > 0.25) return { detected: false, strength: 0 };
135
+ // 下ヒゲがレンジの60%以上
136
+ if (lower / range < 0.6) return { detected: false, strength: 0 };
137
+
138
+ const strength = Math.min((lower / range - 0.4) / 0.6, 1.0);
139
+ return { detected: true, strength };
140
+ }
141
+
142
+ /**
143
+ * シューティングスター (shooting_star) の検出
144
+ * 条件(ハンマーの逆): 長い上ヒゲ、小さい実体(下部)、短い下ヒゲ
145
+ */
146
+ function detectShootingStar(candles: Candle[], _context?: PatternContext): { detected: boolean; strength: number } {
147
+ const c = candles[0];
148
+ const range = totalRange(c);
149
+ if (range === 0) return { detected: false, strength: 0 };
150
+
151
+ const body = bodySize(c);
152
+ const lower = lowerShadow(c);
153
+ const upper = upperShadow(c);
154
+ const bodyRatio = body / range;
155
+
156
+ if (bodyRatio < 0.05 || bodyRatio > 0.35) return { detected: false, strength: 0 };
157
+ if (upper < body * 2) return { detected: false, strength: 0 };
158
+ if (lower / range > 0.25) return { detected: false, strength: 0 };
159
+ if (upper / range < 0.6) return { detected: false, strength: 0 };
160
+
161
+ const strength = Math.min((upper / range - 0.4) / 0.6, 1.0);
162
+ return { detected: true, strength };
163
+ }
164
+
165
+ /**
166
+ * 十字線 (doji) の検出
167
+ * 条件: 実体がレンジの5%未満(始値≒終値)
168
+ * ヒゲの偏りで亜種を判別:
169
+ * - 上下均等 → 通常十字線, 下ヒゲ優勢 → トンボ型, 上ヒゲ優勢 → トウバ型
170
+ */
171
+ function detectDoji(candles: Candle[], _context?: PatternContext): { detected: boolean; strength: number } {
172
+ const c = candles[0];
173
+ const range = totalRange(c);
174
+ if (range === 0) return { detected: false, strength: 0 };
175
+
176
+ const body = bodySize(c);
177
+ if (body / range >= 0.05) return { detected: false, strength: 0 };
178
+
179
+ const upper = upperShadow(c);
180
+ const lower = lowerShadow(c);
181
+ const shadowImbalance = Math.abs(upper - lower) / range;
182
+ const strength = Math.min(0.5 + shadowImbalance * 0.5, 1.0);
183
+
184
+ return { detected: true, strength };
185
+ }
186
+
187
+ // ----- 2本足パターン (Phase 1-2) -----
188
+
189
+ /** 陽線包み線 (bullish_engulfing): 陰線 → それを完全に包む陽線 */
190
+ function detectBullishEngulfing(candles: Candle[]): { detected: boolean; strength: number } {
191
+ const [c1, c2] = candles;
192
+ if (!isBearish(c1) || !isBullish(c2)) return { detected: false, strength: 0 };
193
+ if (!(c2.open <= c1.close && c2.close >= c1.open)) return { detected: false, strength: 0 };
194
+
195
+ const body1 = bodySize(c1);
196
+ const body2 = bodySize(c2);
197
+ const strength = Math.min((body1 > 0 ? body2 / body1 : 1) / 2, 1.0);
198
+ return { detected: true, strength };
199
+ }
200
+
201
+ /** 陰線包み線 (bearish_engulfing): 陽線 → それを完全に包む陰線 */
202
+ function detectBearishEngulfing(candles: Candle[]): { detected: boolean; strength: number } {
203
+ const [c1, c2] = candles;
204
+ if (!isBullish(c1) || !isBearish(c2)) return { detected: false, strength: 0 };
205
+ if (!(c2.open >= c1.close && c2.close <= c1.open)) return { detected: false, strength: 0 };
206
+
207
+ const body1 = bodySize(c1);
208
+ const body2 = bodySize(c2);
209
+ const strength = Math.min((body1 > 0 ? body2 / body1 : 1) / 2, 1.0);
210
+ return { detected: true, strength };
211
+ }
212
+
213
+ /** 陽線はらみ線 (bullish_harami): 大陰線 → 小さいローソク足が内包 */
214
+ function detectBullishHarami(candles: Candle[]): { detected: boolean; strength: number } {
215
+ const [c1, c2] = candles;
216
+ if (!isBearish(c1)) return { detected: false, strength: 0 };
217
+ if (!(bodyTop(c2) <= bodyTop(c1) && bodyBottom(c2) >= bodyBottom(c1))) return { detected: false, strength: 0 };
218
+
219
+ const body1 = bodySize(c1);
220
+ const body2 = bodySize(c2);
221
+ if (body1 === 0 || body2 >= body1 * 0.7) return { detected: false, strength: 0 };
222
+
223
+ return { detected: true, strength: Math.min(1 - body2 / body1, 1.0) };
224
+ }
225
+
226
+ /** 陰線はらみ線 (bearish_harami): 大陽線 → 小さいローソク足が内包 */
227
+ function detectBearishHarami(candles: Candle[]): { detected: boolean; strength: number } {
228
+ const [c1, c2] = candles;
229
+ if (!isBullish(c1)) return { detected: false, strength: 0 };
230
+ if (!(bodyTop(c2) <= bodyTop(c1) && bodyBottom(c2) >= bodyBottom(c1))) return { detected: false, strength: 0 };
231
+
232
+ const body1 = bodySize(c1);
233
+ const body2 = bodySize(c2);
234
+ if (body1 === 0 || body2 >= body1 * 0.7) return { detected: false, strength: 0 };
235
+
236
+ return { detected: true, strength: Math.min(1 - body2 / body1, 1.0) };
237
+ }
238
+
239
+ /** 毛抜き天井 (tweezer_top): 2日連続で高値がほぼ同じ(±0.5%) */
240
+ function detectTweezerTop(candles: Candle[], context?: PatternContext): { detected: boolean; strength: number } {
241
+ const [c1, c2] = candles;
242
+ const avgHigh = (c1.high + c2.high) / 2;
243
+ const highDiff = Math.abs(c1.high - c2.high);
244
+ if (highDiff > avgHigh * 0.005) return { detected: false, strength: 0 };
245
+
246
+ if (context) {
247
+ const range = context.rangeHigh - context.rangeLow;
248
+ const threshold = context.rangeHigh - range * 0.2;
249
+ if (c1.high < threshold && c2.high < threshold) return { detected: false, strength: 0 };
250
+ }
251
+
252
+ const strength = Math.max(0, Math.min(1 - (highDiff / avgHigh) * 100, 1.0));
253
+ return { detected: true, strength };
254
+ }
255
+
256
+ /** 毛抜き底 (tweezer_bottom): 2日連続で安値がほぼ同じ(±0.5%) */
257
+ function detectTweezerBottom(candles: Candle[], context?: PatternContext): { detected: boolean; strength: number } {
258
+ const [c1, c2] = candles;
259
+ const avgLow = (c1.low + c2.low) / 2;
260
+ const lowDiff = Math.abs(c1.low - c2.low);
261
+ if (lowDiff > avgLow * 0.005) return { detected: false, strength: 0 };
262
+
263
+ if (context) {
264
+ const range = context.rangeHigh - context.rangeLow;
265
+ const threshold = context.rangeLow + range * 0.2;
266
+ if (c1.low > threshold && c2.low > threshold) return { detected: false, strength: 0 };
267
+ }
268
+
269
+ const strength = Math.max(0, Math.min(1 - (lowDiff / avgLow) * 100, 1.0));
270
+ return { detected: true, strength };
271
+ }
272
+
273
+ /** かぶせ線 (dark_cloud_cover) */
274
+ function detectDarkCloudCover(candles: Candle[], context?: PatternContext): { detected: boolean; strength: number } {
275
+ const [c1, c2] = candles;
276
+ if (!isBullish(c1) || !isBearish(c2)) return { detected: false, strength: 0 };
277
+
278
+ const body1 = bodySize(c1);
279
+ const avgBody = context?.avgBodySize || body1;
280
+ if (body1 < avgBody * 1.5) return { detected: false, strength: 0 };
281
+
282
+ const gapTol = body1 * 0.1;
283
+ if (c2.open < c1.close - gapTol) return { detected: false, strength: 0 };
284
+
285
+ const midPoint = (c1.open + c1.close) / 2;
286
+ if (c2.close >= midPoint) return { detected: false, strength: 0 };
287
+ if (c2.close <= c1.open) return { detected: false, strength: 0 };
288
+
289
+ return { detected: true, strength: Math.min((midPoint - c2.close) / body1, 1.0) };
290
+ }
291
+
292
+ /** 切り込み線 (piercing_line) */
293
+ function detectPiercingLine(candles: Candle[], context?: PatternContext): { detected: boolean; strength: number } {
294
+ const [c1, c2] = candles;
295
+ if (!isBearish(c1) || !isBullish(c2)) return { detected: false, strength: 0 };
296
+
297
+ const body1 = bodySize(c1);
298
+ const avgBody = context?.avgBodySize || body1;
299
+ if (body1 < avgBody * 1.5) return { detected: false, strength: 0 };
300
+
301
+ const gapTol = body1 * 0.1;
302
+ if (c2.open > c1.close + gapTol) return { detected: false, strength: 0 };
303
+
304
+ const midPoint = (c1.open + c1.close) / 2;
305
+ if (c2.close <= midPoint) return { detected: false, strength: 0 };
306
+ if (c2.close >= c1.open) return { detected: false, strength: 0 };
307
+
308
+ return { detected: true, strength: Math.min((c2.close - midPoint) / body1, 1.0) };
309
+ }
310
+
311
+ // ----- 3本足パターン (Phase 3) -----
312
+
313
+ /**
314
+ * 明けの明星 (morning_star)
315
+ * 大陰線→小さい実体→大陽線で1本目の中心値超え
316
+ */
317
+ function detectMorningStar(candles: Candle[], context?: PatternContext): { detected: boolean; strength: number } {
318
+ const [c1, c2, c3] = candles;
319
+ if (!isBearish(c1) || !isBullish(c3)) return { detected: false, strength: 0 };
320
+
321
+ const body1 = bodySize(c1);
322
+ const body2 = bodySize(c2);
323
+ const body3 = bodySize(c3);
324
+ const avgBody = context?.avgBodySize || body1;
325
+
326
+ if (body1 < avgBody * 0.8) return { detected: false, strength: 0 };
327
+ if (body2 > body1 * 0.4) return { detected: false, strength: 0 };
328
+ if (body3 < avgBody * 0.8) return { detected: false, strength: 0 };
329
+
330
+ const midPointC1 = (c1.open + c1.close) / 2;
331
+ if (c3.close < midPointC1) return { detected: false, strength: 0 };
332
+
333
+ // BTC24h緩和: 2本目の実体下端が1本目の実体下端以下
334
+ if (bodyBottom(c2) > bodyBottom(c1)) return { detected: false, strength: 0 };
335
+
336
+ const recovery = c3.close - midPointC1;
337
+ return { detected: true, strength: Math.min(recovery / body1 + 0.3, 1.0) };
338
+ }
339
+
340
+ /**
341
+ * 宵の明星 (evening_star)
342
+ * 大陽線→小さい実体→大陰線で1本目の中心値割れ
343
+ */
344
+ function detectEveningStar(candles: Candle[], context?: PatternContext): { detected: boolean; strength: number } {
345
+ const [c1, c2, c3] = candles;
346
+ if (!isBullish(c1) || !isBearish(c3)) return { detected: false, strength: 0 };
347
+
348
+ const body1 = bodySize(c1);
349
+ const body2 = bodySize(c2);
350
+ const body3 = bodySize(c3);
351
+ const avgBody = context?.avgBodySize || body1;
352
+
353
+ if (body1 < avgBody * 0.8) return { detected: false, strength: 0 };
354
+ if (body2 > body1 * 0.4) return { detected: false, strength: 0 };
355
+ if (body3 < avgBody * 0.8) return { detected: false, strength: 0 };
356
+
357
+ const midPointC1 = (c1.open + c1.close) / 2;
358
+ if (c3.close > midPointC1) return { detected: false, strength: 0 };
359
+
360
+ // BTC24h緩和: 2本目の実体上端が1本目の実体上端以上
361
+ if (bodyTop(c2) < bodyTop(c1)) return { detected: false, strength: 0 };
362
+
363
+ const decline = midPointC1 - c3.close;
364
+ return { detected: true, strength: Math.min(decline / body1 + 0.3, 1.0) };
365
+ }
366
+
367
+ /**
368
+ * 赤三兵 (three_white_soldiers)
369
+ * 3本連続陽線、各終値が前を上回る、始値は前の実体内
370
+ */
371
+ function detectThreeWhiteSoldiers(
372
+ candles: Candle[],
373
+ context?: PatternContext,
374
+ ): { detected: boolean; strength: number } {
375
+ const [c1, c2, c3] = candles;
376
+ if (!isBullish(c1) || !isBullish(c2) || !isBullish(c3)) return { detected: false, strength: 0 };
377
+ if (c2.close <= c1.close || c3.close <= c2.close) return { detected: false, strength: 0 };
378
+
379
+ const body1 = bodySize(c1);
380
+ const body2 = bodySize(c2);
381
+ const body3 = bodySize(c3);
382
+ const avgBody = context?.avgBodySize || (body1 + body2 + body3) / 3;
383
+
384
+ if (body1 < avgBody * 0.5 || body2 < avgBody * 0.5 || body3 < avgBody * 0.5) return { detected: false, strength: 0 };
385
+
386
+ // 各始値が前の実体内またはその近辺
387
+ const tol2 = body1 * 0.5;
388
+ const tol3 = body2 * 0.5;
389
+ if (c2.open < bodyBottom(c1) - tol2 || c2.open > bodyTop(c1) + tol2) return { detected: false, strength: 0 };
390
+ if (c3.open < bodyBottom(c2) - tol3 || c3.open > bodyTop(c2) + tol3) return { detected: false, strength: 0 };
391
+
392
+ // 上ヒゲが短い
393
+ for (const c of [c1, c2, c3]) {
394
+ const r = totalRange(c);
395
+ if (r > 0 && upperShadow(c) / r > 0.4) return { detected: false, strength: 0 };
396
+ }
397
+
398
+ const maxBody = Math.max(body1, body2, body3);
399
+ const minBody = Math.min(body1, body2, body3);
400
+ return { detected: true, strength: Math.min(minBody / maxBody + 0.2, 1.0) };
401
+ }
402
+
403
+ /**
404
+ * 黒三兵 (three_black_crows)
405
+ * 3本連続陰線、各終値が前を下回る
406
+ */
407
+ function detectThreeBlackCrows(candles: Candle[], context?: PatternContext): { detected: boolean; strength: number } {
408
+ const [c1, c2, c3] = candles;
409
+ if (!isBearish(c1) || !isBearish(c2) || !isBearish(c3)) return { detected: false, strength: 0 };
410
+ if (c2.close >= c1.close || c3.close >= c2.close) return { detected: false, strength: 0 };
411
+
412
+ const body1 = bodySize(c1);
413
+ const body2 = bodySize(c2);
414
+ const body3 = bodySize(c3);
415
+ const avgBody = context?.avgBodySize || (body1 + body2 + body3) / 3;
416
+
417
+ if (body1 < avgBody * 0.5 || body2 < avgBody * 0.5 || body3 < avgBody * 0.5) return { detected: false, strength: 0 };
418
+
419
+ const tol2 = body1 * 0.5;
420
+ const tol3 = body2 * 0.5;
421
+ if (c2.open < bodyBottom(c1) - tol2 || c2.open > bodyTop(c1) + tol2) return { detected: false, strength: 0 };
422
+ if (c3.open < bodyBottom(c2) - tol3 || c3.open > bodyTop(c2) + tol3) return { detected: false, strength: 0 };
423
+
424
+ // 下ヒゲが短い
425
+ for (const c of [c1, c2, c3]) {
426
+ const r = totalRange(c);
427
+ if (r > 0 && lowerShadow(c) / r > 0.4) return { detected: false, strength: 0 };
428
+ }
429
+
430
+ const maxBody = Math.max(body1, body2, body3);
431
+ const minBody = Math.min(body1, body2, body3);
432
+ return { detected: true, strength: Math.min(minBody / maxBody + 0.2, 1.0) };
433
+ }
434
+
435
+ // =====================================================================
436
+ // パターン定義レジストリ
437
+ // =====================================================================
438
+
439
+ const PATTERN_CONFIGS: Record<CandlePatternType, PatternConfig> = {
440
+ // 1本足
441
+ hammer: { span: 1, direction: 'bullish', jp_name: 'ハンマー(カラカサ)', detect: detectHammer },
442
+ shooting_star: {
443
+ span: 1,
444
+ direction: 'bearish',
445
+ jp_name: 'シューティングスター(流れ星)',
446
+ detect: detectShootingStar,
447
+ },
448
+ doji: { span: 1, direction: 'neutral', jp_name: '十字線(Doji)', detect: detectDoji },
449
+ // 2本足
450
+ bullish_engulfing: { span: 2, direction: 'bullish', jp_name: '陽線包み線', detect: detectBullishEngulfing },
451
+ bearish_engulfing: { span: 2, direction: 'bearish', jp_name: '陰線包み線', detect: detectBearishEngulfing },
452
+ bullish_harami: { span: 2, direction: 'bullish', jp_name: '陽線はらみ線', detect: detectBullishHarami },
453
+ bearish_harami: { span: 2, direction: 'bearish', jp_name: '陰線はらみ線', detect: detectBearishHarami },
454
+ tweezer_top: { span: 2, direction: 'bearish', jp_name: '毛抜き天井', detect: detectTweezerTop },
455
+ tweezer_bottom: { span: 2, direction: 'bullish', jp_name: '毛抜き底', detect: detectTweezerBottom },
456
+ dark_cloud_cover: { span: 2, direction: 'bearish', jp_name: 'かぶせ線', detect: detectDarkCloudCover },
457
+ piercing_line: { span: 2, direction: 'bullish', jp_name: '切り込み線', detect: detectPiercingLine },
458
+ // 3本足
459
+ morning_star: { span: 3, direction: 'bullish', jp_name: '明けの明星', detect: detectMorningStar },
460
+ evening_star: { span: 3, direction: 'bearish', jp_name: '宵の明星', detect: detectEveningStar },
461
+ three_white_soldiers: { span: 3, direction: 'bullish', jp_name: '赤三兵', detect: detectThreeWhiteSoldiers },
462
+ three_black_crows: { span: 3, direction: 'bearish', jp_name: '黒三兵', detect: detectThreeBlackCrows },
463
+ };
464
+
465
+ // ----- 過去統計計算 -----
466
+ interface PatternOccurrence {
467
+ index: number;
468
+ pattern: CandlePatternType;
469
+ basePrice: number;
470
+ }
471
+
472
+ /**
473
+ * 過去のパターン出現を検索(span 対応)
474
+ */
475
+ function findHistoricalPatterns(
476
+ candles: Candle[],
477
+ pattern: CandlePatternType,
478
+ excludeLastN: number = 1,
479
+ ): PatternOccurrence[] {
480
+ const config = PATTERN_CONFIGS[pattern];
481
+ const occurrences: PatternOccurrence[] = [];
482
+
483
+ const endIndex = candles.length - 1 - excludeLastN;
484
+
485
+ for (let i = config.span - 1; i <= endIndex; i++) {
486
+ const slice = candles.slice(i - config.span + 1, i + 1);
487
+ const result = config.detect(slice);
488
+ if (result.detected) {
489
+ occurrences.push({
490
+ index: i,
491
+ pattern,
492
+ basePrice: candles[i].close,
493
+ });
494
+ }
495
+ }
496
+
497
+ return occurrences;
498
+ }
499
+
500
+ /**
501
+ * 過去統計を計算
502
+ */
503
+ function calculateHistoryStats(
504
+ candles: Candle[],
505
+ pattern: CandlePatternType,
506
+ horizons: number[],
507
+ lookbackDays: number,
508
+ ): HistoryStats | null {
509
+ // lookbackDays分のデータがあるか確認
510
+ if (candles.length < lookbackDays) {
511
+ return null;
512
+ }
513
+
514
+ // lookbackDays期間内のパターンを検索
515
+ const startIndex = candles.length - lookbackDays;
516
+ const relevantCandles = candles.slice(startIndex);
517
+
518
+ const occurrences = findHistoricalPatterns(relevantCandles, pattern, 5);
519
+
520
+ if (occurrences.length < 5) {
521
+ // サンプル数が少なすぎる場合はnull
522
+ return null;
523
+ }
524
+
525
+ const horizonStats: Record<string, HistoryHorizonStats> = {};
526
+
527
+ for (const h of horizons) {
528
+ const returns: number[] = [];
529
+
530
+ for (const occ of occurrences) {
531
+ // グローバルインデックスに変換
532
+ const globalIndex = startIndex + occ.index;
533
+
534
+ // h本後のデータが存在するか確認
535
+ if (globalIndex + h < candles.length) {
536
+ const futureCandle = candles[globalIndex + h];
537
+ const returnPct = ((futureCandle.close - occ.basePrice) / occ.basePrice) * 100;
538
+ returns.push(returnPct);
539
+ }
540
+ }
541
+
542
+ if (returns.length > 0) {
543
+ const avgReturn = returns.reduce((a, b) => a + b, 0) / returns.length;
544
+ const winCount = returns.filter((r) => r > 0).length;
545
+ const winRate = winCount / returns.length;
546
+
547
+ horizonStats[String(h)] = {
548
+ avg_return: Number(avgReturn.toFixed(2)),
549
+ win_rate: Number(winRate.toFixed(2)),
550
+ sample: returns.length,
551
+ };
552
+ }
553
+ }
554
+
555
+ return {
556
+ lookback_days: lookbackDays,
557
+ occurrences: occurrences.length,
558
+ horizons: horizonStats,
559
+ };
560
+ }
561
+
562
+ // ----- ヘルパー: 日付形式の正規化 -----
563
+ /**
564
+ * ISO形式 ("2025-11-05") または YYYYMMDD ("20251105") を YYYYMMDD に正規化
565
+ */
566
+ function normalizeDateToYYYYMMDD(dateStr: string | undefined): string | undefined {
567
+ if (!dateStr) return undefined;
568
+
569
+ // ISO形式 ("2025-11-05" or "2025-11-05T...") の場合
570
+ if (dateStr.includes('-')) {
571
+ const match = dateStr.match(/^(\d{4})-(\d{2})-(\d{2})/);
572
+ if (match) {
573
+ return `${match[1]}${match[2]}${match[3]}`;
574
+ }
575
+ }
576
+
577
+ // 既にYYYYMMDD形式の場合
578
+ if (/^\d{8}$/.test(dateStr)) {
579
+ return dateStr;
580
+ }
581
+
582
+ return undefined;
583
+ }
584
+
585
+ // ----- メイン関数 -----
586
+ export default async function analyzeCandlePatterns(
587
+ opts: {
588
+ pair?: string;
589
+ timeframe?: '1day';
590
+ as_of?: string; // ISO "2025-11-05" or YYYYMMDD "20251105"
591
+ date?: string; // DEPRECATED: YYYYMMDD format (for backward compatibility)
592
+ window_days?: number;
593
+ focus_last_n?: number;
594
+ patterns?: CandlePatternType[];
595
+ history_lookback_days?: number;
596
+ history_horizons?: number[];
597
+ allow_partial_patterns?: boolean;
598
+ } = {},
599
+ ) {
600
+ try {
601
+ // 入力の正規化
602
+ const input = AnalyzeCandlePatternsInputSchema.parse(opts);
603
+ const chk = ensurePair(input.pair);
604
+ if (!chk.ok) return failFromValidation(chk);
605
+ const pair = chk.pair;
606
+ const timeframe = input.timeframe;
607
+
608
+ // as_of を優先、なければ date を使用(互換性のため)
609
+ // as_of: ISO形式 "2025-11-05" または YYYYMMDD "20251105" を受け付け
610
+ const rawDate = input.as_of || input.date;
611
+ const targetDate = normalizeDateToYYYYMMDD(rawDate);
612
+ const windowDays = input.window_days;
613
+ const focusLastN = input.focus_last_n;
614
+ const targetPatterns = input.patterns || (Object.keys(PATTERN_CONFIGS) as CandlePatternType[]);
615
+ const historyLookbackDays = input.history_lookback_days;
616
+ const historyHorizons = input.history_horizons;
617
+ const allowPartial = input.allow_partial_patterns;
618
+
619
+ // 日付指定があるかどうか
620
+ const isHistoricalQuery = !!targetDate;
621
+
622
+ // ローソク足データを取得(統計計算用に多めに取得)
623
+ const requiredCandles = Math.max(windowDays, historyLookbackDays + 10);
624
+ const candlesResult = await getCandles(pair, '1day', targetDate, requiredCandles);
625
+
626
+ if (!candlesResult.ok) {
627
+ return AnalyzeCandlePatternsOutputSchema.parse(fail(candlesResult.summary, 'internal'));
628
+ }
629
+
630
+ // 全データを保持(統計計算用)
631
+ const allCandlesForStats = candlesResult.data.normalized;
632
+ let allCandles = [...allCandlesForStats];
633
+
634
+ // 🚨 CRITICAL: 日付指定時は、その日付以前のデータのみにフィルタリング
635
+ // get_candles は年単位でデータを取得するため、指定日以降のデータも含まれる
636
+ if (isHistoricalQuery && targetDate) {
637
+ // targetDate は YYYYMMDD 形式(例: "20251105")
638
+ const year = targetDate.slice(0, 4);
639
+ const month = targetDate.slice(4, 6);
640
+ const day = targetDate.slice(6, 8);
641
+ const targetDateMs = dayjs.utc(`${year}-${month}-${day}`).endOf('day').valueOf();
642
+
643
+ allCandles = allCandles.filter((c) => {
644
+ if (!c.isoTime) return false;
645
+ return dayjs(c.isoTime).valueOf() <= targetDateMs;
646
+ });
647
+ }
648
+
649
+ if (allCandles.length < windowDays) {
650
+ return AnalyzeCandlePatternsOutputSchema.parse(
651
+ fail(`ローソク足データが不足しています(${allCandles.length}本 < ${windowDays}本)`, 'user'),
652
+ );
653
+ }
654
+
655
+ // 直近windowDays分を切り出し
656
+ // CRITICAL: allCandlesは [最古, ..., 最新] の順序
657
+ const windowStart = allCandles.length - windowDays;
658
+ const windowCandles = allCandles.slice(windowStart);
659
+
660
+ // 日足確定判定:
661
+ // - 過去日付指定時: すべて確定済み(is_partial = false)
662
+ // - 最新データ時: 最新の日足が今日のデータなら未確定
663
+ const todayStr = today('YYYY-MM-DD');
664
+ const lastCandleTime = windowCandles[windowCandles.length - 1]?.isoTime?.split('T')[0];
665
+ const isLastPartial = !isHistoricalQuery && lastCandleTime === todayStr;
666
+
667
+ // WindowCandle形式に変換
668
+ const formattedWindowCandles: WindowCandle[] = windowCandles.map((c, idx) => ({
669
+ timestamp: c.isoTime || toIsoTime(c.time || 0) || '',
670
+ open: c.open,
671
+ high: c.high,
672
+ low: c.low,
673
+ close: c.close,
674
+ volume: c.volume || 0,
675
+ is_partial: idx === windowCandles.length - 1 && isLastPartial,
676
+ }));
677
+
678
+ // パターン検出
679
+ // CRITICAL: windowCandles配列は [最古, ..., 最新] の順序
680
+ const detectedPatterns: DetectedCandlePattern[] = [];
681
+ // startCheckIndex: 1本足はindex 0から、spanチェックでガード
682
+ const startCheckIndex = Math.max(0, windowCandles.length - focusLastN);
683
+
684
+ // パターンコンテキストを計算
685
+ const highs = windowCandles.map((c) => c.high);
686
+ const lows = windowCandles.map((c) => c.low);
687
+ const bodies = windowCandles.map((c) => Math.abs(c.close - c.open));
688
+ const patternContext: PatternContext = {
689
+ rangeHigh: Math.max(...highs),
690
+ rangeLow: Math.min(...lows),
691
+ avgBodySize: bodies.reduce((sum, b) => sum + b, 0) / bodies.length,
692
+ };
693
+
694
+ for (let i = startCheckIndex; i < windowCandles.length; i++) {
695
+ const usesPartial = i === windowCandles.length - 1 && isLastPartial;
696
+
697
+ if (usesPartial && !allowPartial) {
698
+ continue;
699
+ }
700
+
701
+ for (const patternType of targetPatterns) {
702
+ const config = PATTERN_CONFIGS[patternType];
703
+
704
+ // spanに必要な本数があるかチェック
705
+ if (i < config.span - 1) continue;
706
+
707
+ const slice = windowCandles.slice(i - config.span + 1, i + 1);
708
+ const result = config.detect(slice, patternContext);
709
+
710
+ if (result.detected) {
711
+ // トレンドはパターン開始位置より前で判定
712
+ const patternStartIdx = i - config.span + 1;
713
+ const trendBefore = detectTrendBefore(windowCandles, patternStartIdx > 0 ? patternStartIdx - 1 : 0, 3);
714
+ const volatilityLevel = detectVolatilityLevel(windowCandles, i, 5);
715
+
716
+ // doji は直前トレンドで方向を動的決定
717
+ let direction = config.direction;
718
+ if (direction === 'neutral' && patternType === 'doji') {
719
+ if (trendBefore === 'up') direction = 'bearish';
720
+ else if (trendBefore === 'down') direction = 'bullish';
721
+ }
722
+
723
+ const historyStats = calculateHistoryStats(allCandles, patternType, historyHorizons, historyLookbackDays);
724
+
725
+ detectedPatterns.push({
726
+ pattern: patternType,
727
+ pattern_jp: config.jp_name,
728
+ direction,
729
+ strength: Number(result.strength.toFixed(2)),
730
+ candle_range_index: [i - config.span + 1, i] as [number, number],
731
+ uses_partial_candle: usesPartial,
732
+ status: usesPartial ? 'forming' : 'confirmed',
733
+ local_context: {
734
+ trend_before: trendBefore,
735
+ volatility_level: volatilityLevel,
736
+ },
737
+ history_stats: historyStats,
738
+ });
739
+ }
740
+ }
741
+ }
742
+
743
+ // 強度フィルタ: 50%未満のパターンを除外(初心者向けにノイズを減らす)
744
+ const MIN_STRENGTH_THRESHOLD = 0.5; // 50%
745
+ const filteredPatterns = detectedPatterns.filter((p) => p.strength >= MIN_STRENGTH_THRESHOLD);
746
+
747
+ // サマリーとコンテント生成(フィルタ後のパターンを使用)
748
+ const summary = generateSummary(filteredPatterns, formattedWindowCandles);
749
+ const content = generateContent(filteredPatterns, formattedWindowCandles);
750
+
751
+ const data = {
752
+ pair,
753
+ timeframe,
754
+ snapshot_time: nowIso(),
755
+ window: {
756
+ from: formattedWindowCandles[0]?.timestamp?.split('T')[0] || '',
757
+ to: formattedWindowCandles[formattedWindowCandles.length - 1]?.timestamp?.split('T')[0] || '',
758
+ candles: formattedWindowCandles,
759
+ },
760
+ recent_patterns: filteredPatterns, // 強度50%以上のパターンのみ
761
+ summary,
762
+ };
763
+
764
+ const meta = {
765
+ ...createMeta(pair, {}),
766
+ timeframe,
767
+ as_of: rawDate || null, // original input value
768
+ date: targetDate || null, // YYYYMMDD normalized or null (latest)
769
+ window_days: windowDays,
770
+ patterns_checked: targetPatterns,
771
+ history_lookback_days: historyLookbackDays,
772
+ history_horizons: historyHorizons,
773
+ };
774
+
775
+ const result = {
776
+ ok: true as const,
777
+ summary,
778
+ content,
779
+ data,
780
+ meta,
781
+ };
782
+
783
+ return AnalyzeCandlePatternsOutputSchema.parse(result);
784
+ } catch (e: unknown) {
785
+ return failFromError(e, {
786
+ schema: AnalyzeCandlePatternsOutputSchema,
787
+ defaultMessage: 'ローソク足パターン分析中にエラーが発生しました',
788
+ });
789
+ }
790
+ }
791
+
792
+ // ── MCP ツール定義(tool-registry から自動収集) ──
793
+ export const toolDef: ToolDefinition = {
794
+ name: 'analyze_candle_patterns',
795
+ description:
796
+ '[Candlestick Patterns / Doji / Engulfing] ローソク足パターン検出(candle patterns / doji / engulfing / hammer / harami)。1〜3本足パターンを検出し文脈と過去統計を付けて解説。',
797
+ inputSchema: AnalyzeCandlePatternsInputSchema,
798
+ handler: async (args: {
799
+ pair?: string;
800
+ timeframe?: '1day';
801
+ as_of?: string;
802
+ date?: string;
803
+ window_days?: number;
804
+ focus_last_n?: number;
805
+ patterns?: CandlePatternType[];
806
+ history_lookback_days?: number;
807
+ history_horizons?: number[];
808
+ allow_partial_patterns?: boolean;
809
+ }) => analyzeCandlePatterns(args),
810
+ };