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,294 @@
1
+ /**
2
+ * prepare_chart_data — Visualizer / チャート描画用の時系列データを返す。
3
+ *
4
+ * analyze_indicators の chart (ChartPayload) を内部で呼び出し、
5
+ * コンパクトな配列形式に整形して返す。
6
+ * 一目均衡表の chikou シフトは適用済み。
7
+ *
8
+ * デフォルトではローソク足(OHLCV)のみ返す。
9
+ * indicators パラメータで指標グループを明示指定した場合のみ、その系列を付加する。
10
+ */
11
+
12
+ import { dayjs, toIsoWithTz } from '../lib/datetime.js';
13
+ import { fail, failFromError, ok, toStructured } from '../lib/result.js';
14
+ import { createMeta, ensurePair } from '../lib/validate.js';
15
+ import type { Candle, FailResult, NumericSeries, OkResult } from '../src/schemas.js';
16
+ import { PrepareChartDataInputSchema } from '../src/schemas.js';
17
+ import type { ToolDefinition } from '../src/tool-definition.js';
18
+ import analyzeIndicators from './analyze_indicators.js';
19
+
20
+ // ── candles 配列の各要素の意味 ──
21
+ const CANDLE_FORMAT = ['open', 'high', 'low', 'close', 'volume'] as const;
22
+
23
+ // ── 指標グループ → chart.indicators キーのマッピング ──
24
+
25
+ const MAIN_SERIES_KEYS: Record<string, string[]> = {
26
+ SMA_5: ['SMA_5'],
27
+ SMA_20: ['SMA_20'],
28
+ SMA_25: ['SMA_25'],
29
+ SMA_50: ['SMA_50'],
30
+ SMA_75: ['SMA_75'],
31
+ SMA_200: ['SMA_200'],
32
+ EMA_12: ['EMA_12'],
33
+ EMA_26: ['EMA_26'],
34
+ EMA_50: ['EMA_50'],
35
+ EMA_200: ['EMA_200'],
36
+ BB: ['BB_upper', 'BB_middle', 'BB_lower'],
37
+ ICHIMOKU: ['ICHI_tenkan', 'ICHI_kijun', 'ICHI_spanA', 'ICHI_spanB', 'ICHI_chikou'],
38
+ };
39
+
40
+ /** JPY ペアかどうか判定 */
41
+ function isJpyPair(pair: string): boolean {
42
+ return pair.endsWith('_jpy');
43
+ }
44
+
45
+ /** 数値を丸める(JPY ペアは整数、それ以外は小数2桁) */
46
+ function roundValue(v: number | null, jpyPair: boolean): number | null {
47
+ if (v === null) return null;
48
+ return jpyPair ? Math.round(v) : Number(v.toFixed(2));
49
+ }
50
+
51
+ /** 系列が全て null かどうか判定 */
52
+ function isAllNull(series: NumericSeries): boolean {
53
+ return series.every((v) => v === null);
54
+ }
55
+
56
+ /** NumericSeries → 丸め済み値配列(全 null なら undefined) */
57
+ function toRoundedArray(series: NumericSeries, jpyPair: boolean): (number | null)[] | undefined {
58
+ if (isAllNull(series)) return undefined;
59
+ return series.map((v) => roundValue(v, jpyPair));
60
+ }
61
+
62
+ // ── コンパクト出力型 ──
63
+
64
+ interface CompactCandle {
65
+ /** [open, high, low, close, volume] */
66
+ ohlcv: number[];
67
+ }
68
+
69
+ interface CompactSubPanels {
70
+ RSI_14?: (number | null)[];
71
+ MACD?: { line: (number | null)[]; signal: (number | null)[]; hist: (number | null)[] };
72
+ STOCH_K?: (number | null)[];
73
+ STOCH_D?: (number | null)[];
74
+ }
75
+
76
+ interface PrepareChartDataResult {
77
+ times: string[];
78
+ labels?: string[];
79
+ candleFormat: readonly string[];
80
+ candles: CompactCandle['ohlcv'][];
81
+ series?: Record<string, (number | null)[]>;
82
+ subPanels?: CompactSubPanels;
83
+ }
84
+
85
+ interface PrepareChartDataMeta {
86
+ pair: string;
87
+ type: string;
88
+ count: number;
89
+ indicators: string[];
90
+ volumeUnit: string;
91
+ }
92
+
93
+ /**
94
+ * CandleType に応じた短縮ラベルフォーマットを返す。
95
+ * 日足以上は "MM/DD"、それ以外は "MM/DD HH:mm"。
96
+ */
97
+ function labelFormat(candleType: string): string {
98
+ switch (candleType) {
99
+ case '1day':
100
+ case '1week':
101
+ case '1month':
102
+ return 'MM/DD';
103
+ default:
104
+ return 'MM/DD HH:mm';
105
+ }
106
+ }
107
+
108
+ export default async function prepareChartData(
109
+ pair: string = 'btc_jpy',
110
+ type: string = '1day',
111
+ limit: number = 30,
112
+ indicators?: string[],
113
+ tz: string = 'Asia/Tokyo',
114
+ ): Promise<OkResult<PrepareChartDataResult, PrepareChartDataMeta> | FailResult> {
115
+ const chk = ensurePair(pair);
116
+ if (!chk.ok) return fail(chk.error.message, chk.error.type);
117
+
118
+ // サーバー側ガード: limit × インジケーター系列数がしきい値を超える場合、limit を自動切り詰め
119
+ const MAX_TOTAL_SERIES = 150; // limit × (1 + indicatorCount) の上限
120
+ const indicatorCount = indicators?.length ?? 0;
121
+ const seriesMultiplier = 1 + indicatorCount; // 1 = OHLCV 本体
122
+ let effectiveLimit = limit;
123
+ if (seriesMultiplier > 1 && limit * seriesMultiplier > MAX_TOTAL_SERIES) {
124
+ effectiveLimit = Math.max(5, Math.floor(MAX_TOTAL_SERIES / seriesMultiplier));
125
+ }
126
+
127
+ const jpyPair = isJpyPair(chk.pair);
128
+
129
+ try {
130
+ const res = await analyzeIndicators(chk.pair, type, effectiveLimit);
131
+ if (!res.ok) return fail(res.summary.replace(/^Error: /, ''), res.meta.errorType);
132
+
133
+ const chart = res.data.chart;
134
+ const candles = chart.candles.slice(-effectiveLimit);
135
+ const chartIndicators = chart.indicators as Record<string, unknown>;
136
+
137
+ // 指標フィルタ: 指定がなければ空(ローソク足のみ)
138
+ const selectedGroups = indicators && indicators.length > 0 ? new Set(indicators) : new Set<string>();
139
+
140
+ // 共有タイムスタンプ(tz 指定時はローカル時刻に変換)
141
+ const useTz = typeof tz === 'string' && tz.length > 0;
142
+ const fmt = labelFormat(type);
143
+ const times: string[] = [];
144
+ const labels: string[] | undefined = useTz ? [] : undefined;
145
+
146
+ for (const c of candles) {
147
+ const iso = c.isoTime ?? '';
148
+ if (!useTz || !iso) {
149
+ times.push(iso);
150
+ } else {
151
+ const ms = dayjs.utc(iso).valueOf();
152
+ times.push(toIsoWithTz(ms, tz) ?? iso);
153
+ labels?.push(dayjs(ms).tz(tz).format(fmt));
154
+ }
155
+ }
156
+
157
+ // コンパクトなローソク足配列: [o, h, l, c, v]
158
+ const compactCandles = candles.map((c: Candle) => {
159
+ const o = roundValue(c.open, jpyPair) ?? c.open;
160
+ const h = roundValue(c.high, jpyPair) ?? c.high;
161
+ const l = roundValue(c.low, jpyPair) ?? c.low;
162
+ const cl = roundValue(c.close, jpyPair) ?? c.close;
163
+ const v = c.volume ?? 0;
164
+ return [o, h, l, cl, v];
165
+ });
166
+
167
+ // メインパネル系列の構築(全 null 系列は除外)
168
+ const series: Record<string, (number | null)[]> = {};
169
+ for (const [group, keys] of Object.entries(MAIN_SERIES_KEYS)) {
170
+ if (!selectedGroups.has(group)) continue;
171
+ for (const key of keys) {
172
+ const arr = chartIndicators[key];
173
+ if (!Array.isArray(arr)) continue;
174
+ const sliced = (arr as NumericSeries).slice(-effectiveLimit);
175
+ const rounded = toRoundedArray(sliced, jpyPair);
176
+ if (rounded) {
177
+ series[key] = rounded;
178
+ }
179
+ }
180
+ }
181
+
182
+ // サブパネル系列の構築
183
+ const subPanels: CompactSubPanels = {};
184
+
185
+ if (selectedGroups.has('RSI')) {
186
+ const rsiArr = chartIndicators.RSI_14_series;
187
+ if (Array.isArray(rsiArr)) {
188
+ const sliced = (rsiArr as NumericSeries).slice(-effectiveLimit);
189
+ const rounded = toRoundedArray(sliced, false); // RSI は 0-100 なので小数2桁を維持
190
+ if (rounded) subPanels.RSI_14 = rounded;
191
+ }
192
+ }
193
+
194
+ if (selectedGroups.has('MACD')) {
195
+ const macdData = chartIndicators.macd_series as
196
+ | { line: NumericSeries; signal: NumericSeries; hist: NumericSeries }
197
+ | undefined;
198
+ if (macdData) {
199
+ const line = toRoundedArray(macdData.line.slice(-effectiveLimit), jpyPair);
200
+ const signal = toRoundedArray(macdData.signal.slice(-effectiveLimit), jpyPair);
201
+ const hist = toRoundedArray(macdData.hist.slice(-effectiveLimit), jpyPair);
202
+ if (line || signal || hist) {
203
+ subPanels.MACD = {
204
+ line: line ?? macdData.line.slice(-effectiveLimit),
205
+ signal: signal ?? macdData.signal.slice(-effectiveLimit),
206
+ hist: hist ?? macdData.hist.slice(-effectiveLimit),
207
+ };
208
+ }
209
+ }
210
+ }
211
+
212
+ if (selectedGroups.has('STOCH')) {
213
+ const stochK = chartIndicators.stoch_k_series;
214
+ const stochD = chartIndicators.stoch_d_series;
215
+ if (Array.isArray(stochK)) {
216
+ const rounded = toRoundedArray((stochK as NumericSeries).slice(-effectiveLimit), false);
217
+ if (rounded) subPanels.STOCH_K = rounded;
218
+ }
219
+ if (Array.isArray(stochD)) {
220
+ const rounded = toRoundedArray((stochD as NumericSeries).slice(-effectiveLimit), false);
221
+ if (rounded) subPanels.STOCH_D = rounded;
222
+ }
223
+ }
224
+
225
+ const hasSeries = Object.keys(series).length > 0;
226
+ const hasSubPanels = Object.keys(subPanels).length > 0;
227
+ const indicatorNames = [...Object.keys(series), ...Object.keys(subPanels)];
228
+
229
+ const data: PrepareChartDataResult = {
230
+ times,
231
+ ...(labels ? { labels } : {}),
232
+ candleFormat: CANDLE_FORMAT,
233
+ candles: compactCandles,
234
+ ...(hasSeries ? { series } : {}),
235
+ ...(hasSubPanels ? { subPanels } : {}),
236
+ };
237
+
238
+ // 出来高の単位はペアのベース通貨(例: btc_jpy → BTC)
239
+ const volumeUnit = chk.pair.split('_')[0].toUpperCase();
240
+
241
+ const meta: PrepareChartDataMeta = {
242
+ ...createMeta(chk.pair),
243
+ type,
244
+ count: candles.length,
245
+ indicators: indicatorNames,
246
+ volumeUnit,
247
+ };
248
+
249
+ const seriesNote = indicatorNames.length > 0 ? `, indicators: ${indicatorNames.join(', ')}` : '';
250
+ const truncNote =
251
+ effectiveLimit < limit ? ` ⚠️ limit was capped from ${limit} to ${effectiveLimit} to reduce context size` : '';
252
+ return ok(`${chk.pair} ${type} chart data (${candles.length} candles${seriesNote})${truncNote}`, data, meta);
253
+ } catch (err: unknown) {
254
+ return failFromError(err);
255
+ }
256
+ }
257
+
258
+ export const toolDef: ToolDefinition = {
259
+ name: 'prepare_chart_data',
260
+ description:
261
+ '[Chart / Candlestick / Visualization] チャート描画の第一選択ツール。\n\n' +
262
+ '⚠️ limit はデフォルト 30 を推奨。ユーザーが期間を明示した場合のみ増やすこと。\n' +
263
+ 'indicators はユーザーが明示的に要求した指標のみ指定すること。分析のついでに追加しない。\n' +
264
+ 'indicators の同時指定はコンテキストを大幅に消費するため、必要最小限に留めること。\n\n' +
265
+ 'デフォルトはローソク足(OHLCV)のみ返す。indicators 未指定 = ローソク足のみ。\n' +
266
+ '指標が必要な場合は indicators に明示指定: SMA_5, SMA_20, SMA_25, SMA_50, SMA_75, SMA_200, EMA_12, EMA_26, EMA_50, EMA_200, BB, ICHIMOKU, RSI, MACD, STOCH\n\n' +
267
+ 'レスポンス形式: { times[], labels?[], candles: [[o,h,l,c,v],...], series?: {指標名: values[]}, subPanels?: {...} }\n' +
268
+ 'JPY ペアの価格は整数に丸め済み。全 null 系列は自動除外。\n\n' +
269
+ 'tz パラメータ(例: "Asia/Tokyo")指定時、times がローカル時刻に変換され、labels("03/16 17:00" 等の短縮表示文字列)も付加される。\n\n' +
270
+ 'SVG/PNG ファイル保存 → render_chart_svg。指標の最新値やトレンド判定 → analyze_indicators。',
271
+ inputSchema: PrepareChartDataInputSchema,
272
+ handler: async ({
273
+ pair,
274
+ type,
275
+ limit,
276
+ indicators,
277
+ tz,
278
+ }: {
279
+ pair?: string;
280
+ type?: string;
281
+ limit?: number;
282
+ indicators?: string[];
283
+ tz?: string;
284
+ }) => {
285
+ const result = await prepareChartData(pair ?? 'btc_jpy', type ?? '1day', limit ?? 30, indicators, tz);
286
+ if (!result.ok) return result;
287
+ // LLM は structuredContent を参照できないため、content テキストにデータを含める
288
+ const text = `${result.summary}\n${JSON.stringify(result.data)}`;
289
+ return {
290
+ content: [{ type: 'text', text }],
291
+ structuredContent: toStructured(result),
292
+ };
293
+ },
294
+ };
@@ -0,0 +1,189 @@
1
+ /**
2
+ * prepare_depth_data — Visualizer / 板深度チャート描画用の累積データを返す。
3
+ *
4
+ * getDepth(/depth API)を呼び出し、price × cumulative volume の階段配列として
5
+ * コンパクトに整形する。render_depth_svg と同じ累積計算ロジック(lib/depth-analysis)
6
+ * を共有する。
7
+ *
8
+ * クライアント側(Claude.ai Visualizer 等)で描画可能な場合はこのツールを優先。
9
+ * ファイル保存が必要な場合は render_depth_svg を使用すること。
10
+ */
11
+
12
+ import { toIsoTime } from '../lib/datetime.js';
13
+ import { buildCumulativeSteps } from '../lib/depth-analysis.js';
14
+ import getDepth from '../lib/get-depth.js';
15
+ import { fail, failFromError, failFromValidation, ok, toStructured } from '../lib/result.js';
16
+ import { createMeta, ensurePair } from '../lib/validate.js';
17
+ import type { FailResult, OkResult, Pair } from '../src/schemas.js';
18
+ import { PrepareDepthDataInputSchema } from '../src/schemas.js';
19
+ import type { ToolDefinition } from '../src/tool-definition.js';
20
+
21
+ /** 価格を「JPY ペアなら整数、それ以外は小数2桁」で丸める */
22
+ function roundPrice(p: number, jpyPair: boolean): number {
23
+ return jpyPair ? Math.round(p) : Number(p.toFixed(2));
24
+ }
25
+
26
+ /** Volume は固定小数桁に丸める */
27
+ function roundVolume(v: number): number {
28
+ return Number(v.toFixed(6));
29
+ }
30
+
31
+ interface PrepareDepthDataResult {
32
+ /** bids: 高価格 → 低価格 の [price, cumulativeVolume] 階段データ */
33
+ bids: Array<[number, number]>;
34
+ /** asks: 低価格 → 高価格 の [price, cumulativeVolume] 階段データ */
35
+ asks: Array<[number, number]>;
36
+ bestBid: number | null;
37
+ bestAsk: number | null;
38
+ /** (bestBid + bestAsk) / 2 */
39
+ mid: number | null;
40
+ /** bestAsk - bestBid */
41
+ spread: number | null;
42
+ /** spread / mid */
43
+ spreadPct: number | null;
44
+ /** 買い板全体の累積量 */
45
+ totalBidVolume: number;
46
+ /** 売り板全体の累積量 */
47
+ totalAskVolume: number;
48
+ /** mid を中心とする ±bandPct% 範囲の買い/売り量と比率 */
49
+ band: {
50
+ pct: number;
51
+ bidVolume: number;
52
+ askVolume: number;
53
+ ratio: number | null;
54
+ };
55
+ /** 板取得時刻(Unix ms) */
56
+ timestamp: number;
57
+ /** 板取得時刻(ISO8601, UTC) */
58
+ isoTime: string | null;
59
+ }
60
+
61
+ interface PrepareDepthDataMeta {
62
+ pair: Pair;
63
+ fetchedAt: string;
64
+ levels: { bids: number; asks: number };
65
+ /** 出来高の単位(ペアのベース通貨。例: btc_jpy → "BTC") */
66
+ volumeUnit: string;
67
+ }
68
+
69
+ export interface PrepareDepthDataParams {
70
+ pair?: string;
71
+ /** 取得する最大レベル数(片側) */
72
+ levels?: number;
73
+ /** mid を中心とした band 比率(0.01 = ±1%) */
74
+ bandPct?: number;
75
+ }
76
+
77
+ export default async function prepareDepthData(
78
+ params: PrepareDepthDataParams = {},
79
+ ): Promise<OkResult<PrepareDepthDataResult, PrepareDepthDataMeta> | FailResult> {
80
+ const { pair = 'btc_jpy', levels = 200, bandPct = 0.01 } = params;
81
+
82
+ const chk = ensurePair(pair);
83
+ if (!chk.ok) return failFromValidation(chk);
84
+
85
+ const maxLevels = Math.max(10, Math.min(1000, Math.floor(levels)));
86
+ const jpyPair = chk.pair.endsWith('_jpy');
87
+
88
+ try {
89
+ const depth = await getDepth(chk.pair, { maxLevels });
90
+ if (!depth.ok) return fail(depth.summary.replace(/^Error: /, ''), depth.meta?.errorType || 'internal');
91
+
92
+ const asksRaw: Array<[string, string]> = depth.data.asks || [];
93
+ const bidsRaw: Array<[string, string]> = depth.data.bids || [];
94
+
95
+ if (!asksRaw.length || !bidsRaw.length) {
96
+ return fail('板データが不足しています(asks/bids の両方が必要です)', 'upstream');
97
+ }
98
+
99
+ const bidsNum = bidsRaw.map(([p, s]) => [Number(p), Number(s)] as [number, number]);
100
+ const asksNum = asksRaw.map(([p, s]) => [Number(p), Number(s)] as [number, number]);
101
+
102
+ const bidStepsRaw = buildCumulativeSteps(bidsNum, 'bid');
103
+ const askStepsRaw = buildCumulativeSteps(asksNum, 'ask');
104
+
105
+ const bidSteps: Array<[number, number]> = bidStepsRaw.map(([p, q]) => [roundPrice(p, jpyPair), roundVolume(q)]);
106
+ const askSteps: Array<[number, number]> = askStepsRaw.map(([p, q]) => [roundPrice(p, jpyPair), roundVolume(q)]);
107
+
108
+ const bestBid = bidStepsRaw[0]?.[0] ?? null;
109
+ const bestAsk = askStepsRaw[0]?.[0] ?? null;
110
+ const mid = bestBid != null && bestAsk != null ? (bestBid + bestAsk) / 2 : null;
111
+ const spread = bestBid != null && bestAsk != null ? bestAsk - bestBid : null;
112
+ const spreadPct = mid != null && spread != null && mid !== 0 ? Number((spread / mid).toFixed(6)) : null;
113
+
114
+ const totalBidVolume = bidStepsRaw.at(-1)?.[1] ?? 0;
115
+ const totalAskVolume = askStepsRaw.at(-1)?.[1] ?? 0;
116
+
117
+ // mid を中心とした band 集計
118
+ let bandBidVol = 0;
119
+ let bandAskVol = 0;
120
+ if (mid != null) {
121
+ const bidFloor = mid * (1 - bandPct);
122
+ const askCeil = mid * (1 + bandPct);
123
+ for (const [p, s] of bidsNum) if (p >= bidFloor && p <= mid) bandBidVol += s;
124
+ for (const [p, s] of asksNum) if (p >= mid && p <= askCeil) bandAskVol += s;
125
+ }
126
+ const bandRatio = bandAskVol > 0 ? Number((bandBidVol / bandAskVol).toFixed(4)) : null;
127
+
128
+ const timestamp = Number(depth.data.timestamp ?? Date.now());
129
+
130
+ const data: PrepareDepthDataResult = {
131
+ bids: bidSteps,
132
+ asks: askSteps,
133
+ bestBid: bestBid != null ? roundPrice(bestBid, jpyPair) : null,
134
+ bestAsk: bestAsk != null ? roundPrice(bestAsk, jpyPair) : null,
135
+ mid: mid != null ? roundPrice(mid, jpyPair) : null,
136
+ spread: spread != null ? roundPrice(spread, jpyPair) : null,
137
+ spreadPct,
138
+ totalBidVolume: roundVolume(totalBidVolume),
139
+ totalAskVolume: roundVolume(totalAskVolume),
140
+ band: {
141
+ pct: bandPct,
142
+ bidVolume: roundVolume(bandBidVol),
143
+ askVolume: roundVolume(bandAskVol),
144
+ ratio: bandRatio,
145
+ },
146
+ timestamp,
147
+ isoTime: toIsoTime(timestamp),
148
+ };
149
+
150
+ const volumeUnit = chk.pair.split('_')[0].toUpperCase();
151
+ const meta: PrepareDepthDataMeta = {
152
+ ...createMeta(chk.pair),
153
+ levels: { bids: bidSteps.length, asks: askSteps.length },
154
+ volumeUnit,
155
+ } as PrepareDepthDataMeta;
156
+
157
+ const ratioText = bandRatio == null ? 'n/a' : bandRatio.toFixed(2);
158
+ const summary = `${chk.pair} depth data (bids: ${bidSteps.length}, asks: ${askSteps.length}, mid: ${data.mid ?? 'n/a'}, ±${(bandPct * 100).toFixed(2)}% ratio: ${ratioText})`;
159
+ return ok<PrepareDepthDataResult, PrepareDepthDataMeta>(summary, data, meta);
160
+ } catch (err: unknown) {
161
+ return failFromError(err, { defaultMessage: '板深度データの整形に失敗しました' });
162
+ }
163
+ }
164
+
165
+ export type { PrepareDepthDataMeta, PrepareDepthDataResult };
166
+
167
+ export const toolDef: ToolDefinition = {
168
+ name: 'prepare_depth_data',
169
+ description:
170
+ '[Depth Chart / Order Book / Visualization] 板の深度チャート描画の第一選択ツール。\n\n' +
171
+ 'getDepth(/depth API)を呼び出し、累積 volume の階段配列 ([price, cumulativeVolume][]) として返す。\n' +
172
+ 'Claude.ai の Visualizer 等クライアント側で描画可能な場合はこのツールを優先。\n' +
173
+ 'ファイル保存(SVG/PNG)が必要な場合は render_depth_svg を使用。\n\n' +
174
+ 'レスポンス形式: { bids: [[price, cumVolume], ...], asks: [[price, cumVolume], ...], bestBid, bestAsk, mid, spread, spreadPct, totalBidVolume, totalAskVolume, band: {pct, bidVolume, askVolume, ratio}, timestamp, isoTime }\n' +
175
+ '- bids は価格降順、asks は価格昇順\n' +
176
+ '- band は mid を中心とした ±bandPct 範囲の買い/売り量と比率(ratio = bidVolume / askVolume)\n' +
177
+ '- JPY ペアの価格は整数に丸め済み',
178
+ inputSchema: PrepareDepthDataInputSchema,
179
+ handler: async ({ pair, levels, bandPct }: { pair?: string; levels?: number; bandPct?: number }) => {
180
+ const result = await prepareDepthData({ pair, levels, bandPct });
181
+ if (!result.ok) return result;
182
+ // LLM は structuredContent を参照できないため、content テキストにデータを含める
183
+ const text = `${result.summary}\n${JSON.stringify(result.data)}`;
184
+ return {
185
+ content: [{ type: 'text', text }],
186
+ structuredContent: toStructured(result),
187
+ };
188
+ },
189
+ };
@@ -0,0 +1,21 @@
1
+ /**
2
+ * analyze_my_portfolio — ポートフォリオ分析ツール(Phase 3 + Phase 4 拡張)。
3
+ *
4
+ * 保有資産・約定履歴・入出金履歴・テクニカル分析を統合し、
5
+ * 損益状況とポートフォリオ全体の評価を LLM に提供する。
6
+ * 入出金データがあれば口座全体のリターンを概算する。
7
+ */
8
+
9
+ import analyzeMyPortfolioHandler from '../../src/handlers/analyzeMyPortfolioHandler.js';
10
+ import { AnalyzeMyPortfolioInputSchema } from '../../src/private/schemas.js';
11
+ import type { ToolDefinition } from '../../src/tool-definition.js';
12
+
13
+ // ── MCP ツール定義(tool-registry から自動収集) ──
14
+ export const toolDef: ToolDefinition = {
15
+ name: 'analyze_my_portfolio',
16
+ description:
17
+ '[Portfolio Analysis / PnL] 自分のポートフォリオ分析(portfolio / pnl / balance / return)。保有資産の評価損益・実現損益・口座リターンを一括算出。テクニカル分析統合オプション付き。Private API(要APIキー設定)。',
18
+ inputSchema: AnalyzeMyPortfolioInputSchema,
19
+ handler: async (args: { include_technical?: boolean; include_pnl?: boolean; include_deposit_withdrawal?: boolean }) =>
20
+ analyzeMyPortfolioHandler(args),
21
+ };
@@ -0,0 +1,127 @@
1
+ /**
2
+ * cancel_order — 注文をキャンセルする Private API ツール。
3
+ *
4
+ * bitbank Private API `POST /v1/user/spot/cancel_order` を呼び出し、
5
+ * 指定した注文IDの注文をキャンセルする。
6
+ *
7
+ * エラーケース:
8
+ * - 50009: 注文が見つからない
9
+ * - 50010: キャンセル不可(既にキャンセル・約定済みなど)
10
+ * - 50026: 既にキャンセル済み
11
+ * - 50027: 既に約定済み
12
+ */
13
+
14
+ import { nowIso, toIsoMs } from '../../lib/datetime.js';
15
+ import { formatPair, formatPrice } from '../../lib/formatter.js';
16
+ import { logTradeAction } from '../../lib/logger.js';
17
+ import { fail, ok, toStructured } from '../../lib/result.js';
18
+ import { getDefaultClient, PrivateApiError } from '../../src/private/client.js';
19
+ import { validateToken } from '../../src/private/confirmation.js';
20
+ import type { OrderResponse } from '../../src/private/schemas.js';
21
+ import { CancelOrderInputSchema, CancelOrderOutputSchema } from '../../src/private/schemas.js';
22
+ import type { ToolDefinition } from '../../src/tool-definition.js';
23
+
24
+ export default async function cancelOrder(
25
+ args: {
26
+ pair: string;
27
+ order_id: number;
28
+ confirmation_token: string;
29
+ token_expires_at: number;
30
+ },
31
+ route: 'elicitation' | 'ui-button' | 'direct-text' = 'direct-text',
32
+ ) {
33
+ const { pair, order_id, confirmation_token, token_expires_at } = args;
34
+
35
+ // HITL: 確認トークンの検証
36
+ const tokenError = validateToken(confirmation_token, 'cancel_order', { pair, order_id }, token_expires_at);
37
+ if (tokenError) {
38
+ return CancelOrderOutputSchema.parse(fail(tokenError.message, tokenError.code));
39
+ }
40
+
41
+ const client = getDefaultClient();
42
+
43
+ try {
44
+ const rawOrder = await client.post<OrderResponse>('/v1/user/spot/cancel_order', {
45
+ pair,
46
+ order_id,
47
+ });
48
+
49
+ const timestamp = nowIso();
50
+ const isJpy = pair.includes('jpy');
51
+ const sideLabel = rawOrder.side === 'buy' ? '買' : '売';
52
+ const price = rawOrder.price ? (isJpy ? formatPrice(Number(rawOrder.price)) : rawOrder.price) : '成行';
53
+ const amount = rawOrder.start_amount ?? rawOrder.executed_amount;
54
+
55
+ const lines: string[] = [];
56
+ lines.push(`注文キャンセル完了: ${formatPair(pair)}`);
57
+ lines.push(` 注文ID: ${order_id}`);
58
+ lines.push(` ${sideLabel} ${rawOrder.type} ${amount} @ ${price}`);
59
+ lines.push(` ステータス: ${rawOrder.status}`);
60
+ if (rawOrder.executed_amount && rawOrder.executed_amount !== '0') {
61
+ lines.push(` 約定済み数量: ${rawOrder.executed_amount}`);
62
+ }
63
+ lines.push(
64
+ ` キャンセル日時: ${rawOrder.canceled_at ? (toIsoMs(rawOrder.canceled_at) ?? String(rawOrder.canceled_at)) : timestamp}`,
65
+ );
66
+
67
+ const summary = lines.join('\n');
68
+
69
+ logTradeAction({
70
+ type: 'cancel_order',
71
+ orderId: order_id,
72
+ pair,
73
+ side: rawOrder.side,
74
+ status: rawOrder.status,
75
+ confirmed: true,
76
+ route,
77
+ });
78
+
79
+ return CancelOrderOutputSchema.parse(
80
+ ok(
81
+ summary,
82
+ { order: rawOrder, timestamp },
83
+ {
84
+ fetchedAt: timestamp,
85
+ orderId: order_id,
86
+ pair,
87
+ ...(client.lastRateLimit ? { rateLimit: client.lastRateLimit } : {}),
88
+ },
89
+ ),
90
+ );
91
+ } catch (err) {
92
+ if (err instanceof PrivateApiError) {
93
+ // キャンセル固有エラーの補足メッセージ
94
+ const codeMessages: Record<number, string> = {
95
+ 50009: '指定された注文が見つかりません(3ヶ月以上前の注文は参照不可)',
96
+ 50010: 'この注文はキャンセルできません',
97
+ 50026: 'この注文は既にキャンセル済みです',
98
+ 50027: 'この注文は既に約定済みです',
99
+ };
100
+ const msg = (err.bitbankCode && codeMessages[err.bitbankCode]) || err.message;
101
+ return CancelOrderOutputSchema.parse(fail(msg, err.errorType));
102
+ }
103
+ return CancelOrderOutputSchema.parse(
104
+ fail(err instanceof Error ? err.message : '注文キャンセル中に予期しないエラーが発生しました', 'upstream_error'),
105
+ );
106
+ }
107
+ }
108
+
109
+ export const toolDef: ToolDefinition = {
110
+ name: 'cancel_order',
111
+ description:
112
+ '[Cancel Order] 指定した注文IDの注文をキャンセルする。キャンセル後の注文情報を返す。Private API。' +
113
+ ' ⚠️ 事前に preview_cancel_order で確認トークンを取得し、confirmation_token と token_expires_at を渡すこと。' +
114
+ ' トークンなしの直接呼び出しは拒否される。',
115
+ inputSchema: CancelOrderInputSchema,
116
+ handler: async (args) => {
117
+ const result = await cancelOrder(
118
+ args as { pair: string; order_id: number; confirmation_token: string; token_expires_at: number },
119
+ );
120
+ if (!result.ok) return result;
121
+ const text = `${result.summary}\n${JSON.stringify(result.data, null, 2)}`;
122
+ return {
123
+ content: [{ type: 'text', text }],
124
+ structuredContent: toStructured(result),
125
+ };
126
+ },
127
+ };