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,240 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ import { TtlCache } from '../lib/cache.js';
4
+ import { toNum } from '../lib/conversions.js';
5
+ import { nowIso } from '../lib/datetime.js';
6
+ import { getErrorMessage } from '../lib/error.js';
7
+ import { BITBANK_API_BASE } from '../lib/http.js';
8
+ import { fail, ok } from '../lib/result.js';
9
+ import { ALLOWED_PAIRS } from '../lib/validate.js';
10
+ import { GetTickersJpyOutputSchema } from '../src/schemas.js';
11
+
12
+ type Item = {
13
+ pair: string;
14
+ sell: string;
15
+ buy: string;
16
+ high: string;
17
+ low: string;
18
+ open: string;
19
+ last: string;
20
+ vol: string;
21
+ timestamp: number;
22
+ };
23
+
24
+ // テキスト summary にティッカー全件を含める(LLM が structuredContent.data を読めない対策)
25
+ function buildTickerText(
26
+ baseSummary: string,
27
+ data: Array<Item & { change24h?: number | null; change24hPct?: number | null }>,
28
+ ): string {
29
+ const lines = data.map((t, i) => {
30
+ const chg = t.change24hPct != null ? ` chg:${t.change24hPct >= 0 ? '+' : ''}${t.change24hPct}%` : '';
31
+ return `[${i}] ${t.pair} last:${t.last} high:${t.high} low:${t.low} vol:${t.vol}${chg}`;
32
+ });
33
+ return (
34
+ baseSummary +
35
+ `\n\n📋 全${data.length}件のティッカー:\n` +
36
+ lines.join('\n') +
37
+ `\n\n---\n📌 含まれるもの: 現時点のスナップショット(last/high/low/vol/bid/ask/24h変動率)` +
38
+ `\n📌 含まれないもの: 価格の時系列推移、板の深度、個別約定履歴、テクニカル指標` +
39
+ `\n📌 補完ツール: get_candles(時系列OHLCV), get_orderbook(板情報), get_transactions(約定履歴), analyze_indicators(指標)`
40
+ );
41
+ }
42
+
43
+ const CACHE_KEY = 'tickers_jpy';
44
+ const tickerCache = new TtlCache<Item[]>({ ttlMs: 10_000, maxEntries: 1 });
45
+
46
+ // === Auto mode (official pairs sync) ===
47
+ let dynamicPairs: Set<string> | null = null;
48
+ let dynamicPairsFetchedAt = 0;
49
+ const PAIRS_TTL_MS = Number(process.env.BITBANK_PAIRS_TTL_MS ?? 15 * 60 * 1000); // 15min
50
+ type PairsMode = 'strict' | 'auto' | 'off';
51
+ function getPairsMode(): PairsMode {
52
+ // Back-compat: BITBANK_STRICT_PAIRS=0 → off
53
+ const strictEnv = String(process.env.BITBANK_STRICT_PAIRS ?? '1');
54
+ if (strictEnv === '0') return 'off';
55
+ const mode = String(process.env.BITBANK_PAIRS_MODE || 'strict').toLowerCase();
56
+ if (mode === 'auto') return 'auto';
57
+ if (mode === 'off') return 'off';
58
+ return 'strict';
59
+ }
60
+ async function fetchOfficialJpyPairs(timeoutMs: number, retries: number, retryWaitMs: number): Promise<Set<string>> {
61
+ const officialUrl = `${BITBANK_API_BASE}/tickers_jpy`;
62
+ let lastErr: unknown;
63
+ for (let i = 0; i <= retries; i++) {
64
+ const ctrl = new AbortController();
65
+ const t = setTimeout(() => ctrl.abort(), timeoutMs);
66
+ try {
67
+ const res = await fetch(officialUrl, { signal: ctrl.signal });
68
+ clearTimeout(t);
69
+ if (!res.ok) throw new Error(`HTTP ${res.status}`);
70
+ const raw = (await res.json()) as { success?: number; data?: Array<Record<string, unknown>> };
71
+ if (!raw || raw.success !== 1 || !Array.isArray(raw.data)) throw new Error('bad payload');
72
+ const set = new Set<string>();
73
+ for (const it of raw.data) {
74
+ const p = String(it?.pair || '').toLowerCase();
75
+ if (p.endsWith('_jpy')) set.add(p);
76
+ }
77
+ return set;
78
+ } catch (e: unknown) {
79
+ clearTimeout(t);
80
+ lastErr = e;
81
+ if (i < retries) await new Promise((r) => setTimeout(r, retryWaitMs));
82
+ }
83
+ }
84
+ throw lastErr ?? new Error('pairs fetch failed');
85
+ }
86
+ async function getFilterSet(
87
+ timeoutMs: number,
88
+ retries: number,
89
+ retryWaitMs: number,
90
+ ): Promise<{ mode: PairsMode; set: Set<string> | null; source: 'dynamic' | 'static' | 'off' }> {
91
+ const mode = getPairsMode();
92
+ if (mode === 'off') return { mode, set: null, source: 'off' };
93
+ if (mode === 'strict') return { mode, set: ALLOWED_PAIRS as Set<string>, source: 'static' };
94
+ // auto
95
+ const now = Date.now();
96
+ const stillFresh = dynamicPairs && now - dynamicPairsFetchedAt < PAIRS_TTL_MS;
97
+ if (!stillFresh) {
98
+ try {
99
+ dynamicPairs = await fetchOfficialJpyPairs(timeoutMs, retries, retryWaitMs);
100
+ dynamicPairsFetchedAt = now;
101
+ } catch {
102
+ // keep previous dynamic or fall back to static
103
+ if (!dynamicPairs || dynamicPairs.size === 0) {
104
+ return { mode, set: ALLOWED_PAIRS as Set<string>, source: 'static' };
105
+ }
106
+ }
107
+ }
108
+ return { mode, set: dynamicPairs!, source: 'dynamic' };
109
+ }
110
+ async function filterByMode(
111
+ items: Item[],
112
+ timeoutMs: number,
113
+ retries: number,
114
+ retryWaitMs: number,
115
+ ): Promise<{ data: Item[]; filterInfo: { mode: PairsMode; source: 'dynamic' | 'static' | 'off'; setSize: number } }> {
116
+ const { mode, set, source } = await getFilterSet(timeoutMs, retries, retryWaitMs);
117
+ if (!set) return { data: items, filterInfo: { mode, source, setSize: 0 } };
118
+ const out = items.filter((it) => set.has(String(it.pair).toLowerCase()));
119
+ return { data: out, filterInfo: { mode, source, setSize: set.size } };
120
+ }
121
+
122
+ export default async function getTickersJpy(opts?: { bypassCache?: boolean }) {
123
+ if (!opts?.bypassCache) {
124
+ const cached = tickerCache.get(CACHE_KEY);
125
+ if (cached) {
126
+ return GetTickersJpyOutputSchema.parse(
127
+ ok(buildTickerText('tickers_jpy (cache)', cached), cached, {
128
+ cache: { hit: true, key: CACHE_KEY },
129
+ ts: nowIso(),
130
+ }),
131
+ );
132
+ }
133
+ }
134
+
135
+ const url = String(process.env.TICKERS_JPY_URL || `${BITBANK_API_BASE}/tickers_jpy`);
136
+ const timeoutMs = Number(process.env.TICKERS_JPY_TIMEOUT_MS ?? 2000);
137
+ const retries = Number(process.env.TICKERS_JPY_RETRIES ?? 1);
138
+ const retryWaitMs = Number(process.env.TICKERS_JPY_RETRY_WAIT_MS ?? 500);
139
+ const t0 = Date.now();
140
+ try {
141
+ // テスト用: about:timeout を指定すると擬似タイムアウト
142
+ if (url === 'about:timeout') {
143
+ await new Promise((r) => setTimeout(r, Math.min(timeoutMs + 10, 1000)));
144
+ throw new Error('AbortError: simulated timeout');
145
+ }
146
+
147
+ // テスト用: file:// を指定するとローカルJSONを読み込む
148
+ if (url.startsWith('file://')) {
149
+ const filePath = url.replace('file://', '');
150
+ const abs = path.isAbsolute(filePath) ? filePath : path.join(process.cwd(), filePath);
151
+ const buf = fs.readFileSync(abs, 'utf8');
152
+ const raw = JSON.parse(buf);
153
+ if (!raw || raw.success !== 1 || !Array.isArray(raw.data)) {
154
+ return GetTickersJpyOutputSchema.parse(
155
+ fail('UPSTREAM_ERROR: unexpected response from bitbank API', 'upstream'),
156
+ );
157
+ }
158
+ const dataRaw: Item[] = raw.data as Item[];
159
+ const { data: filtered, filterInfo } = await filterByMode(dataRaw, timeoutMs, retries, retryWaitMs);
160
+ // 24h変動率を open/last から算出(%)
161
+ const data = filtered.map((it) => {
162
+ const openN = toNum(it.open);
163
+ const lastN = toNum(it.last);
164
+ const change =
165
+ openN != null && openN > 0 && lastN != null ? Number((((lastN - openN) / openN) * 100).toFixed(2)) : null;
166
+ return { ...it, change24h: change, change24hPct: change };
167
+ });
168
+ tickerCache.set(CACHE_KEY, data);
169
+ const ms = Date.now() - t0;
170
+ const payloadBytes = Buffer.byteLength(JSON.stringify(dataRaw));
171
+ const summaryText = buildTickerText(
172
+ `tickers_jpy fetched in ${ms}ms (${data.length}/${dataRaw.length} items after filter, ${payloadBytes} bytes raw, mode=${filterInfo.mode}/${filterInfo.source})`,
173
+ data,
174
+ );
175
+ return GetTickersJpyOutputSchema.parse(
176
+ ok(summaryText, data, { cache: { hit: false, key: 'tickers_jpy' }, ts: nowIso(), latencyMs: ms, payloadBytes }),
177
+ );
178
+ }
179
+
180
+ // 固定バックオフでの簡易リトライ
181
+ let lastErr: unknown;
182
+ let raw: { success?: number; data?: Item[] } | undefined;
183
+ for (let i = 0; i <= retries; i++) {
184
+ const ctrl = new AbortController();
185
+ const t = setTimeout(() => ctrl.abort(), timeoutMs);
186
+ try {
187
+ const res = await fetch(url, { signal: ctrl.signal });
188
+ clearTimeout(t);
189
+ if (!res.ok) throw new Error(`HTTP ${res.status}`);
190
+ raw = (await res.json()) as { success?: number; data?: Item[] };
191
+ break;
192
+ } catch (e: unknown) {
193
+ clearTimeout(t);
194
+ lastErr = e;
195
+ if (i < retries) await new Promise((r) => setTimeout(r, retryWaitMs));
196
+ }
197
+ }
198
+ if (!raw) throw lastErr ?? new Error('no response');
199
+ if (!raw || raw.success !== 1 || !Array.isArray(raw.data)) {
200
+ return GetTickersJpyOutputSchema.parse(fail('UPSTREAM_ERROR: unexpected response from bitbank API', 'upstream'));
201
+ }
202
+ const dataRaw: Item[] = raw.data as Item[];
203
+ const { data: filtered, filterInfo } = await filterByMode(dataRaw, timeoutMs, retries, retryWaitMs);
204
+ const data = filtered.map((it) => {
205
+ const openN = toNum(it.open);
206
+ const lastN = toNum(it.last);
207
+ const change =
208
+ openN != null && openN > 0 && lastN != null ? Number((((lastN - openN) / openN) * 100).toFixed(2)) : null;
209
+ return { ...it, change24h: change, change24hPct: change };
210
+ });
211
+ tickerCache.set(CACHE_KEY, data);
212
+ const ms = Date.now() - t0;
213
+ const payloadBytes = Buffer.byteLength(JSON.stringify(dataRaw));
214
+ // ロギングはサーバ側集約。ここではsummaryに最小指標を含める
215
+ const summaryTextHttp = buildTickerText(
216
+ `tickers_jpy fetched in ${ms}ms (${data.length}/${dataRaw.length} items after filter, ${payloadBytes} bytes raw, mode=${filterInfo.mode}/${filterInfo.source})`,
217
+ data,
218
+ );
219
+ return GetTickersJpyOutputSchema.parse(
220
+ ok(summaryTextHttp, data, {
221
+ cache: { hit: false, key: 'tickers_jpy' },
222
+ ts: nowIso(),
223
+ latencyMs: ms,
224
+ payloadBytes,
225
+ filtered: true,
226
+ }),
227
+ );
228
+ } catch (e: unknown) {
229
+ const msg = getErrorMessage(e) || 'network error';
230
+ const isTimeout =
231
+ msg.includes('AbortError') ||
232
+ msg.includes('timeout') ||
233
+ msg.includes('fetch failed') ||
234
+ msg.includes('ECONNREFUSED') ||
235
+ msg.includes('ENOTFOUND');
236
+ return GetTickersJpyOutputSchema.parse(
237
+ fail(isTimeout ? `TIMEOUT_OR_NETWORK` : `UPSTREAM_${msg}`, isTimeout ? 'timeout' : 'upstream'),
238
+ );
239
+ }
240
+ }
@@ -0,0 +1,209 @@
1
+ import type { z } from 'zod';
2
+ import { toNum } from '../lib/conversions.js';
3
+ import { dayjs, toIsoMs } from '../lib/datetime.js';
4
+ import { formatPair, formatPrice } from '../lib/formatter.js';
5
+ import { BITBANK_API_BASE, DEFAULT_RETRIES, fetchJsonWithRateLimit } from '../lib/http.js';
6
+ import { failFromError, failFromValidation, ok } from '../lib/result.js';
7
+ import { createMeta, ensurePair, validateLimit } from '../lib/validate.js';
8
+ import {
9
+ type GetTransactionsDataSchemaOut,
10
+ GetTransactionsInputSchema,
11
+ type GetTransactionsMetaSchemaOut,
12
+ GetTransactionsOutputSchema,
13
+ } from '../src/schemas.js';
14
+ import type { ToolDefinition } from '../src/tool-definition.js';
15
+
16
+ type TxnRaw = Record<string, unknown>;
17
+
18
+ function toMs(input: unknown): number | null {
19
+ const n = toNum(input);
20
+ if (n == null) return null;
21
+ return n < 1e12 ? Math.floor(n * 1000) : Math.floor(n);
22
+ }
23
+
24
+ function normalizeSide(v: unknown): 'buy' | 'sell' | null {
25
+ const s = String(v ?? '')
26
+ .trim()
27
+ .toLowerCase();
28
+ if (s === 'buy') return 'buy';
29
+ if (s === 'sell') return 'sell';
30
+ return null;
31
+ }
32
+
33
+ type NormalizedTxn = { price: number; amount: number; side: 'buy' | 'sell'; timestampMs: number; isoTime: string };
34
+
35
+ /**
36
+ * 取引サマリを生成
37
+ */
38
+ function formatTransactionsSummary(pair: string, transactions: NormalizedTxn[], buys: number, sells: number): string {
39
+ const pairDisplay = formatPair(pair);
40
+ const _isJpy = pair.toLowerCase().includes('jpy');
41
+ const baseCurrency = pair.split('_')[0]?.toUpperCase() ?? '';
42
+ const lines: string[] = [];
43
+
44
+ const fmtPx = (price: number) => formatPrice(price, pair);
45
+
46
+ const formatTime = (ms: number): string => {
47
+ return dayjs(ms).tz('Asia/Tokyo').format('HH:mm:ss');
48
+ };
49
+
50
+ lines.push(`${pairDisplay} 直近取引 ${transactions.length}件`);
51
+
52
+ if (transactions.length > 0) {
53
+ const latestTxn = transactions[transactions.length - 1];
54
+ lines.push(`最新約定: ${fmtPx(latestTxn.price)}`);
55
+
56
+ // 買い/売り比率
57
+ const total = buys + sells;
58
+ const buyRatio = total > 0 ? Math.round((buys / total) * 100) : 0;
59
+ const sellRatio = 100 - buyRatio;
60
+ const dominant = buyRatio >= 60 ? '買い優勢' : buyRatio <= 40 ? '売り優勢' : '拮抗';
61
+ const dominantRatio = buyRatio >= 60 ? buyRatio : buyRatio <= 40 ? sellRatio : buyRatio;
62
+ lines.push(`買い: ${buys}件 / 売り: ${sells}件(${dominant} ${dominantRatio}%)`);
63
+
64
+ // 出来高合計
65
+ const totalVolume = transactions.reduce((sum, t) => sum + t.amount, 0);
66
+ const volStr = totalVolume >= 1 ? totalVolume.toFixed(4) : totalVolume.toFixed(6);
67
+ lines.push(`出来高: ${volStr} ${baseCurrency}`);
68
+
69
+ // 期間
70
+ const oldest = transactions[0];
71
+ const newest = transactions[transactions.length - 1];
72
+ lines.push(`期間: ${formatTime(oldest.timestampMs)}〜${formatTime(newest.timestampMs)}`);
73
+ }
74
+
75
+ return lines.join('\n');
76
+ }
77
+
78
+ export default async function getTransactions(pair: string = 'btc_jpy', limit: number = 60, date?: string) {
79
+ const chk = ensurePair(pair);
80
+ if (!chk.ok) return failFromValidation(chk, GetTransactionsOutputSchema);
81
+
82
+ const lim = validateLimit(limit, 1, 1000);
83
+ if (!lim.ok) return failFromValidation(lim, GetTransactionsOutputSchema);
84
+
85
+ const url =
86
+ date && /\d{8}/.test(String(date))
87
+ ? `${BITBANK_API_BASE}/${chk.pair}/transactions/${date}`
88
+ : `${BITBANK_API_BASE}/${chk.pair}/transactions`;
89
+
90
+ try {
91
+ const { data: json, rateLimit } = await fetchJsonWithRateLimit(url, { timeoutMs: 4000, retries: DEFAULT_RETRIES });
92
+ const jsonObj = json as { data?: { transactions?: TxnRaw[] } };
93
+ const arr: TxnRaw[] = (jsonObj?.data?.transactions ?? []) as TxnRaw[];
94
+
95
+ const items = arr
96
+ .map((t) => {
97
+ const price = toNum(t.price);
98
+ const amount = toNum(t.amount ?? t.size);
99
+ const side = normalizeSide(t.side);
100
+ const ms = toMs(t.executed_at ?? t.timestamp ?? t.date);
101
+ const isoTime = toIsoMs(ms);
102
+ if (price == null || amount == null || side == null || isoTime == null) return null;
103
+ return { price, amount, side, timestampMs: ms as number, isoTime };
104
+ })
105
+ .filter(Boolean) as NormalizedTxn[];
106
+
107
+ const sorted = items.sort((a, b) => a.timestampMs - b.timestampMs);
108
+ const latest = sorted.slice(-lim.value);
109
+
110
+ const buys = latest.filter((t) => t.side === 'buy').length;
111
+ const sells = latest.filter((t) => t.side === 'sell').length;
112
+ const baseSummary = formatTransactionsSummary(chk.pair, latest, buys, sells);
113
+ // テキスト summary に全取引データを含める(LLM が structuredContent.data を読めない対策)
114
+ const txLines = latest.map((t, i) => {
115
+ const time = dayjs(t.timestampMs).tz('Asia/Tokyo').format('HH:mm:ss');
116
+ return `[${i}] ${time} ${t.side} ${t.price} x${t.amount}`;
117
+ });
118
+ const summary =
119
+ baseSummary +
120
+ `\n\n📋 全${latest.length}件の取引:\n` +
121
+ txLines.join('\n') +
122
+ `\n\n---\n📌 含まれるもの: 個別約定(時刻・売買方向・価格・数量)、買い/売り件数比率` +
123
+ `\n📌 含まれないもの: 集計済みフロー指標(CVD・Zスコア・スパイク)、OHLCV、板情報` +
124
+ `\n📌 補完ツール: get_flow_metrics(集計フロー・CVD・スパイク検出), get_candles(OHLCV), get_orderbook(板情報)`;
125
+
126
+ const data = { raw: json, normalized: latest };
127
+ const meta = createMeta(chk.pair, {
128
+ count: latest.length,
129
+ source: date ? 'by_date' : 'latest',
130
+ ...(rateLimit ? { rateLimit } : {}),
131
+ });
132
+ return GetTransactionsOutputSchema.parse(
133
+ ok<z.infer<typeof GetTransactionsDataSchemaOut>, z.infer<typeof GetTransactionsMetaSchemaOut>>(
134
+ summary,
135
+ data,
136
+ meta as z.infer<typeof GetTransactionsMetaSchemaOut>,
137
+ ),
138
+ );
139
+ } catch (e: unknown) {
140
+ // 失敗時は叩いた URL をエラーメッセージに含め、呼び出し側で原因を特定しやすくする。
141
+ // ただし AbortError は failFromError 側の timeout 判定で必要なのでそのまま渡す。
142
+ if (e instanceof Error && e.name === 'AbortError') {
143
+ return failFromError(e, {
144
+ schema: GetTransactionsOutputSchema,
145
+ timeoutMs: 4000,
146
+ defaultType: 'network',
147
+ defaultMessage: `ネットワークエラー [url: ${url}]`,
148
+ });
149
+ }
150
+ const baseMsg = e instanceof Error && e.message ? e.message : typeof e === 'string' ? e : 'ネットワークエラー';
151
+ const wrapped = new Error(`${baseMsg} [url: ${url}]`);
152
+ return failFromError(wrapped, {
153
+ schema: GetTransactionsOutputSchema,
154
+ timeoutMs: 4000,
155
+ defaultType: 'network',
156
+ defaultMessage: `ネットワークエラー [url: ${url}]`,
157
+ });
158
+ }
159
+ }
160
+
161
+ // ── MCP ツール定義(tool-registry から自動収集) ──
162
+ export const toolDef: ToolDefinition = {
163
+ name: 'get_transactions',
164
+ description:
165
+ '[Transactions / Trades] 市場の約定履歴(transactions / recent trades)を取得。直近60件 or 日付指定。金額・価格でフィルタ可能。',
166
+ inputSchema: GetTransactionsInputSchema,
167
+ handler: async ({
168
+ pair,
169
+ limit,
170
+ date,
171
+ minAmount,
172
+ maxAmount,
173
+ minPrice,
174
+ maxPrice,
175
+ view,
176
+ }: {
177
+ pair?: string;
178
+ limit?: number;
179
+ date?: string;
180
+ minAmount?: number;
181
+ maxAmount?: number;
182
+ minPrice?: number;
183
+ maxPrice?: number;
184
+ view?: 'summary' | 'items';
185
+ }) => {
186
+ const res = await getTransactions(pair, limit, date);
187
+ if (!res?.ok) return res;
188
+ const hasFilter = minAmount != null || maxAmount != null || minPrice != null || maxPrice != null;
189
+ type TxItem = { price: number; amount: number; side: 'buy' | 'sell'; timestampMs: number; isoTime: string };
190
+ const items = (res?.data?.normalized ?? ([] as TxItem[])).filter(
191
+ (t: TxItem) =>
192
+ (minAmount == null || t.amount >= minAmount) &&
193
+ (maxAmount == null || t.amount <= maxAmount) &&
194
+ (minPrice == null || t.price >= minPrice) &&
195
+ (maxPrice == null || t.price <= maxPrice),
196
+ );
197
+ const summary = hasFilter
198
+ ? `${String(pair).toUpperCase().replace('_', '/')} フィルタ後 ${items.length}件 (buy=${items.filter((t: TxItem) => t.side === 'buy').length} sell=${items.filter((t: TxItem) => t.side === 'sell').length})`
199
+ : res.summary;
200
+ if (view === 'items') {
201
+ const text = JSON.stringify(items, null, 2);
202
+ return {
203
+ content: [{ type: 'text', text }],
204
+ structuredContent: { ...res, summary, data: { ...res.data, normalized: items } } as Record<string, unknown>,
205
+ };
206
+ }
207
+ return { ...res, summary, data: { ...res.data, normalized: items } };
208
+ },
209
+ };