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,530 @@
1
+ import { formatPair, formatPercent, formatPrice, timeframeLabel } from '../lib/formatter.js';
2
+ import { fail, failFromError, failFromValidation } from '../lib/result.js';
3
+ import { createMeta, ensurePair } from '../lib/validate.js';
4
+ import type { Pair } from '../src/schemas.js';
5
+ import { AnalyzeFibonacciInputSchema, AnalyzeFibonacciOutputSchema } from '../src/schemas.js';
6
+ import type { ToolDefinition } from '../src/tool-definition.js';
7
+ import getCandles from './get_candles.js';
8
+
9
+ // ── Constants ──
10
+
11
+ /** Standard Fibonacci retracement ratios */
12
+ const RETRACEMENT_RATIOS = [0, 0.236, 0.382, 0.5, 0.618, 0.786, 1.0];
13
+
14
+ /** Standard Fibonacci extension ratios */
15
+ const EXTENSION_RATIOS = [1.272, Math.SQRT2, 1.618, 2.0, 2.618];
16
+
17
+ // ── Types ──
18
+
19
+ interface NormalizedCandle {
20
+ open: number;
21
+ high: number;
22
+ low: number;
23
+ close: number;
24
+ volume?: number;
25
+ isoTime?: string | null;
26
+ }
27
+
28
+ interface SwingPoint {
29
+ price: number;
30
+ date: string;
31
+ index: number;
32
+ }
33
+
34
+ interface FibLevel {
35
+ ratio: number;
36
+ price: number;
37
+ distancePct: number;
38
+ isNearest: boolean;
39
+ }
40
+
41
+ interface LevelStat {
42
+ ratio: number;
43
+ samplesCount: number;
44
+ bounceRate: number;
45
+ avgBounceReturnPct: number;
46
+ avgBreakthroughReturnPct: number;
47
+ medianDwellBars: number;
48
+ confidence: 'high' | 'medium' | 'low';
49
+ }
50
+
51
+ // ── Swing Detection ──
52
+
53
+ /**
54
+ * Detect the most significant swing high and swing low within the given candle range.
55
+ * Uses a simple approach: find the highest high and lowest low, then determine trend
56
+ * based on which came first.
57
+ */
58
+ function detectSignificantSwings(candles: NormalizedCandle[]): {
59
+ swingHigh: SwingPoint;
60
+ swingLow: SwingPoint;
61
+ trend: 'up' | 'down';
62
+ } {
63
+ let highestIdx = 0;
64
+ let lowestIdx = 0;
65
+
66
+ for (let i = 1; i < candles.length; i++) {
67
+ if (candles[i].high > candles[highestIdx].high) highestIdx = i;
68
+ if (candles[i].low < candles[lowestIdx].low) lowestIdx = i;
69
+ }
70
+
71
+ const swingHigh: SwingPoint = {
72
+ price: candles[highestIdx].high,
73
+ date: candles[highestIdx].isoTime?.split('T')[0] ?? '',
74
+ index: highestIdx,
75
+ };
76
+
77
+ const swingLow: SwingPoint = {
78
+ price: candles[lowestIdx].low,
79
+ date: candles[lowestIdx].isoTime?.split('T')[0] ?? '',
80
+ index: lowestIdx,
81
+ };
82
+
83
+ // Trend is determined by which swing came last:
84
+ // If the low came after the high → downtrend (price fell from high to low)
85
+ // If the high came after the low → uptrend (price rose from low to high)
86
+ const trend: 'up' | 'down' = lowestIdx > highestIdx ? 'down' : 'up';
87
+
88
+ return { swingHigh, swingLow, trend };
89
+ }
90
+
91
+ // ── Fibonacci Level Calculation ──
92
+
93
+ function calculateLevels(
94
+ swingHigh: SwingPoint,
95
+ swingLow: SwingPoint,
96
+ trend: 'up' | 'down',
97
+ currentPrice: number,
98
+ ratios: number[],
99
+ ): FibLevel[] {
100
+ const range = swingHigh.price - swingLow.price;
101
+
102
+ return ratios.map((ratio) => {
103
+ // In downtrend: retracement goes up from low
104
+ // In uptrend: retracement goes down from high
105
+ const price = trend === 'down' ? swingLow.price + range * ratio : swingHigh.price - range * ratio;
106
+
107
+ const distancePct = ((price - currentPrice) / currentPrice) * 100;
108
+
109
+ return { ratio, price: Math.round(price), distancePct: Number(distancePct.toFixed(2)), isNearest: false };
110
+ });
111
+ }
112
+
113
+ function calculateExtensions(
114
+ swingHigh: SwingPoint,
115
+ swingLow: SwingPoint,
116
+ trend: 'up' | 'down',
117
+ currentPrice: number,
118
+ ratios: number[],
119
+ ): FibLevel[] {
120
+ const range = swingHigh.price - swingLow.price;
121
+
122
+ return ratios.map((ratio) => {
123
+ // Extensions project beyond the swing points
124
+ // Uptrend: project above swingHigh → swingLow + range * ratio
125
+ // Downtrend: project below swingLow → swingHigh - range * ratio
126
+ const price = trend === 'up' ? swingLow.price + range * ratio : swingHigh.price - range * ratio;
127
+
128
+ const distancePct = ((price - currentPrice) / currentPrice) * 100;
129
+
130
+ return { ratio, price: Math.round(price), distancePct: Number(distancePct.toFixed(2)), isNearest: false };
131
+ });
132
+ }
133
+
134
+ function markNearest(levels: FibLevel[], currentPrice: number): FibLevel[] {
135
+ if (levels.length === 0) return levels;
136
+
137
+ let minDist = Infinity;
138
+ let nearestIdx = 0;
139
+
140
+ for (let i = 0; i < levels.length; i++) {
141
+ const dist = Math.abs(levels[i].price - currentPrice);
142
+ if (dist < minDist) {
143
+ minDist = dist;
144
+ nearestIdx = i;
145
+ }
146
+ }
147
+
148
+ return levels.map((l, i) => ({ ...l, isNearest: i === nearestIdx }));
149
+ }
150
+
151
+ function findPosition(
152
+ levels: FibLevel[],
153
+ currentPrice: number,
154
+ ): { aboveLevel: FibLevel | null; belowLevel: FibLevel | null; nearestLevel: FibLevel | null } {
155
+ const sorted = [...levels].sort((a, b) => a.price - b.price);
156
+
157
+ let aboveLevel: FibLevel | null = null;
158
+ let belowLevel: FibLevel | null = null;
159
+ const nearestLevel = levels.find((l) => l.isNearest) ?? null;
160
+
161
+ for (const level of sorted) {
162
+ if (level.price <= currentPrice) belowLevel = level;
163
+ if (level.price > currentPrice && !aboveLevel) aboveLevel = level;
164
+ }
165
+
166
+ return { aboveLevel, belowLevel, nearestLevel };
167
+ }
168
+
169
+ // ── Historical Reaction Statistics (Feature #3) ──
170
+
171
+ /**
172
+ * Analyze how price has historically reacted at each Fibonacci level.
173
+ * Uses past candle data to count bounces vs breakthroughs at each level zone.
174
+ */
175
+ function calculateLevelStats(candles: NormalizedCandle[], levels: FibLevel[], tolerancePct: number = 0.5): LevelStat[] {
176
+ return levels.map((level) => {
177
+ const zone = level.price * (tolerancePct / 100);
178
+ const zoneMin = level.price - zone;
179
+ const zoneMax = level.price + zone;
180
+
181
+ let samplesCount = 0;
182
+ let bounceCount = 0;
183
+ const bounceReturns: number[] = [];
184
+ const breakthroughReturns: number[] = [];
185
+ const dwellBars: number[] = [];
186
+
187
+ for (let i = 0; i < candles.length; i++) {
188
+ const candle = candles[i];
189
+
190
+ // Check if price touched the zone
191
+ const touchedZone = candle.low <= zoneMax && candle.high >= zoneMin;
192
+ if (!touchedZone) continue;
193
+
194
+ samplesCount++;
195
+
196
+ // Count dwell bars (how many consecutive bars stayed in zone)
197
+ let dwell = 1;
198
+ for (let j = i + 1; j < candles.length; j++) {
199
+ if (candles[j].low <= zoneMax && candles[j].high >= zoneMin) {
200
+ dwell++;
201
+ } else {
202
+ break;
203
+ }
204
+ }
205
+ dwellBars.push(dwell);
206
+
207
+ // Check what happened after touching the zone (look ahead 5 bars)
208
+ const lookAhead = Math.min(i + dwell + 5, candles.length - 1);
209
+ if (lookAhead <= i + dwell) continue;
210
+
211
+ const afterCandle = candles[lookAhead];
212
+ const returnPct = ((afterCandle.close - candle.close) / candle.close) * 100;
213
+
214
+ // Determine if it bounced (reversed) or broke through
215
+ const priceWasAbove = candle.close > level.price;
216
+ const priceStayedAbove = afterCandle.close > level.price;
217
+
218
+ if (priceWasAbove === priceStayedAbove) {
219
+ // Bounced back to same side
220
+ bounceCount++;
221
+ bounceReturns.push(Math.abs(returnPct));
222
+ } else {
223
+ // Broke through
224
+ breakthroughReturns.push(returnPct);
225
+ }
226
+
227
+ // Skip the dwell period to avoid double-counting
228
+ i += dwell - 1;
229
+ }
230
+
231
+ const bounceRate = samplesCount > 0 ? bounceCount / samplesCount : 0;
232
+ const avgBounceReturnPct =
233
+ bounceReturns.length > 0 ? bounceReturns.reduce((a, b) => a + b, 0) / bounceReturns.length : 0;
234
+ const avgBreakthroughReturnPct =
235
+ breakthroughReturns.length > 0 ? breakthroughReturns.reduce((a, b) => a + b, 0) / breakthroughReturns.length : 0;
236
+
237
+ // Median dwell bars
238
+ const sortedDwell = [...dwellBars].sort((a, b) => a - b);
239
+ const medianDwellBars = sortedDwell.length > 0 ? sortedDwell[Math.floor(sortedDwell.length / 2)] : 0;
240
+
241
+ const confidence: 'high' | 'medium' | 'low' = samplesCount >= 8 ? 'high' : samplesCount >= 4 ? 'medium' : 'low';
242
+
243
+ return {
244
+ ratio: level.ratio,
245
+ samplesCount,
246
+ bounceRate: Number(bounceRate.toFixed(3)),
247
+ avgBounceReturnPct: Number(avgBounceReturnPct.toFixed(2)),
248
+ avgBreakthroughReturnPct: Number(avgBreakthroughReturnPct.toFixed(2)),
249
+ medianDwellBars,
250
+ confidence,
251
+ };
252
+ });
253
+ }
254
+
255
+ // ── Content Generation ──
256
+
257
+ function generateContent(
258
+ pair: string,
259
+ timeframe: string,
260
+ currentPrice: number,
261
+ trend: 'up' | 'down',
262
+ swingHigh: SwingPoint,
263
+ swingLow: SwingPoint,
264
+ range: number,
265
+ levels: FibLevel[],
266
+ extensions: FibLevel[],
267
+ position: { aboveLevel: FibLevel | null; belowLevel: FibLevel | null; nearestLevel: FibLevel | null },
268
+ levelStats: LevelStat[],
269
+ mode: string,
270
+ lookbackDays: number,
271
+ ): Array<{ type: 'text'; text: string }> {
272
+ const lines: string[] = [];
273
+ const pairLabel = formatPair(pair);
274
+ const tfLabel = timeframeLabel(timeframe);
275
+
276
+ lines.push(`【フィボナッチ分析】${pairLabel} ${tfLabel}(過去${lookbackDays}日)`);
277
+ lines.push(`現在価格: ${formatPrice(currentPrice, pair)}`);
278
+ lines.push(`トレンド: ${trend === 'up' ? '上昇↑' : '下降↓'}`);
279
+ lines.push('');
280
+
281
+ lines.push(`スイングハイ: ${formatPrice(swingHigh.price, pair)}(${swingHigh.date})`);
282
+ lines.push(`スイングロー: ${formatPrice(swingLow.price, pair)}(${swingLow.date})`);
283
+ lines.push(`レンジ幅: ${formatPrice(range, pair)}`);
284
+ lines.push('');
285
+
286
+ // Current position
287
+ if (position.nearestLevel) {
288
+ lines.push(
289
+ `現在位置: ${(position.nearestLevel.ratio * 100).toFixed(1)}% 水準付近(距離 ${formatPercent(position.nearestLevel.distancePct, { sign: true })})`,
290
+ );
291
+ if (position.belowLevel && position.aboveLevel) {
292
+ lines.push(
293
+ ` 下: ${(position.belowLevel.ratio * 100).toFixed(1)}% = ${formatPrice(position.belowLevel.price, pair)}`,
294
+ );
295
+ lines.push(
296
+ ` 上: ${(position.aboveLevel.ratio * 100).toFixed(1)}% = ${formatPrice(position.aboveLevel.price, pair)}`,
297
+ );
298
+ }
299
+ }
300
+ lines.push('');
301
+
302
+ // Retracement levels
303
+ if (mode !== 'extension') {
304
+ lines.push('【リトレースメント水準】');
305
+ for (const level of levels) {
306
+ const nearest = level.isNearest ? ' ← 最寄り' : '';
307
+ lines.push(
308
+ ` ${(level.ratio * 100).toFixed(1)}%: ${formatPrice(level.price, pair)} (${formatPercent(level.distancePct, { sign: true })})${nearest}`,
309
+ );
310
+ }
311
+ lines.push('');
312
+ }
313
+
314
+ // Extension levels
315
+ if (mode !== 'retracement' && extensions.length > 0) {
316
+ lines.push('【エクステンション水準】');
317
+ for (const ext of extensions) {
318
+ lines.push(
319
+ ` ${(ext.ratio * 100).toFixed(1)}%: ${formatPrice(ext.price, pair)} (${formatPercent(ext.distancePct, { sign: true })})`,
320
+ );
321
+ }
322
+ lines.push('');
323
+ }
324
+
325
+ // Reaction stats — all levels with full detail
326
+ const meaningfulStats = levelStats.filter((s) => s.samplesCount >= 1);
327
+ if (meaningfulStats.length > 0) {
328
+ lines.push('【過去の反応実績(各水準の統計)】');
329
+ for (const stat of meaningfulStats) {
330
+ const ratioLabel = `${(stat.ratio * 100).toFixed(1)}%`;
331
+ if (stat.samplesCount === 0) {
332
+ lines.push(` ${ratioLabel}: データなし`);
333
+ continue;
334
+ }
335
+ const bounceRatePct = (stat.bounceRate * 100).toFixed(0);
336
+ lines.push(
337
+ ` ${ratioLabel}: 反発率 ${bounceRatePct}%(${stat.samplesCount}回中${Math.round(stat.bounceRate * stat.samplesCount)}回反発)`,
338
+ );
339
+ lines.push(` - 反発後の平均リターン: ${formatPercent(stat.avgBounceReturnPct, { sign: true })}`);
340
+ lines.push(` - ブレイク後の平均リターン: ${formatPercent(stat.avgBreakthroughReturnPct, { sign: true })}`);
341
+ lines.push(` - 水準付近の滞在足数(中央値): ${stat.medianDwellBars}本`);
342
+ lines.push(` - 信頼度: ${stat.confidence}(サンプル${stat.samplesCount}件)`);
343
+ }
344
+ lines.push('');
345
+
346
+ // Highlight best bounce
347
+ const bestBounce = [...meaningfulStats]
348
+ .filter((s) => s.samplesCount >= 2)
349
+ .sort((a, b) => b.bounceRate - a.bounceRate)[0];
350
+ if (bestBounce) {
351
+ lines.push(
352
+ `注目: ${(bestBounce.ratio * 100).toFixed(1)}% 水準が最も反発率が高い(${(bestBounce.bounceRate * 100).toFixed(0)}%、${bestBounce.samplesCount}回中)`,
353
+ );
354
+ lines.push('');
355
+ }
356
+ } else {
357
+ lines.push('【過去の反応実績】該当データなし(分析期間内に各水準へのタッチがありませんでした)');
358
+ lines.push('');
359
+ }
360
+
361
+ lines.push('【判定ロジック】');
362
+ lines.push(`- スイング検出: 期間内の最高値・最安値を自動検出`);
363
+ lines.push(`- トレンド判定: 高値→安値(下降)、安値→高値(上昇)の時系列順`);
364
+ lines.push(`- リトレースメント: 0%, 23.6%, 38.2%, 50%, 61.8%, 78.6%, 100%`);
365
+ lines.push(`- エクステンション: 127.2%, 141.4%, 161.8%, 200%, 261.8%`);
366
+ if (meaningfulStats.length > 0) {
367
+ lines.push(`- 反応実績: 各水準±0.5%ゾーンへのタッチを集計(過去データ)`);
368
+ }
369
+
370
+ return [{ type: 'text', text: lines.join('\n') }];
371
+ }
372
+
373
+ // ── Main Handler ──
374
+
375
+ export default async function analyzeFibonacci(opts: Record<string, unknown> = {}) {
376
+ const input = AnalyzeFibonacciInputSchema.parse(opts);
377
+ const pair = input.pair as string;
378
+ const { type: timeframe, lookbackDays, mode, historyLookbackDays } = input;
379
+
380
+ const chk = ensurePair(pair);
381
+ if (!chk.ok) return failFromValidation(chk, AnalyzeFibonacciOutputSchema);
382
+
383
+ try {
384
+ // Fetch candle data for analysis period
385
+ const candlesRes = await getCandles(chk.pair, timeframe, undefined, lookbackDays + 10);
386
+ if (!candlesRes.ok) {
387
+ return AnalyzeFibonacciOutputSchema.parse(
388
+ fail(candlesRes.summary || 'candles failed', candlesRes.meta.errorType || 'internal'),
389
+ );
390
+ }
391
+
392
+ const candles: NormalizedCandle[] = candlesRes.data.normalized || [];
393
+ if (candles.length < 10) {
394
+ return AnalyzeFibonacciOutputSchema.parse(fail('ローソク足データが不足しています(最低10本必要)', 'data'));
395
+ }
396
+
397
+ const currentPrice = candles[candles.length - 1].close;
398
+
399
+ // Detect swings
400
+ const { swingHigh, swingLow, trend } = detectSignificantSwings(candles);
401
+ const range = swingHigh.price - swingLow.price;
402
+
403
+ if (range <= 0) {
404
+ return AnalyzeFibonacciOutputSchema.parse(fail('スイングハイとスイングローの差が検出できません', 'data'));
405
+ }
406
+
407
+ // Calculate levels
408
+ let levels: FibLevel[] = [];
409
+ let extensions: FibLevel[] = [];
410
+
411
+ if (mode !== 'extension') {
412
+ levels = calculateLevels(swingHigh, swingLow, trend, currentPrice, RETRACEMENT_RATIOS);
413
+ levels = markNearest(levels, currentPrice);
414
+ }
415
+
416
+ if (mode !== 'retracement') {
417
+ extensions = calculateExtensions(swingHigh, swingLow, trend, currentPrice, EXTENSION_RATIOS);
418
+ }
419
+
420
+ const _allLevels = [...levels, ...extensions];
421
+ const position = findPosition(levels.length > 0 ? levels : extensions, currentPrice);
422
+
423
+ // Calculate historical reaction stats (Feature #3)
424
+ // Fetch extended history for statistics
425
+ let levelStats: LevelStat[] = [];
426
+ if (levels.length > 0) {
427
+ let historyCandles: NormalizedCandle[] = candles;
428
+ if (historyLookbackDays > lookbackDays) {
429
+ try {
430
+ const histRes = await getCandles(chk.pair, timeframe, undefined, historyLookbackDays + 10);
431
+ if (histRes.ok && histRes.data.normalized?.length > 0) {
432
+ historyCandles = histRes.data.normalized;
433
+ }
434
+ } catch {
435
+ // Fall back to current candle data
436
+ }
437
+ }
438
+ levelStats = calculateLevelStats(historyCandles, levels);
439
+ }
440
+
441
+ // Generate content
442
+ const content = generateContent(
443
+ chk.pair,
444
+ timeframe,
445
+ currentPrice,
446
+ trend,
447
+ swingHigh,
448
+ swingLow,
449
+ range,
450
+ levels,
451
+ extensions,
452
+ position,
453
+ levelStats,
454
+ mode,
455
+ lookbackDays,
456
+ );
457
+
458
+ const nearestLabel = position.nearestLevel ? `${(position.nearestLevel.ratio * 100).toFixed(1)}%水準付近` : '';
459
+ const summaryText = `${formatPair(chk.pair)} フィボナッチ分析: ${trend === 'up' ? '上昇' : '下降'}トレンド、${nearestLabel}(${formatPrice(currentPrice, chk.pair)})`;
460
+
461
+ const data = {
462
+ pair: chk.pair,
463
+ timeframe,
464
+ currentPrice,
465
+ trend,
466
+ swingHigh,
467
+ swingLow,
468
+ range,
469
+ levels,
470
+ extensions,
471
+ position,
472
+ levelStats: levelStats.length > 0 ? levelStats : undefined,
473
+ };
474
+
475
+ const meta = createMeta(chk.pair as Pair, {
476
+ timeframe,
477
+ lookbackDays,
478
+ mode,
479
+ historyLookbackDays: levelStats.length > 0 ? historyLookbackDays : undefined,
480
+ });
481
+
482
+ return AnalyzeFibonacciOutputSchema.parse({
483
+ ok: true,
484
+ summary: summaryText,
485
+ content,
486
+ data,
487
+ meta,
488
+ });
489
+ } catch (err: unknown) {
490
+ return failFromError(err, {
491
+ schema: AnalyzeFibonacciOutputSchema,
492
+ defaultMessage: 'フィボナッチ分析中にエラーが発生しました',
493
+ });
494
+ }
495
+ }
496
+
497
+ // ── MCP ツール定義(tool-registry から自動収集) ──
498
+ export const toolDef: ToolDefinition = {
499
+ name: 'analyze_fibonacci',
500
+ description: `フィボナッチ・リトレースメント/エクステンション水準を自動計算。
501
+
502
+ 【機能】
503
+ - スイングハイ・スイングローを自動検出しトレンド判定
504
+ - リトレースメント水準(0%, 23.6%, 38.2%, 50%, 61.8%, 78.6%, 100%)を算出
505
+ - エクステンション水準(127.2%, 141.4%, 161.8%, 200%, 261.8%)を算出
506
+ - 現在価格と各水準の距離(%)、最寄り水準を特定
507
+ - 過去の反応実績(反発率・平均リターン・滞在期間)を統計
508
+
509
+ 【出力】
510
+ - content: LLM 向けテキスト解説
511
+ - structuredContent.data: 全水準の価格・距離%・反応統計を含む JSON
512
+ → render_chart_svg や HTML アーティファクトで即座に可視化可能
513
+
514
+ 【パラメータ】
515
+ - pair: 通貨ペア(デフォルト: btc_jpy)
516
+ - type: 時間足(デフォルト: 1day)
517
+ - lookbackDays: 分析期間(デフォルト: 90日)
518
+ - mode: retracement / extension / both(デフォルト: both)
519
+ - historyLookbackDays: 反応実績の集計期間(デフォルト: 180日)
520
+
521
+ 複数タイムフレーム分析が必要な場合は analyze_mtf_fibonacci を使用。`,
522
+ inputSchema: AnalyzeFibonacciInputSchema,
523
+ handler: async (args: {
524
+ pair?: string;
525
+ type?: string;
526
+ lookbackDays?: number;
527
+ mode?: 'retracement' | 'extension' | 'both';
528
+ historyLookbackDays?: number;
529
+ }) => analyzeFibonacci(args),
530
+ };