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,174 @@
1
+ /**
2
+ * tool-registry.ts — 全 MCP ツール定義の集約
3
+ *
4
+ * 各ツールファイル(tools/*.ts)または複雑なハンドラファイル(src/handlers/*Handler.ts)から
5
+ * toolDef をインポートし、配列として server.ts に提供する。
6
+ *
7
+ * 【ツール追加手順】
8
+ * 1. tools/<name>.ts にツール関数を実装
9
+ * 2. 同ファイル(または src/handlers/<name>Handler.ts)に toolDef を export
10
+ * 3. ★ 本ファイルに import + allToolDefs に追加 ★
11
+ * 4. npm run gen:types && npm run typecheck
12
+ */
13
+
14
+ import { log } from '../lib/logger.js';
15
+ import { toolDef as analyzeBbSnapshot } from '../tools/analyze_bb_snapshot.js';
16
+ import { toolDef as analyzeCandlePatterns } from '../tools/analyze_candle_patterns.js';
17
+ import { toolDef as analyzeCurrencyStrength } from '../tools/analyze_currency_strength.js';
18
+ import { toolDef as analyzeEmaSnapshot } from '../tools/analyze_ema_snapshot.js';
19
+ import { toolDef as analyzeIchimokuSnapshot } from '../tools/analyze_ichimoku_snapshot.js';
20
+ import { toolDef as analyzeMtfFibonacci } from '../tools/analyze_mtf_fibonacci.js';
21
+ import { toolDef as analyzeMtfSma } from '../tools/analyze_mtf_sma.js';
22
+ import { toolDef as analyzeSmaSnapshot } from '../tools/analyze_sma_snapshot.js';
23
+ import { toolDef as analyzeStochSnapshot } from '../tools/analyze_stoch_snapshot.js';
24
+ import { toolDef as analyzeSupportResistance } from '../tools/analyze_support_resistance.js';
25
+ import { toolDef as analyzeVolumeProfile } from '../tools/analyze_volume_profile.js';
26
+ import { toolDef as detectMacdCross } from '../tools/detect_macd_cross.js';
27
+ import { toolDef as detectWhaleEvents } from '../tools/detect_whale_events.js';
28
+ import { toolDef as getCandles } from '../tools/get_candles.js';
29
+ import { toolDef as getFlowMetrics } from '../tools/get_flow_metrics.js';
30
+ import { toolDef as getOrderbook } from '../tools/get_orderbook.js';
31
+ import { toolDef as getTicker } from '../tools/get_ticker.js';
32
+ import { toolDef as getTransactions } from '../tools/get_transactions.js';
33
+ import { toolDef as prepareChartData } from '../tools/prepare_chart_data.js';
34
+ import { toolDef as prepareDepthData } from '../tools/prepare_depth_data.js';
35
+ import { toolDef as renderCandlePatternDiagram } from '../tools/render_candle_pattern_diagram.js';
36
+ import { toolDef as renderDepthSvg } from '../tools/render_depth_svg.js';
37
+ import { toolDef as validateCandleData } from '../tools/validate_candle_data.js';
38
+ import { toolDef as analyzeFibonacci } from './handlers/analyzeFibonacciHandler.js';
39
+ import { toolDef as analyzeIndicators } from './handlers/analyzeIndicatorsHandler.js';
40
+ import { toolDef as analyzeMarketSignal } from './handlers/analyzeMarketSignalHandler.js';
41
+ import { toolDef as detectPatterns } from './handlers/detectPatternsHandler.js';
42
+ import { toolDef as getTickersJpy } from './handlers/getTickersJpyHandler.js';
43
+ import { toolDef as getVolatilityMetrics } from './handlers/getVolatilityMetricsHandler.js';
44
+ import { toolDef as renderChartSvg } from './handlers/renderChartSvgHandler.js';
45
+ import { toolDef as runBacktest } from './handlers/runBacktestHandler.js';
46
+ import { isPrivateApiEnabled } from './private/config.js';
47
+ import { startCleanupTimer } from './private/confirmation.js';
48
+ import type { ToolDefinition } from './tool-definition.js';
49
+
50
+ /**
51
+ * Public ツール一覧(カテゴリ別)
52
+ *
53
+ * | カテゴリ | ツール数 | 概要 |
54
+ * |---------------------|---------|------------------------------------------|
55
+ * | Data Retrieval | 7 | ticker, orderbook, candles, transactions |
56
+ * | Technical Analysis | 13 | BB, 一目, SMA, EMA, Stoch, Fibonacci 等 |
57
+ * | Signal & Detection | 4 | 総合シグナル, パターン, MACD, クジラ |
58
+ * | Visualization | 4 | SVG チャート, 板チャート, データ整形 |
59
+ * | Backtesting | 1 | SMA/MACD/BB/RSI 戦略バックテスト |
60
+ * | **Private** | **16** | 残高, 注文, 約定, ポートフォリオ, 信用取引, HITL確認(要 API キー)|
61
+ */
62
+ export const allToolDefs: ToolDefinition[] = [
63
+ // ── Data Retrieval (7) ──
64
+ getTicker,
65
+ getOrderbook,
66
+ getCandles,
67
+ getTransactions,
68
+ getFlowMetrics,
69
+ getVolatilityMetrics,
70
+ getTickersJpy,
71
+
72
+ // ── Technical Analysis (13) ──
73
+ analyzeIndicators,
74
+ analyzeBbSnapshot,
75
+ analyzeIchimokuSnapshot,
76
+ analyzeSmaSnapshot,
77
+ analyzeEmaSnapshot,
78
+ analyzeStochSnapshot,
79
+ analyzeMtfSma,
80
+ analyzeSupportResistance,
81
+ analyzeCandlePatterns,
82
+ analyzeVolumeProfile,
83
+ analyzeCurrencyStrength,
84
+ analyzeFibonacci,
85
+ analyzeMtfFibonacci,
86
+
87
+ // ── Signal & Detection (4) ──
88
+ analyzeMarketSignal,
89
+ detectPatterns,
90
+ detectMacdCross,
91
+ detectWhaleEvents,
92
+
93
+ // ── Visualization (5) ──
94
+ prepareChartData,
95
+ prepareDepthData,
96
+ renderChartSvg,
97
+ renderDepthSvg,
98
+ renderCandlePatternDiagram,
99
+
100
+ // ── Data Quality (1) ──
101
+ validateCandleData,
102
+
103
+ // ── Backtesting (1) ──
104
+ runBacktest,
105
+ ];
106
+
107
+ // ── Private API tools(APIキー設定時のみ有効化) ──
108
+ if (isPrivateApiEnabled()) {
109
+ // 動的 import で private ツールを追加
110
+ // tool-registry.ts は起動時に1回だけ評価されるため、top-level await 相当の
111
+ // 即時実行関数で動的 import を行い、allToolDefs に追加する。
112
+ // ※ ESM の top-level await が使えない環境でも動作するよう IIFE で対応。
113
+ const { toolDef: getMyAssets } = await import('../tools/private/get_my_assets.js');
114
+ const { toolDef: getMyTradeHistory } = await import('../tools/private/get_my_trade_history.js');
115
+ const { toolDef: getMyOrders } = await import('../tools/private/get_my_orders.js');
116
+ const { toolDef: analyzeMyPortfolio } = await import('../tools/private/analyze_my_portfolio.js');
117
+ const { toolDef: getMyDepositWithdrawal } = await import('../tools/private/get_my_deposit_withdrawal.js');
118
+ // Trading tools (preview → execute の2ステップ確認)
119
+ const { toolDef: previewOrder } = await import('../tools/private/preview_order.js');
120
+ const { toolDef: createOrder } = await import('../tools/private/create_order.js');
121
+ const { toolDef: previewCancelOrder } = await import('../tools/private/preview_cancel_order.js');
122
+ const { toolDef: cancelOrder } = await import('../tools/private/cancel_order.js');
123
+ const { toolDef: previewCancelOrders } = await import('../tools/private/preview_cancel_orders.js');
124
+ const { toolDef: cancelOrders } = await import('../tools/private/cancel_orders.js');
125
+ const { toolDef: getOrder } = await import('../tools/private/get_order.js');
126
+ const { toolDef: getOrdersInfo } = await import('../tools/private/get_orders_info.js');
127
+ // Margin tools
128
+ const { toolDef: getMarginStatus } = await import('../tools/private/get_margin_status.js');
129
+ const { toolDef: getMarginPositions } = await import('../tools/private/get_margin_positions.js');
130
+ const { toolDef: getMarginTradeHistory } = await import('../tools/private/get_margin_trade_history.js');
131
+ allToolDefs.push(
132
+ getMyAssets,
133
+ getMyTradeHistory,
134
+ getMyOrders,
135
+ analyzeMyPortfolio,
136
+ getMyDepositWithdrawal,
137
+ previewOrder,
138
+ createOrder,
139
+ previewCancelOrder,
140
+ cancelOrder,
141
+ previewCancelOrders,
142
+ cancelOrders,
143
+ getOrder,
144
+ getOrdersInfo,
145
+ getMarginStatus,
146
+ getMarginPositions,
147
+ getMarginTradeHistory,
148
+ );
149
+ startCleanupTimer();
150
+ log('info', {
151
+ type: 'private_api',
152
+ message: 'Private API tools enabled',
153
+ tools: [
154
+ 'get_my_assets',
155
+ 'get_my_trade_history',
156
+ 'get_my_orders',
157
+ 'analyze_my_portfolio',
158
+ 'get_my_deposit_withdrawal',
159
+ 'preview_order',
160
+ 'create_order',
161
+ 'preview_cancel_order',
162
+ 'cancel_order',
163
+ 'preview_cancel_orders',
164
+ 'cancel_orders',
165
+ 'get_order',
166
+ 'get_orders_info',
167
+ 'get_margin_status',
168
+ 'get_margin_positions',
169
+ 'get_margin_trade_history',
170
+ ],
171
+ });
172
+ } else {
173
+ log('info', { type: 'private_api', message: 'Private API tools disabled (no API key configured)' });
174
+ }
@@ -0,0 +1,9 @@
1
+ declare module 'express' {
2
+ import type { Express } from 'express-serve-static-core';
3
+ interface ExpressFactory {
4
+ (): Express;
5
+ json(options?: { limit?: string }): import('express-serve-static-core').RequestHandler;
6
+ }
7
+ const e: ExpressFactory;
8
+ export default e;
9
+ }
@@ -0,0 +1,23 @@
1
+ // Auto-generated by scripts/gen_types.ts. Do NOT edit manually.
2
+ /* eslint-disable */
3
+ import type { z } from 'zod';
4
+ import type {
5
+ CandleTypeEnum,
6
+ RenderChartSvgInputSchema,
7
+ RenderChartSvgOutputSchema,
8
+ ChartPayloadSchema,
9
+ GetIndicatorsDataSchema,
10
+ GetIndicatorsMetaSchema,
11
+ } from '../schemas.js';
12
+
13
+ // === Enums & Inputs ===
14
+
15
+ export type CandleType = z.infer<typeof CandleTypeEnum>;
16
+ export type RenderChartSvgInput = z.infer<typeof RenderChartSvgInputSchema>;
17
+ export type RenderChartSvgOutput = z.infer<typeof RenderChartSvgOutputSchema>;
18
+
19
+ // === Indicators DTOs ===
20
+
21
+ export type ChartPayloadFromSchema = z.infer<typeof ChartPayloadSchema>;
22
+ export type GetIndicatorsDataFromSchema = z.infer<typeof GetIndicatorsDataSchema>;
23
+ export type GetIndicatorsMetaFromSchema = z.infer<typeof GetIndicatorsMetaSchema>;
@@ -0,0 +1,385 @@
1
+ import type { z } from 'zod';
2
+ import { nowIso } from '../lib/datetime.js';
3
+ import { formatSummary } from '../lib/formatter.js';
4
+ import { fail, failFromError, failFromValidation, ok } from '../lib/result.js';
5
+ import { createMeta, ensurePair } from '../lib/validate.js';
6
+ import {
7
+ type AnalyzeBbSnapshotDataSchemaOut,
8
+ AnalyzeBbSnapshotInputSchema,
9
+ AnalyzeBbSnapshotOutputSchema,
10
+ } from '../src/schemas.js';
11
+ import type { ToolDefinition } from '../src/tool-definition.js';
12
+ import analyzeIndicators from './analyze_indicators.js';
13
+
14
+ export interface BbTimeseriesEntry {
15
+ time: string;
16
+ zScore: number | null;
17
+ bandWidthPct: number | null;
18
+ }
19
+
20
+ export interface BuildBbDefaultTextInput {
21
+ baseSummary: string;
22
+ position: string | null;
23
+ bandwidth_state: string | null;
24
+ volatility_trend: string | null;
25
+ bandWidthPct_percentile: number | null;
26
+ current_vs_avg: string | null;
27
+ signals: string[];
28
+ next_steps: {
29
+ if_need_detail: string;
30
+ if_need_visualization: string;
31
+ if_extreme_detected?: string;
32
+ };
33
+ mid: number | null;
34
+ upper: number | null;
35
+ lower: number | null;
36
+ zScore: number | null;
37
+ bandWidthPct: number | null;
38
+ timeseries: BbTimeseriesEntry[] | null;
39
+ }
40
+
41
+ /** テキスト組み立て(BBデフォルトモード表示)— テスト可能な純粋関数 */
42
+ export function buildBbDefaultText(input: BuildBbDefaultTextInput): string {
43
+ const {
44
+ baseSummary,
45
+ position,
46
+ bandwidth_state,
47
+ volatility_trend,
48
+ bandWidthPct_percentile,
49
+ current_vs_avg,
50
+ signals,
51
+ next_steps,
52
+ mid,
53
+ upper,
54
+ lower,
55
+ zScore,
56
+ bandWidthPct,
57
+ timeseries,
58
+ } = input;
59
+ return (
60
+ [
61
+ String(baseSummary),
62
+ '',
63
+ `Position: ${position ?? 'n/a'}`,
64
+ `Band State: ${bandwidth_state ?? 'n/a'}`,
65
+ `Volatility Trend: ${volatility_trend ?? 'n/a'}`,
66
+ ...(bandWidthPct_percentile != null
67
+ ? [`Band Width Percentile: ${bandWidthPct_percentile}th (${current_vs_avg} vs avg)`]
68
+ : []),
69
+ '',
70
+ 'Signals:',
71
+ ...(signals?.length ? signals.map((s) => `- ${s}`) : ['- None']),
72
+ '',
73
+ 'Next Steps:',
74
+ `- ${next_steps.if_need_detail}`,
75
+ `- ${next_steps.if_need_visualization}`,
76
+ '',
77
+ '📊 数値データ:',
78
+ `BB middle:${mid} upper:${upper} lower:${lower} zScore:${zScore?.toFixed(3)} bw:${bandWidthPct?.toFixed(2)}%`,
79
+ ...(timeseries
80
+ ? [
81
+ '',
82
+ `📋 直近${timeseries.length}本のBB推移:`,
83
+ ...timeseries.map((t) => `${t.time.slice(0, 10)} z:${t.zScore} bw:${t.bandWidthPct}%`),
84
+ ]
85
+ : []),
86
+ ].join('\n') +
87
+ `\n\n---\n📌 含まれるもの: ボリンジャーバンド(±2σ)、Zスコア、バンド幅、直近30本の推移` +
88
+ `\n📌 含まれないもの: 他のテクニカル指標(RSI・MACD・一目均衡表)、出来高フロー、板情報` +
89
+ `\n📌 補完ツール: analyze_indicators(他指標), analyze_ichimoku_snapshot(一目), get_flow_metrics(出来高), get_volatility_metrics(ボラ詳細)`
90
+ );
91
+ }
92
+
93
+ export default async function analyzeBbSnapshot(
94
+ pair: string = 'btc_jpy',
95
+ type: string = '1day',
96
+ limit: number = 120,
97
+ mode: 'default' | 'extended' = 'default',
98
+ ) {
99
+ const chk = ensurePair(pair);
100
+ if (!chk.ok) return failFromValidation(chk, AnalyzeBbSnapshotOutputSchema);
101
+ try {
102
+ const indRes = await analyzeIndicators(chk.pair, type, Math.max(60, limit));
103
+ if (!indRes?.ok)
104
+ return AnalyzeBbSnapshotOutputSchema.parse(
105
+ fail(indRes?.summary || 'indicators failed', (indRes?.meta as { errorType?: string })?.errorType || 'internal'),
106
+ ) as ReturnType<typeof fail>;
107
+
108
+ const close = indRes.data.normalized.at(-1)?.close ?? null;
109
+ const mid = indRes.data.indicators.BB2_middle ?? indRes.data.indicators.BB_middle ?? null;
110
+ const upper = indRes.data.indicators.BB2_upper ?? indRes.data.indicators.BB_upper ?? null;
111
+ const lower = indRes.data.indicators.BB2_lower ?? indRes.data.indicators.BB_lower ?? null;
112
+
113
+ let zScore: number | null = null;
114
+ if (close != null && mid != null && upper != null && lower != null) {
115
+ const halfWidth = (upper - lower) / 2;
116
+ if (halfWidth > 0) zScore = (close - mid) / halfWidth;
117
+ }
118
+ let bandWidthPct: number | null = null;
119
+ if (upper != null && lower != null && mid != null && mid !== 0) bandWidthPct = ((upper - lower) / mid) * 100;
120
+
121
+ const tags: string[] = [];
122
+ if (zScore != null && zScore > 1) tags.push('above_upper_band_risk');
123
+ if (zScore != null && zScore < -1) tags.push('below_lower_band_risk');
124
+
125
+ const summaryBase = formatSummary({
126
+ pair: chk.pair,
127
+ latest: close ?? undefined,
128
+ extra: `z=${zScore?.toFixed(2) ?? 'n/a'} bw=${bandWidthPct?.toFixed(2) ?? 'n/a'}%`,
129
+ });
130
+ // Build helper timeseries (last 30)
131
+ const candles = indRes?.data?.normalized as Array<{ isoTime?: string; close: number }> | undefined;
132
+ const bbSeries = (
133
+ indRes?.data?.indicators as { bb2_series?: { upper: number[]; middle: number[]; lower: number[] } }
134
+ )?.bb2_series;
135
+ const timeseries = (() => {
136
+ try {
137
+ if (!candles || !bbSeries) return null;
138
+ const n = Math.min(30, candles.length, bbSeries.middle.length, bbSeries.upper.length, bbSeries.lower.length);
139
+ const arr: Array<{ time: string; zScore: number | null; bandWidthPct: number | null }> = [];
140
+ for (let i = n; i >= 1; i--) {
141
+ const idx = candles.length - i;
142
+ const t = candles[idx]?.isoTime || '';
143
+ const m = bbSeries.middle[idx];
144
+ const u = bbSeries.upper[idx];
145
+ const l = bbSeries.lower[idx];
146
+ const c = candles[idx]?.close;
147
+ const half = (u - l) / 2;
148
+ const z = m != null && half > 0 ? (c - m) / half : null;
149
+ const bw = m ? ((u - l) / m) * 100 : null;
150
+ arr.push({
151
+ time: t,
152
+ zScore: z == null ? null : Number(z.toFixed(2)),
153
+ bandWidthPct: bw == null ? null : Number(bw.toFixed(2)),
154
+ });
155
+ }
156
+ return arr;
157
+ } catch {
158
+ return null;
159
+ }
160
+ })();
161
+
162
+ if (mode === 'default') {
163
+ const position =
164
+ zScore == null
165
+ ? null
166
+ : Math.abs(zScore) < 0.3
167
+ ? 'near_middle'
168
+ : zScore >= 1.8
169
+ ? 'at_upper'
170
+ : zScore <= -1.8
171
+ ? 'at_lower'
172
+ : zScore > 0
173
+ ? 'upper_zone'
174
+ : 'lower_zone';
175
+ const bw = bandWidthPct ?? 0;
176
+ const bandwidth_state = bw <= 8 ? 'squeeze' : bw <= 18 ? 'normal' : bw <= 30 ? 'expanding' : 'wide';
177
+ // 統計情報の計算(過去30本のバンド幅から)
178
+ const context = (() => {
179
+ if (!timeseries || timeseries.length === 0 || bandWidthPct == null) {
180
+ return {
181
+ bandWidthPct_30d_avg: null as number | null,
182
+ bandWidthPct_percentile: null as number | null,
183
+ current_vs_avg: null as string | null,
184
+ };
185
+ }
186
+
187
+ const bandWidths = timeseries.map((t) => t.bandWidthPct).filter((bw): bw is number => bw != null);
188
+
189
+ if (bandWidths.length === 0) {
190
+ return {
191
+ bandWidthPct_30d_avg: null as number | null,
192
+ bandWidthPct_percentile: null as number | null,
193
+ current_vs_avg: null as string | null,
194
+ };
195
+ }
196
+
197
+ const avg = bandWidths.reduce((a, b) => a + b, 0) / bandWidths.length;
198
+ const sorted = [...bandWidths].sort((a, b) => a - b);
199
+ const below = sorted.filter((bw) => bw < (bandWidthPct as number)).length;
200
+ const percentile = Math.round((below / sorted.length) * 100);
201
+ const diffPct = avg !== 0 ? (((bandWidthPct as number) - avg) / avg) * 100 : 0;
202
+ const current_vs_avg = `${diffPct > 0 ? '+' : ''}${diffPct.toFixed(1)}%`;
203
+
204
+ return {
205
+ bandWidthPct_30d_avg: Number(avg.toFixed(2)),
206
+ bandWidthPct_percentile: percentile,
207
+ current_vs_avg,
208
+ };
209
+ })();
210
+
211
+ // ボラティリティトレンドの判定(直近5本 vs それ以前)
212
+ const volatility_trend = (() => {
213
+ if (!timeseries || timeseries.length < 10) return 'stable' as const;
214
+ const recent5 = timeseries
215
+ .slice(-5)
216
+ .map((t) => t.bandWidthPct)
217
+ .filter((bw): bw is number => bw != null);
218
+ const prev5 = timeseries
219
+ .slice(-10, -5)
220
+ .map((t) => t.bandWidthPct)
221
+ .filter((bw): bw is number => bw != null);
222
+ if (recent5.length === 0 || prev5.length === 0) return 'stable' as const;
223
+ const recentAvg = recent5.reduce((a, b) => a + b, 0) / recent5.length;
224
+ const prevAvg = prev5.reduce((a, b) => a + b, 0) / prev5.length;
225
+ const change = (recentAvg - prevAvg) / prevAvg;
226
+ if (change > 0.1) return 'increasing' as const;
227
+ if (change < -0.1) return 'decreasing' as const;
228
+ return 'stable' as const;
229
+ })();
230
+
231
+ const interpretation = { position, bandwidth_state, volatility_trend } as const;
232
+
233
+ const signals: string[] = [];
234
+ if (position === 'near_middle') signals.push('Price consolidating near middle band');
235
+ if (bandwidth_state === 'normal') signals.push('Band width around typical levels');
236
+
237
+ // 統計情報を使った追加シグナル
238
+ if (context.bandWidthPct_30d_avg != null && context.current_vs_avg != null) {
239
+ if (context.bandWidthPct_percentile != null) {
240
+ if (context.bandWidthPct_percentile < 20) {
241
+ signals.push(
242
+ `Band width compressed (${context.bandWidthPct_percentile}th percentile) - potential breakout setup`,
243
+ );
244
+ } else if (context.bandWidthPct_percentile > 80) {
245
+ signals.push(
246
+ `Band width expanded (${context.bandWidthPct_percentile}th percentile) - high volatility phase`,
247
+ );
248
+ }
249
+ }
250
+ signals.push(`Band width ${context.current_vs_avg} vs 30-day average`);
251
+ }
252
+
253
+ if (volatility_trend === 'increasing') {
254
+ signals.push('Volatility increasing in recent periods');
255
+ } else if (volatility_trend === 'decreasing') {
256
+ signals.push('Volatility decreasing - potential squeeze forming');
257
+ }
258
+
259
+ if (!signals.length) signals.push('No extreme positioning detected');
260
+ const next_steps = {
261
+ if_need_detail: "Use mode='extended' for ±1σ/±3σ analysis",
262
+ if_need_visualization: 'Use render_chart_svg with withBB=true',
263
+ if_extreme_detected: 'Consider get_volatility_metrics for deeper analysis',
264
+ };
265
+ const data: z.infer<typeof AnalyzeBbSnapshotDataSchemaOut> = {
266
+ mode,
267
+ price: close ?? null,
268
+ bb: { middle: mid, upper, lower, zScore, bandWidthPct },
269
+ interpretation,
270
+ context,
271
+ signals,
272
+ next_steps,
273
+ };
274
+ // content 強化用: LLM が本文だけ見ても要点が掴めるように複数行の要約を生成
275
+ const summaryLines = buildBbDefaultText({
276
+ baseSummary: summaryBase,
277
+ position: interpretation.position,
278
+ bandwidth_state: interpretation.bandwidth_state,
279
+ volatility_trend: interpretation.volatility_trend,
280
+ bandWidthPct_percentile: context.bandWidthPct_percentile,
281
+ current_vs_avg: context.current_vs_avg,
282
+ signals,
283
+ next_steps,
284
+ mid,
285
+ upper,
286
+ lower,
287
+ zScore,
288
+ bandWidthPct,
289
+ timeseries,
290
+ });
291
+ const meta = createMeta(chk.pair, {
292
+ type,
293
+ count: indRes.data.normalized.length,
294
+ mode,
295
+ extra: {
296
+ timeseries: timeseries ? { last_30_candles: timeseries } : undefined,
297
+ metadata: {
298
+ calculation_params: { period: 20, std_dev_multiplier: 2 },
299
+ data_quality: 'complete',
300
+ last_updated: nowIso(),
301
+ },
302
+ },
303
+ });
304
+ return AnalyzeBbSnapshotOutputSchema.parse(ok(summaryLines, data, meta));
305
+ }
306
+
307
+ // extended mode
308
+ const bbBands: Record<string, number | null> = {
309
+ '+3σ': null,
310
+ '+2σ': upper,
311
+ '+1σ': null,
312
+ '-1σ': null,
313
+ '-2σ': lower,
314
+ '-3σ': null,
315
+ };
316
+ const bandWidthAll: Record<string, number | null> = { '±1σ': null, '±2σ': bandWidthPct, '±3σ': null };
317
+ const current_zone =
318
+ zScore == null
319
+ ? null
320
+ : Math.abs(zScore) <= 1
321
+ ? 'within_1σ'
322
+ : Math.abs(zScore) <= 2
323
+ ? '1σ_to_2σ'
324
+ : Math.abs(zScore) <= 3
325
+ ? 'beyond_2σ'
326
+ : 'beyond_3σ';
327
+ const data: z.infer<typeof AnalyzeBbSnapshotDataSchemaOut> = {
328
+ mode,
329
+ price: close ?? null,
330
+ bb: { middle: mid, bands: bbBands, zScore, bandWidthPct: bandWidthAll },
331
+ position_analysis: { current_zone },
332
+ extreme_events: {
333
+ touches_3σ_last_30d: null,
334
+ touches_2σ_last_30d: null,
335
+ band_walk_detected: null,
336
+ squeeze_percentile: null,
337
+ },
338
+ interpretation: { volatility_state: null, extreme_risk: null, mean_reversion_potential: null },
339
+ tags,
340
+ };
341
+ const meta = createMeta(chk.pair, {
342
+ type,
343
+ count: indRes.data.normalized.length,
344
+ mode,
345
+ extra: {
346
+ timeseries: timeseries ? { last_30_candles: timeseries } : undefined,
347
+ metadata: {
348
+ calculation_params: { period: 20, std_dev_multiplier: 2 },
349
+ data_quality: 'complete',
350
+ last_updated: nowIso(),
351
+ },
352
+ },
353
+ });
354
+ const extSummary =
355
+ summaryBase +
356
+ `\n\n---\n📌 含まれるもの: ボリンジャーバンド拡張(±1σ/±2σ/±3σ)、Zスコア、バンド幅` +
357
+ `\n📌 含まれないもの: 他のテクニカル指標(RSI・MACD・一目均衡表)、出来高フロー、板情報` +
358
+ `\n📌 補完ツール: analyze_indicators(他指標), get_flow_metrics(出来高), get_volatility_metrics(ボラ詳細)`;
359
+ return AnalyzeBbSnapshotOutputSchema.parse(ok(extSummary, data, meta));
360
+ } catch (e: unknown) {
361
+ return failFromError(e, { schema: AnalyzeBbSnapshotOutputSchema });
362
+ }
363
+ }
364
+
365
+ // ── MCP ツール定義(tool-registry から自動収集) ──
366
+ export const toolDef: ToolDefinition = {
367
+ name: 'analyze_bb_snapshot',
368
+ description: `[Bollinger Bands / BB / Squeeze] ボリンジャーバンド(BB / squeeze / bandwidth / zScore)の数値スナップショット。軽量・BB特化。
369
+
370
+ mode=default: ±2σ帯の基本情報 / mode=extended: ±1σ/±2σ/±3σの詳細分析。
371
+
372
+ ⚠️ 最新値のみ。時系列チャート描画 → prepare_chart_data(indicators: ["BB"])。`,
373
+ inputSchema: AnalyzeBbSnapshotInputSchema,
374
+ handler: async ({
375
+ pair,
376
+ type,
377
+ limit,
378
+ mode,
379
+ }: {
380
+ pair?: string;
381
+ type?: string;
382
+ limit?: number;
383
+ mode?: 'default' | 'extended';
384
+ }) => analyzeBbSnapshot(pair, type, limit, mode),
385
+ };