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,487 @@
1
+ import { dayjs, daysAgo, today, toIsoTime, toIsoWithTz } from '../lib/datetime.js';
2
+ import { getErrorMessage } from '../lib/error.js';
3
+ import { formatSummary } from '../lib/formatter.js';
4
+ import { BITBANK_API_BASE, DEFAULT_RETRIES, fetchJsonWithRateLimit, type RateLimitInfo } from '../lib/http.js';
5
+ import { fail, failFromError, failFromValidation, ok, parseAsResult, toStructured } from '../lib/result.js';
6
+ import { createMeta, ensurePair, validateDate, validateLimit } from '../lib/validate.js';
7
+ import type { CandleType, FailResult, GetCandlesData, GetCandlesMeta, OkResult } from '../src/schemas.js';
8
+ import { GetCandlesInputSchema, GetCandlesOutputSchema } from '../src/schemas.js';
9
+ import type { ToolDefinition } from '../src/tool-definition.js';
10
+
11
+ const TYPES: Set<CandleType | string> = new Set([
12
+ '1min',
13
+ '5min',
14
+ '15min',
15
+ '30min',
16
+ '1hour',
17
+ '4hour',
18
+ '8hour',
19
+ '12hour',
20
+ '1day',
21
+ '1week',
22
+ '1month',
23
+ ]);
24
+
25
+ // 年単位でリクエストする時間足(YYYY形式)
26
+ const YEARLY_TYPES: Set<string> = new Set(['4hour', '8hour', '12hour', '1day', '1week', '1month']);
27
+
28
+ // 日単位でリクエストする時間足(YYYYMMDD形式)
29
+ const DAILY_TYPES: Set<string> = new Set(['1min', '5min', '15min', '30min', '1hour']);
30
+
31
+ // 時間足ごとの年間本数(複数年取得時の計算用)
32
+ const BARS_PER_YEAR: Record<string, number> = {
33
+ '1month': 12,
34
+ '1week': 52,
35
+ '1day': 365,
36
+ '12hour': 730,
37
+ '8hour': 1095,
38
+ '4hour': 2190,
39
+ };
40
+
41
+ // 時間足ごとの1日あたりの本数
42
+ const BARS_PER_DAY: Record<string, number> = {
43
+ '1min': 1440,
44
+ '5min': 288,
45
+ '15min': 96,
46
+ '30min': 48,
47
+ '1hour': 24,
48
+ };
49
+
50
+ function todayYyyymmdd(): string {
51
+ return today('YYYYMMDD');
52
+ }
53
+
54
+ type OhlcvRow = [unknown, unknown, unknown, unknown, unknown, unknown];
55
+ interface FetchChunkResult {
56
+ rows: OhlcvRow[];
57
+ rateLimit: RateLimitInfo | null;
58
+ error?: unknown;
59
+ }
60
+
61
+ // 単一年のデータを取得する内部関数
62
+ async function fetchSingleYear(pair: string, type: string, year: number): Promise<FetchChunkResult> {
63
+ const url = `${BITBANK_API_BASE}/${pair}/candlestick/${type}/${year}`;
64
+ try {
65
+ const { data: json, rateLimit } = await fetchJsonWithRateLimit(url, { timeoutMs: 8000, retries: DEFAULT_RETRIES });
66
+ const jsonObj = json as { data?: { candlestick?: Array<{ ohlcv?: unknown[] }> } };
67
+ const cs = jsonObj?.data?.candlestick?.[0];
68
+ const ohlcvs = cs?.ohlcv ?? [];
69
+ return { rows: ohlcvs as OhlcvRow[], rateLimit };
70
+ } catch (e) {
71
+ // 存在しない年や取得失敗は空配列を返す(エラーも保持)
72
+ return { rows: [], rateLimit: null, error: e };
73
+ }
74
+ }
75
+
76
+ // 単一日のデータを取得する内部関数
77
+ async function fetchSingleDay(
78
+ pair: string,
79
+ type: string,
80
+ dateStr: string, // YYYYMMDD形式
81
+ ): Promise<FetchChunkResult> {
82
+ const url = `${BITBANK_API_BASE}/${pair}/candlestick/${type}/${dateStr}`;
83
+ try {
84
+ const { data: json, rateLimit } = await fetchJsonWithRateLimit(url, { timeoutMs: 8000, retries: DEFAULT_RETRIES });
85
+ const jsonObj = json as { data?: { candlestick?: Array<{ ohlcv?: unknown[] }> } };
86
+ const cs = jsonObj?.data?.candlestick?.[0];
87
+ const ohlcvs = cs?.ohlcv ?? [];
88
+ return { rows: ohlcvs as OhlcvRow[], rateLimit };
89
+ } catch (e) {
90
+ // 存在しない日や取得失敗は空配列を返す(エラーも保持)
91
+ return { rows: [], rateLimit: null, error: e };
92
+ }
93
+ }
94
+
95
+ // N日前の日付をYYYYMMDD形式で取得
96
+ function _getDateNDaysAgo(n: number): string {
97
+ return daysAgo(n, 'YYYYMMDD');
98
+ }
99
+
100
+ export default async function getCandles(
101
+ pair: string,
102
+ type: CandleType | string = '1day',
103
+ date: string | undefined = todayYyyymmdd(),
104
+ limit: number = 200,
105
+ tz: string = 'Asia/Tokyo',
106
+ ): Promise<OkResult<GetCandlesData, GetCandlesMeta> | FailResult> {
107
+ const chk = ensurePair(pair);
108
+ if (!chk.ok) return failFromValidation(chk);
109
+
110
+ if (!TYPES.has(type)) {
111
+ return fail(`type は ${[...TYPES].join(', ')} から選択してください(指定値: ${String(type)})`, 'user');
112
+ }
113
+
114
+ const effectiveDate = date ?? todayYyyymmdd();
115
+ const dateCheck = validateDate(effectiveDate, String(type));
116
+ if (!dateCheck.ok) return failFromValidation(dateCheck);
117
+
118
+ // 複数年取得が必要かどうかを判定
119
+ const isYearlyType = YEARLY_TYPES.has(type);
120
+ const isDailyType = DAILY_TYPES.has(type);
121
+ const barsPerYear = BARS_PER_YEAR[type] || 365;
122
+ const barsPerDay = BARS_PER_DAY[type] || 24;
123
+
124
+ // 年初付近では今年のデータだけでは足りない場合があるため、
125
+ // 今年の経過日数も考慮して必要年数を計算
126
+ const now = dayjs();
127
+ const startOfYear = now.startOf('year');
128
+ const dayOfYear = now.diff(startOfYear, 'day') + 1;
129
+ const estimatedBarsThisYear = Math.floor(dayOfYear * (barsPerYear / 365));
130
+
131
+ // limit が今年の推定本数を超える場合、または単純計算で複数年が必要な場合
132
+ const yearsNeeded = isYearlyType
133
+ ? Math.max(Math.ceil(limit / barsPerYear), limit > estimatedBarsThisYear ? 2 : 1)
134
+ : 1;
135
+ const needsMultiYear = isYearlyType && yearsNeeded > 1;
136
+
137
+ // 日単位タイプの場合、複数日取得が必要かどうかを判定
138
+ const daysNeeded = isDailyType ? Math.ceil(limit / barsPerDay) + 1 : 1; // +1 for buffer
139
+ const needsMultiDay = isDailyType && daysNeeded > 1;
140
+
141
+ // 複数年/複数日取得の場合は上限を緩和
142
+ const maxLimit = needsMultiYear ? 5000 : needsMultiDay ? 10000 : 1000;
143
+ const limitCheck = validateLimit(limit, 1, maxLimit);
144
+ if (!limitCheck.ok) return failFromValidation(limitCheck);
145
+
146
+ let ohlcvs: unknown[] = [];
147
+ let json: unknown = null;
148
+ let fetchWarning: string | undefined;
149
+ let lastRateLimit: RateLimitInfo | null = null;
150
+
151
+ try {
152
+ if (needsMultiYear) {
153
+ // 複数年の並列取得
154
+ const currentYear = dayjs().year();
155
+ const years = Array.from({ length: yearsNeeded }, (_, i) => currentYear - i);
156
+
157
+ const results = await Promise.all(years.map((year) => fetchSingleYear(chk.pair, type, year)));
158
+ // 最後に成功したレスポンスの rateLimit を採用
159
+ for (const r of results) {
160
+ if (r.rateLimit) lastRateLimit = r.rateLimit;
161
+ }
162
+
163
+ // 部分失敗を追跡
164
+ const failedChunks = results.filter((r) => r.error);
165
+ const totalChunks = results.length;
166
+
167
+ // 古い年順にマージ(時系列順)
168
+ const allOhlcvs: OhlcvRow[] = [];
169
+ for (let i = results.length - 1; i >= 0; i--) {
170
+ allOhlcvs.push(...results[i].rows);
171
+ }
172
+
173
+ // 全チャンクがエラーの場合はネットワークエラーとして伝播
174
+ if (allOhlcvs.length === 0) {
175
+ const firstError = results.find((r) => r.error);
176
+ if (firstError?.error) throw firstError.error;
177
+ }
178
+
179
+ // 過半数失敗なら信頼性が低いため fail
180
+ if (failedChunks.length > 0 && failedChunks.length >= totalChunks / 2) {
181
+ return fail(
182
+ `ローソク足取得の過半数が失敗しました(${totalChunks}年中${failedChunks.length}年失敗)`,
183
+ 'upstream',
184
+ );
185
+ }
186
+ if (failedChunks.length > 0) {
187
+ const failedYears = years.filter((_, i) => results[i].error);
188
+ fetchWarning = `⚠️ ${totalChunks}年中${failedChunks.length}年の取得に失敗しました(${failedYears.join(', ')}年)。データが不完全な可能性があります。`;
189
+ }
190
+
191
+ // タイムスタンプでソート(念のため)
192
+ allOhlcvs.sort((a, b) => {
193
+ const tsA = Number(a[5]) || 0;
194
+ const tsB = Number(b[5]) || 0;
195
+ return tsA - tsB;
196
+ });
197
+
198
+ ohlcvs = allOhlcvs;
199
+ json = { data: { candlestick: [{ ohlcv: ohlcvs }] }, _multiYear: { years, totalFetched: ohlcvs.length } };
200
+ } else if (needsMultiDay) {
201
+ // 複数日の並列取得(1hour, 30min, etc.)
202
+ // 最大同時リクエスト数を制限(API負荷対策)
203
+ // bitbank API: レート制限があるため、控えめな設定に
204
+ // 3並列 + バッチ間500ms遅延 → 約6リクエスト/秒
205
+ const maxConcurrent = 3;
206
+ const batchDelayMs = 500;
207
+ const baseDate = dayjs(dateCheck.value, 'YYYYMMDD');
208
+ const dates = Array.from({ length: daysNeeded }, (_, i) => baseDate.subtract(i, 'day').format('YYYYMMDD'));
209
+
210
+ const allOhlcvs: OhlcvRow[] = [];
211
+ const allDayResults: FetchChunkResult[] = [];
212
+
213
+ // バッチ処理で並列取得(バッチ間に遅延を入れる)
214
+ for (let i = 0; i < dates.length; i += maxConcurrent) {
215
+ if (i > 0) {
216
+ // バッチ間の遅延(レート制限対策)
217
+ await new Promise((resolve) => setTimeout(resolve, batchDelayMs));
218
+ }
219
+ const batch = dates.slice(i, i + maxConcurrent);
220
+ const results = await Promise.all(batch.map((dateStr) => fetchSingleDay(chk.pair, type, dateStr)));
221
+ for (const result of results) {
222
+ allOhlcvs.push(...result.rows);
223
+ allDayResults.push(result);
224
+ if (result.rateLimit) lastRateLimit = result.rateLimit;
225
+ }
226
+ }
227
+
228
+ // 部分失敗を追跡
229
+ const failedDays = allDayResults.filter((r) => r.error);
230
+ const totalDays = allDayResults.length;
231
+
232
+ // 全チャンクがエラーの場合はネットワークエラーとして伝播
233
+ if (allOhlcvs.length === 0) {
234
+ const firstError = allDayResults.find((r) => r.error);
235
+ if (firstError?.error) throw firstError.error;
236
+ }
237
+
238
+ // 過半数失敗なら信頼性が低いため fail
239
+ if (failedDays.length > 0 && failedDays.length >= totalDays / 2) {
240
+ return fail(`ローソク足取得の過半数が失敗しました(${totalDays}日中${failedDays.length}日失敗)`, 'upstream');
241
+ }
242
+ if (failedDays.length > 0) {
243
+ fetchWarning = `⚠️ ${totalDays}日中${failedDays.length}日の取得に失敗しました。データが不完全な可能性があります。`;
244
+ }
245
+
246
+ // タイムスタンプでソート(古い順)
247
+ allOhlcvs.sort((a, b) => {
248
+ const tsA = Number(a[5]) || 0;
249
+ const tsB = Number(b[5]) || 0;
250
+ return tsA - tsB;
251
+ });
252
+
253
+ ohlcvs = allOhlcvs;
254
+ json = {
255
+ data: { candlestick: [{ ohlcv: ohlcvs }] },
256
+ _multiDay: { daysRequested: daysNeeded, totalFetched: ohlcvs.length },
257
+ };
258
+ } else {
259
+ // 従来の単一リクエスト
260
+ const url = `${BITBANK_API_BASE}/${chk.pair}/candlestick/${type}/${dateCheck.value}`;
261
+ const fetchResult = await fetchJsonWithRateLimit(url, { timeoutMs: 5000, retries: DEFAULT_RETRIES });
262
+ json = fetchResult.data;
263
+ lastRateLimit = fetchResult.rateLimit;
264
+ const jsonObj = json as { data?: { candlestick?: Array<{ ohlcv?: unknown[] }> } };
265
+ const cs = jsonObj?.data?.candlestick?.[0];
266
+ ohlcvs = cs?.ohlcv ?? [];
267
+ }
268
+
269
+ if (ohlcvs.length === 0) {
270
+ return fail(`ローソク足データが見つかりません (${chk.pair} / ${type} / ${dateCheck.value})`, 'user');
271
+ }
272
+
273
+ const rows = ohlcvs.slice(-limitCheck.value) as Array<[unknown, unknown, unknown, unknown, unknown, unknown]>;
274
+
275
+ // volume (v): base 通貨建ての合算取引量(買い+売り区別なし)
276
+ // bitbank /candlestick API の OHLCV[4] をそのまま使用
277
+ const useTz = typeof tz === 'string' && tz.length > 0;
278
+ const normalized = rows.map(([o, h, l, c, v, ts]) => ({
279
+ open: Number(o),
280
+ high: Number(h),
281
+ low: Number(l),
282
+ close: Number(c),
283
+ volume: Number(v),
284
+ isoTime: toIsoTime(ts) ?? undefined,
285
+ ...(useTz ? { isoTimeLocal: toIsoWithTz(Number(ts), tz) ?? undefined } : {}),
286
+ }));
287
+
288
+ // 期間別のキーポイントを抽出
289
+ const totalItems = normalized.length;
290
+ const today = normalized[totalItems - 1];
291
+ const sevenDaysAgo = totalItems >= 8 ? normalized[totalItems - 1 - 7] : null;
292
+ const thirtyDaysAgo = totalItems >= 31 ? normalized[totalItems - 1 - 30] : null;
293
+ const ninetyDaysAgo = totalItems >= 91 ? normalized[totalItems - 1 - 90] : totalItems > 0 ? normalized[0] : null;
294
+
295
+ // 変化率を計算
296
+ const calcChange = (from: number | undefined, to: number | undefined) => {
297
+ if (!from || !to) return null;
298
+ return ((to - from) / from) * 100;
299
+ };
300
+
301
+ // 出来高情報を計算
302
+ const calcVolumeStats = () => {
303
+ if (totalItems < 14) return null;
304
+
305
+ // 直近7日間の平均出来高
306
+ const recent7Days = normalized.slice(totalItems - 7, totalItems);
307
+ const recent7DaysAvg = recent7Days.reduce((sum, c) => sum + c.volume, 0) / 7;
308
+
309
+ // その前7日間(8〜14日前)の平均出来高
310
+ const previous7Days = normalized.slice(totalItems - 14, totalItems - 7);
311
+ const previous7DaysAvg = previous7Days.reduce((sum, c) => sum + c.volume, 0) / 7;
312
+
313
+ // 過去30日間の平均出来高(データが30本以上ある場合)
314
+ let last30DaysAvg: number | null = null;
315
+ if (totalItems >= 30) {
316
+ const last30 = normalized.slice(totalItems - 30, totalItems);
317
+ last30DaysAvg = last30.reduce((sum, c) => sum + c.volume, 0) / last30.length;
318
+ }
319
+
320
+ // 変化率(直近7日 vs その前7日)
321
+ const volumeChangePct = ((recent7DaysAvg - previous7DaysAvg) / previous7DaysAvg) * 100;
322
+
323
+ // 判定
324
+ let judgment = 'ほぼ変わりません';
325
+ if (volumeChangePct > 20) judgment = '活発になっています';
326
+ else if (volumeChangePct < -20) judgment = '落ち着いています';
327
+
328
+ return {
329
+ recent7DaysAvg: Number(recent7DaysAvg.toFixed(2)),
330
+ previous7DaysAvg: Number(previous7DaysAvg.toFixed(2)),
331
+ last30DaysAvg: last30DaysAvg != null ? Number(last30DaysAvg.toFixed(2)) : null,
332
+ changePct: Number(volumeChangePct.toFixed(1)),
333
+ judgment,
334
+ };
335
+ };
336
+
337
+ const volumeStats = calcVolumeStats();
338
+
339
+ const keyPoints = {
340
+ today: today
341
+ ? {
342
+ index: totalItems - 1,
343
+ date: today.isoTime?.split('T')[0] || null,
344
+ close: today.close,
345
+ }
346
+ : null,
347
+ sevenDaysAgo: sevenDaysAgo
348
+ ? {
349
+ index: totalItems - 1 - 7,
350
+ date: sevenDaysAgo.isoTime?.split('T')[0] || null,
351
+ close: sevenDaysAgo.close,
352
+ changePct: calcChange(sevenDaysAgo.close, today?.close),
353
+ }
354
+ : null,
355
+ thirtyDaysAgo: thirtyDaysAgo
356
+ ? {
357
+ index: totalItems - 1 - 30,
358
+ date: thirtyDaysAgo.isoTime?.split('T')[0] || null,
359
+ close: thirtyDaysAgo.close,
360
+ changePct: calcChange(thirtyDaysAgo.close, today?.close),
361
+ }
362
+ : null,
363
+ ninetyDaysAgo: ninetyDaysAgo
364
+ ? {
365
+ index: ninetyDaysAgo === normalized[0] ? 0 : totalItems - 1 - 90,
366
+ date: ninetyDaysAgo.isoTime?.split('T')[0] || null,
367
+ close: ninetyDaysAgo.close,
368
+ changePct: calcChange(ninetyDaysAgo.close, today?.close),
369
+ }
370
+ : null,
371
+ };
372
+
373
+ // 全件の価格範囲を計算
374
+ const priceRange =
375
+ normalized.length > 0
376
+ ? {
377
+ high: Math.max(...normalized.map((c) => c.high)),
378
+ low: Math.min(...normalized.map((c) => c.low)),
379
+ periodStart: normalized[0].isoTime?.split('T')[0] || '',
380
+ periodEnd: normalized[normalized.length - 1].isoTime?.split('T')[0] || '',
381
+ }
382
+ : undefined;
383
+
384
+ const baseSummary = formatSummary({
385
+ pair: chk.pair,
386
+ timeframe: String(type),
387
+ latest: normalized.at(-1)?.close,
388
+ totalItems,
389
+ keyPoints,
390
+ volumeStats,
391
+ priceRange,
392
+ });
393
+
394
+ // テキスト summary に全ローソク足データを含める
395
+ // (MCP クライアントが structuredContent.data を読めない場合に対応)
396
+ const baseCurrency = chk.pair.split('_')[0]?.toUpperCase() ?? '';
397
+ const candleLines = normalized.map((c, i: number) => {
398
+ const t =
399
+ (c as { isoTimeLocal?: string; isoTime?: string }).isoTimeLocal ||
400
+ (c.isoTime ? c.isoTime.replace(/\.000Z$/, 'Z') : '?');
401
+ return `[${i}] ${t} O:${c.open} H:${c.high} L:${c.low} C:${c.close} V:${c.volume}`;
402
+ });
403
+ const summary =
404
+ (fetchWarning ? `${fetchWarning}\n` : '') +
405
+ baseSummary +
406
+ `\n\n📋 全${normalized.length}件のOHLCV (volume=${baseCurrency}建て合算値):\n` +
407
+ candleLines.join('\n') +
408
+ `\n\n---\n📌 含まれるもの: OHLCV(volume=${baseCurrency}建て合算値)、価格レンジ、期間別変動率` +
409
+ `\n📌 含まれないもの: 出来高の売買内訳、板情報、ファンディングレート、個別約定` +
410
+ `\n📌 補完ツール: get_flow_metrics(売買内訳・CVD), get_transactions(個別約定), get_orderbook(板情報)`;
411
+
412
+ const metaExtra: Record<string, unknown> = { type, count: normalized.length };
413
+ if (lastRateLimit) metaExtra.rateLimit = lastRateLimit;
414
+ if (fetchWarning) metaExtra.warning = fetchWarning;
415
+ if (needsMultiYear) {
416
+ metaExtra.multiYear = {
417
+ yearsRequested: yearsNeeded,
418
+ totalFetched: ohlcvs.length,
419
+ limitApplied: limitCheck.value,
420
+ };
421
+ }
422
+
423
+ const result = ok<GetCandlesData, GetCandlesMeta>(
424
+ summary,
425
+ { raw: json, normalized, keyPoints, volumeStats } as GetCandlesData,
426
+ createMeta(chk.pair, metaExtra) as GetCandlesMeta,
427
+ );
428
+ return parseAsResult<GetCandlesData, GetCandlesMeta>(GetCandlesOutputSchema, result);
429
+ } catch (e: unknown) {
430
+ const rawMsg = getErrorMessage(e);
431
+ const t = String(type);
432
+ if (/404/.test(rawMsg) && ['4hour', '8hour', '12hour'].includes(t)) {
433
+ const hint = `${t} は YYYY 形式(例: 2025)が必要です。なお、現在この時間足がAPIで提供されていない可能性もあります。1hour または 1day での取得もお試しください。`;
434
+ return parseAsResult<GetCandlesData, GetCandlesMeta>(
435
+ GetCandlesOutputSchema,
436
+ fail(`HTTP 404 Not Found (${chk.pair}/${t}). ${hint}`, 'user'),
437
+ );
438
+ }
439
+ return failFromError(e, {
440
+ schema: GetCandlesOutputSchema,
441
+ defaultType: 'network',
442
+ defaultMessage: 'ネットワークエラー',
443
+ });
444
+ }
445
+ }
446
+
447
+ // ── MCP ツール定義(tool-registry から自動収集) ──
448
+ export const toolDef: ToolDefinition = {
449
+ name: 'get_candles',
450
+ description: `[Candles / OHLCV / Candlestick] ローソク足(candles / OHLCV / chart data)を取得。1min〜1monthの各時間足に対応。
451
+
452
+ 【重要】バックテストには run_backtest を使用(データ取得〜チャート描画を一括実行)。`,
453
+ inputSchema: GetCandlesInputSchema,
454
+ handler: async ({
455
+ pair,
456
+ type,
457
+ date,
458
+ limit,
459
+ view,
460
+ tz,
461
+ }: {
462
+ pair: string;
463
+ type: '1min' | '5min' | '15min' | '30min' | '1hour' | '4hour' | '8hour' | '12hour' | '1day' | '1week' | '1month';
464
+ date?: string;
465
+ limit?: number;
466
+ view?: 'full' | 'items';
467
+ tz?: string;
468
+ }) => {
469
+ const result = await getCandles(pair, type, date, limit, tz);
470
+ if (view === 'items') {
471
+ const items = result?.data?.normalized ?? [];
472
+ return {
473
+ content: [{ type: 'text', text: JSON.stringify(items, null, 2) }],
474
+ structuredContent: { items } as Record<string, unknown>,
475
+ };
476
+ }
477
+ try {
478
+ const items = Array.isArray(result?.data?.normalized) ? result.data.normalized : [];
479
+ const sample = items.slice(0, 5);
480
+ const header = String(result?.summary ?? `${String(pair).toUpperCase()} [${String(type)}]`);
481
+ const text = `${header}\nSample (first ${sample.length}/${items.length}):\n${JSON.stringify(sample, null, 2)}`;
482
+ return { content: [{ type: 'text', text }], structuredContent: toStructured(result) };
483
+ } catch {
484
+ return result;
485
+ }
486
+ },
487
+ };