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,296 @@
1
+ import { dayjs, toDisplayTime } from './datetime.js';
2
+
3
+ /** 時間足コードを日本語ラベルに変換 */
4
+ export function timeframeLabel(type: string): string {
5
+ const map: Record<string, string> = {
6
+ '1min': '1分足',
7
+ '5min': '5分足',
8
+ '15min': '15分足',
9
+ '30min': '30分足',
10
+ '1hour': '1時間足',
11
+ '4hour': '4時間足',
12
+ '8hour': '8時間足',
13
+ '12hour': '12時間足',
14
+ '1day': '日足',
15
+ '1week': '週足',
16
+ '1month': '月足',
17
+ };
18
+ return map[type] || type;
19
+ }
20
+
21
+ export function formatPair(pair: string): string {
22
+ return (pair || '').toUpperCase().replace('_', '/');
23
+ }
24
+
25
+ /**
26
+ * タイムスタンプをJST表示形式に変換
27
+ * @param ts タイムスタンプ(ミリ秒)。未指定時は現在時刻
28
+ * @param tz タイムゾーン(デフォルト: 'Asia/Tokyo')
29
+ * @returns "2025/11/24 15:32:45 JST" 形式
30
+ */
31
+ export function formatTimestampJST(ts?: number, tz: string = 'Asia/Tokyo'): string {
32
+ const result = toDisplayTime(ts, tz);
33
+ return result ?? dayjs(ts).toISOString();
34
+ }
35
+
36
+ /**
37
+ * 価格フォーマット(¥プレフィックス、ペア依存)
38
+ * JPY ペア → ¥123,456 / non-JPY → 0.123456
39
+ * pair 省略時は JPY として扱う
40
+ */
41
+ export function formatPrice(value: number | null | undefined, pair?: string): string {
42
+ if (value == null || !Number.isFinite(Number(value))) return 'N/A';
43
+ const n = Number(value);
44
+ const jpy = !pair || (typeof pair === 'string' && pair.toLowerCase().includes('jpy'));
45
+ if (jpy) return `¥${n.toLocaleString('ja-JP')}`;
46
+ return n.toLocaleString('ja-JP');
47
+ }
48
+
49
+ /**
50
+ * 価格フォーマット(円サフィックス、四捨五入)
51
+ * → 123,456円
52
+ */
53
+ export function formatPriceJPY(value: number | null | undefined): string {
54
+ if (value == null || !Number.isFinite(Number(value))) return 'n/a';
55
+ return `${Math.round(Number(value)).toLocaleString('ja-JP')}円`;
56
+ }
57
+
58
+ /**
59
+ * 通貨フォーマット(JPY/非JPY対応、スペース+通貨コード)
60
+ * JPY → 123,456 JPY / 非JPY → 0.12
61
+ */
62
+ export function formatCurrency(value: number | null | undefined, pair?: string): string {
63
+ if (value == null) return 'n/a';
64
+ const jpy = !pair || (typeof pair === 'string' && pair.toLowerCase().includes('jpy'));
65
+ return jpy ? `${Number(value).toLocaleString('ja-JP')} JPY` : `${Number(value).toFixed(2)}`;
66
+ }
67
+
68
+ /**
69
+ * 通貨フォーマット短縮形(大きな値はk表示)
70
+ * JPY ≥1000 → 12k JPY / JPY <1000 → 123 JPY / 非JPY → 0.12
71
+ */
72
+ export function formatCurrencyShort(value: number | null | undefined, pair?: string): string {
73
+ if (value == null) return 'n/a';
74
+ const jpy = !pair || (typeof pair === 'string' && pair.toLowerCase().includes('jpy'));
75
+ if (jpy) {
76
+ const n = Number(value);
77
+ return n >= 1000 ? `${Math.round(n / 1000)}k JPY` : `${n.toLocaleString('ja-JP')} JPY`;
78
+ }
79
+ return `${Number(value).toFixed(2)}`;
80
+ }
81
+
82
+ /**
83
+ * パーセンテージフォーマット
84
+ * @param value 数値
85
+ * @param opts.digits 小数桁数(デフォルト: 1)
86
+ * @param opts.sign 正数に+を付けるか(デフォルト: false)
87
+ * @param opts.multiply 100倍するか(デフォルト: false)。0-1小数→%変換に使う
88
+ */
89
+ export function formatPercent(
90
+ value: number | null | undefined,
91
+ opts: { digits?: number; sign?: boolean; multiply?: boolean } = {},
92
+ ): string {
93
+ if (value == null || !Number.isFinite(Number(value))) return 'n/a';
94
+ const { digits = 1, sign = false, multiply = false } = opts;
95
+ const v = multiply ? Number(value) * 100 : Number(value);
96
+ const prefix = sign && v >= 0 ? '+' : '';
97
+ return `${prefix}${v.toFixed(digits)}%`;
98
+ }
99
+
100
+ /**
101
+ * 出来高フォーマット(日本円単位、億円/万円)
102
+ */
103
+ export function formatVolumeJPY(value: number | null | undefined): string {
104
+ if (value == null || !Number.isFinite(value)) return 'n/a';
105
+ if (value >= 100_000_000) return `${(value / 100_000_000).toFixed(1)}億円`;
106
+ return `${Math.round(value / 10_000)}万円`;
107
+ }
108
+
109
+ // ── unknown 型安全フォーマッタ(デバッグ・パターン検出向け) ──
110
+
111
+ /**
112
+ * unknown 値を固定小数点で整形(デバッグ表示用)
113
+ * @param v 値(unknown 型対応、NaN/null/undefined → 'n/a')
114
+ * @param digits 小数桁数(デフォルト: 8)
115
+ */
116
+ export function formatFixed(v: unknown, digits = 8): string {
117
+ if (v == null) return 'n/a';
118
+ const n = Number(v);
119
+ return Number.isFinite(n) ? n.toFixed(digits) : 'n/a';
120
+ }
121
+
122
+ /**
123
+ * unknown 値を四捨五入してロケール整形
124
+ * @param v 値(unknown 型対応、NaN/null/undefined → 'n/a')
125
+ */
126
+ export function formatRounded(v: unknown): string {
127
+ if (v == null) return 'n/a';
128
+ const n = Number(v);
129
+ return Number.isFinite(n) ? Math.round(n).toLocaleString('ja-JP') : 'n/a';
130
+ }
131
+
132
+ /**
133
+ * unknown 値をそのまま文字列化(整数表示用)
134
+ * @param v 値(unknown 型対応、NaN/null/undefined → 'n/a')
135
+ */
136
+ export function formatInt(v: unknown): string {
137
+ if (v == null) return 'n/a';
138
+ const n = Number(v);
139
+ return Number.isFinite(n) ? String(n) : 'n/a';
140
+ }
141
+
142
+ /**
143
+ * unknown 値を比率(0-1)からパーセント整形
144
+ * 例: 0.05 → "5.0%"
145
+ * @param v 値(unknown 型対応、NaN/null/undefined → 'n/a')
146
+ * @param digits 小数桁数(デフォルト: 1)
147
+ */
148
+ export function formatPctFromRatio(v: unknown, digits = 1): string {
149
+ if (v == null) return 'n/a';
150
+ const n = Number(v);
151
+ return Number.isFinite(n) ? `${(n * 100).toFixed(digits)}%` : 'n/a';
152
+ }
153
+
154
+ // ── トレンド方向シンボル ──
155
+
156
+ /**
157
+ * 傾き(slope)から方向絵文字を返す
158
+ * 正→📈 負→📉 ゼロ/null→➡️
159
+ */
160
+ export function formatTrendSymbol(slope: number | null | undefined): string {
161
+ if (slope == null) return '➡️';
162
+ return slope > 0 ? '📈' : slope < 0 ? '📉' : '➡️';
163
+ }
164
+
165
+ /**
166
+ * 値の基準値との比較で矢印を返す(ボラティリティ推移など)
167
+ * @param val 現在値
168
+ * @param base 基準値
169
+ * @param threshold 有意差の閾値(デフォルト: 0.05 = 5%)
170
+ */
171
+ export function formatTrendArrow(
172
+ val: number | null | undefined,
173
+ base: number | null | undefined,
174
+ threshold = 0.05,
175
+ ): string {
176
+ if (val == null || base == null) return '→';
177
+ if (val > base * (1 + threshold)) return '⬆⬆';
178
+ if (val > base) return '⬆';
179
+ if (val < base * (1 - threshold)) return '⬇⬇';
180
+ if (val < base) return '⬇';
181
+ return '→';
182
+ }
183
+
184
+ /**
185
+ * 現在価格に対する基準価格の乖離率を整形
186
+ * 例: close=100, ref=102 → "+2.0% 上方"
187
+ * @param close 現在価格
188
+ * @param ref 基準価格(SMA等)
189
+ */
190
+ export function formatDeviation(close: number | null, ref: number | null | undefined): string {
191
+ if (close == null || ref == null || !Number.isFinite(close) || !Number.isFinite(ref) || close === 0) return 'n/a';
192
+ const pct = ((ref - close) / Math.abs(close)) * 100;
193
+ if (!Number.isFinite(pct)) return 'n/a';
194
+ const dir = ref >= close ? '上方' : '下方';
195
+ return `${formatPercent(pct, { sign: true, digits: 1 })} ${dir}`;
196
+ }
197
+
198
+ export function formatSummary(
199
+ args: {
200
+ pair?: string;
201
+ timeframe?: string;
202
+ latest?: number;
203
+ totalItems?: number;
204
+ keyPoints?: {
205
+ today?: { date?: string | null; index: number; close: number } | null;
206
+ sevenDaysAgo?: { date?: string | null; index: number; close: number; changePct: number | null } | null;
207
+ thirtyDaysAgo?: { date?: string | null; index: number; close: number; changePct: number | null } | null;
208
+ ninetyDaysAgo?: { date?: string | null; index: number; close: number; changePct: number | null } | null;
209
+ };
210
+ volumeStats?: {
211
+ recent7DaysAvg: number;
212
+ previous7DaysAvg: number;
213
+ last30DaysAvg?: number | null;
214
+ changePct: number;
215
+ judgment: string;
216
+ } | null;
217
+ extra?: string;
218
+ // 追加: 全件の範囲情報
219
+ priceRange?: { high: number; low: number; periodStart: string; periodEnd: string };
220
+ } = {},
221
+ ): string {
222
+ const { pair, timeframe, latest, totalItems, keyPoints, volumeStats, extra, priceRange } = args;
223
+ const p = formatPair(pair ?? '');
224
+ const tf = timeframe ? ` [${timeframe}]` : '';
225
+ const isJpy = typeof pair === 'string' && pair.toLowerCase().includes('jpy');
226
+ const currency = isJpy ? '円' : '';
227
+
228
+ // 基本情報
229
+ let summary = p;
230
+
231
+ // ローソク足取得の場合(totalItemsが明示的に指定されている場合)
232
+ if (typeof totalItems === 'number' && totalItems > 0) {
233
+ summary += `${tf} ローソク足${totalItems}本取得`;
234
+ summary += `\n⚠️ 配列は古い順: data[0]=最古、data[${totalItems - 1}]=最新`;
235
+
236
+ // 全件の範囲情報を追加
237
+ if (priceRange) {
238
+ summary += `\n\n📈 全${totalItems}件の価格範囲:`;
239
+ summary += `\n- 期間: ${priceRange.periodStart} 〜 ${priceRange.periodEnd}`;
240
+ summary += `\n- 高値: ${formatPrice(priceRange.high)}`;
241
+ summary += `\n- 安値: ${formatPrice(priceRange.low)}`;
242
+ }
243
+ }
244
+
245
+ // 期間別の価格推移
246
+ if (keyPoints && keyPoints.today) {
247
+ summary += '\n\n📊 期間別の価格推移:';
248
+
249
+ const fmtChange = (pct: number | null) => {
250
+ if (pct === null) return '';
251
+ return ` → 変化率 ${formatPercent(pct, { sign: true })}`;
252
+ };
253
+
254
+ // 今日
255
+ const today = keyPoints.today;
256
+ summary += `\n- 今日 (${today.date || '不明'}, data[${today.index}]): ${formatPrice(today.close)}`;
257
+
258
+ // 7日前
259
+ if (keyPoints.sevenDaysAgo) {
260
+ const sd = keyPoints.sevenDaysAgo;
261
+ summary += `\n- 7日前 (${sd.date || '不明'}, data[${sd.index}]): ${formatPrice(sd.close)}${fmtChange(sd.changePct)}`;
262
+ }
263
+
264
+ // 30日前
265
+ if (keyPoints.thirtyDaysAgo) {
266
+ const td = keyPoints.thirtyDaysAgo;
267
+ summary += `\n- 30日前 (${td.date || '不明'}, data[${td.index}]): ${formatPrice(td.close)}${fmtChange(td.changePct)}`;
268
+ }
269
+
270
+ // 90日前
271
+ if (keyPoints.ninetyDaysAgo) {
272
+ const nd = keyPoints.ninetyDaysAgo;
273
+ summary += `\n- 90日前 (${nd.date || '不明'}, data[${nd.index}]): ${formatPrice(nd.close)}${fmtChange(nd.changePct)}`;
274
+ }
275
+
276
+ // 出来高情報
277
+ if (volumeStats) {
278
+ summary += '\n\n【出来高推移】';
279
+ summary += `\n- 直近7日間の平均: ${volumeStats.recent7DaysAvg.toFixed(0)} BTC/日`;
280
+ summary += `\n- その前7日間の平均: ${volumeStats.previous7DaysAvg.toFixed(0)} BTC/日`;
281
+ if (typeof volumeStats.last30DaysAvg === 'number') {
282
+ summary += `\n- 過去30日間の平均: ${volumeStats.last30DaysAvg.toFixed(0)} BTC/日`;
283
+ }
284
+ summary += `\n- 出来高変化率: ${volumeStats.changePct >= 0 ? '+' : ''}${volumeStats.changePct}%`;
285
+ summary += `\n- 判定: ${volumeStats.judgment}`;
286
+ }
287
+
288
+ // 全データは呼び出し元(get_candles.ts 等)がテキストに追記する
289
+ } else if (typeof latest === 'number') {
290
+ // keyPointsがない場合(板情報など)は中値を表示
291
+ summary += ` 中値=${latest.toLocaleString('ja-JP')}${currency}`;
292
+ }
293
+
294
+ const tail = extra ? ` ${extra}` : '';
295
+ return `${summary}${tail}`.trim();
296
+ }
@@ -0,0 +1,111 @@
1
+ import { GetDepthOutputSchema } from '../src/schemas.js';
2
+ import { estimateZones } from './depth-analysis.js';
3
+ import { formatSummary, formatTimestampJST } from './formatter.js';
4
+ import { BITBANK_API_BASE, DEFAULT_RETRIES, fetchJsonWithRateLimit } from './http.js';
5
+ import { failFromError, failFromValidation, ok } from './result.js';
6
+ import { createMeta, ensurePair } from './validate.js';
7
+
8
+ export interface GetDepthOptions {
9
+ timeoutMs?: number;
10
+ maxLevels?: number;
11
+ }
12
+
13
+ export interface BuildDepthTextInput {
14
+ timestamp: number;
15
+ summary: string;
16
+ bids: Array<[unknown, unknown]>;
17
+ asks: Array<[unknown, unknown]>;
18
+ mid: number | null;
19
+ }
20
+
21
+ /** テキスト組み立て(板データ表示)— テスト可能な純粋関数 */
22
+ export function buildDepthText(input: BuildDepthTextInput): string {
23
+ const { timestamp, summary, bids, asks, mid } = input;
24
+ const text = [
25
+ `📸 ${formatTimestampJST(timestamp)}`,
26
+ '',
27
+ summary,
28
+ `板の層数: 買い ${bids.length}層 / 売り ${asks.length}層`,
29
+ mid ? `中値: ${mid.toLocaleString('ja-JP')}円` : '',
30
+ '',
31
+ `🟢 買い板 (全${bids.length}層):`,
32
+ ...bids.map(([p, s]: [unknown, unknown], i: number) => ` ${i + 1}. ${Number(p).toLocaleString('ja-JP')}円 ${s}`),
33
+ '',
34
+ `🔴 売り板 (全${asks.length}層):`,
35
+ ...asks.map(([p, s]: [unknown, unknown], i: number) => ` ${i + 1}. ${Number(p).toLocaleString('ja-JP')}円 ${s}`),
36
+ ]
37
+ .filter(Boolean)
38
+ .join('\n');
39
+
40
+ return (
41
+ text +
42
+ `\n\n---\n📌 含まれるもの: 現時点の板(bid/ask全レベル)、壁ゾーン推定` +
43
+ `\n📌 含まれないもの: 板の時系列変化、約定履歴、テクニカル指標、出来高フロー` +
44
+ `\n📌 補完ツール: get_orderbook(分析モード付き板情報), get_flow_metrics(出来高フロー), get_transactions(約定履歴)`
45
+ );
46
+ }
47
+
48
+ export default async function getDepth(pair: string, { timeoutMs = 3000, maxLevels = 200 }: GetDepthOptions = {}) {
49
+ const chk = ensurePair(pair);
50
+ if (!chk.ok) return failFromValidation(chk);
51
+
52
+ const url = `${BITBANK_API_BASE}/${chk.pair}/depth`;
53
+ try {
54
+ const { data: json, rateLimit } = await fetchJsonWithRateLimit(url, { timeoutMs, retries: DEFAULT_RETRIES });
55
+ const jsonObj = json as { data?: Record<string, unknown> };
56
+ const d = jsonObj?.data ?? {};
57
+ const asks = Array.isArray(d.asks) ? d.asks.slice(0, maxLevels) : [];
58
+ const bids = Array.isArray(d.bids) ? d.bids.slice(0, maxLevels) : [];
59
+
60
+ // 簡易サマリ(最良気配と件数)
61
+ const bestAsk = asks[0]?.[0] ?? null;
62
+ const bestBid = bids[0]?.[0] ?? null;
63
+ const mid = bestBid && bestAsk ? Number(((Number(bestBid) + Number(bestAsk)) / 2).toFixed(2)) : null;
64
+ const summary = formatSummary({
65
+ pair: chk.pair,
66
+ latest: mid ?? undefined,
67
+ extra: `levels: bids=${bids.length} asks=${asks.length}`,
68
+ });
69
+
70
+ const data = {
71
+ asks,
72
+ bids,
73
+ asks_over: d.asks_over,
74
+ asks_under: d.asks_under,
75
+ bids_over: d.bids_over,
76
+ bids_under: d.bids_under,
77
+ ask_market: d.ask_market,
78
+ bid_market: d.bid_market,
79
+ timestamp: Number(d.timestamp ?? d.timestamp_ms ?? Date.now()),
80
+ sequenceId:
81
+ d.sequenceId != null ? Number(d.sequenceId) : d.sequence_id != null ? Number(d.sequence_id) : undefined,
82
+ overlays: {
83
+ depth_zones: [
84
+ ...estimateZones(
85
+ bids.slice(0, 50).map(([p, s]: [unknown, unknown]) => [Number(p), Number(s)] as [number, number]),
86
+ 'bid',
87
+ ),
88
+ ...estimateZones(
89
+ asks.slice(0, 50).map(([p, s]: [unknown, unknown]) => [Number(p), Number(s)] as [number, number]),
90
+ 'ask',
91
+ ),
92
+ ],
93
+ },
94
+ };
95
+
96
+ // タイムスタンプ付きテキスト出力(全板データを含める: LLM が structuredContent.data を読めない対策)
97
+ const textWithBoundary = buildDepthText({ timestamp: data.timestamp, summary, bids, asks, mid });
98
+
99
+ const meta = createMeta(chk.pair, rateLimit ? { rateLimit } : {});
100
+ return GetDepthOutputSchema.parse(
101
+ ok(textWithBoundary, data as Record<string, unknown>, meta as Record<string, unknown>),
102
+ );
103
+ } catch (err: unknown) {
104
+ return failFromError(err, {
105
+ schema: GetDepthOutputSchema,
106
+ timeoutMs,
107
+ defaultType: 'network',
108
+ defaultMessage: 'ネットワークエラー',
109
+ }) as ReturnType<typeof GetDepthOutputSchema.parse>;
110
+ }
111
+ }
package/lib/http.ts ADDED
@@ -0,0 +1,132 @@
1
+ /** bitbank Public API ベースURL */
2
+ export const BITBANK_API_BASE = 'https://public.bitbank.cc';
3
+
4
+ /** fetchJson のデフォルトリトライ回数(初回 + N回) */
5
+ export const DEFAULT_RETRIES = 2;
6
+
7
+ /** レートリミット情報(レスポンスヘッダから抽出) */
8
+ export interface RateLimitInfo {
9
+ /** 残りリクエスト数 */
10
+ remaining: number;
11
+ /** 期間あたりの上限数 */
12
+ limit: number;
13
+ /** リセット時刻(Unix epoch 秒) */
14
+ reset: number;
15
+ }
16
+
17
+ /**
18
+ * レスポンスヘッダからレートリミット情報を抽出する。
19
+ * ヘッダが存在しない場合は null を返す。
20
+ */
21
+ export function extractRateLimit(
22
+ headers: { get(name: string): string | null } | undefined | null,
23
+ ): RateLimitInfo | null {
24
+ if (!headers || typeof headers.get !== 'function') return null;
25
+ const remaining = headers.get('X-RateLimit-Remaining');
26
+ const limit = headers.get('X-RateLimit-Limit');
27
+ const reset = headers.get('X-RateLimit-Reset');
28
+ if (remaining == null || limit == null || reset == null) return null;
29
+ const r = parseInt(remaining, 10);
30
+ const l = parseInt(limit, 10);
31
+ const s = parseInt(reset, 10);
32
+ if (Number.isNaN(r) || Number.isNaN(l) || Number.isNaN(s)) return null;
33
+ return { remaining: r, limit: l, reset: s };
34
+ }
35
+
36
+ export interface FetchJsonOptions {
37
+ timeoutMs?: number;
38
+ retries?: number;
39
+ /** Zod スキーマ等の parse 互換オブジェクト。指定時はレスポンスをランタイム検証する。 */
40
+ schema?: { parse: (data: unknown) => unknown };
41
+ }
42
+
43
+ /** リトライ待機の上限(30秒) */
44
+ const MAX_RETRY_WAIT_MS = 30_000;
45
+
46
+ /**
47
+ * Retry-After ヘッダから待機ミリ秒を算出する。
48
+ * 未指定・不正値の場合はフォールバック値を返す。いずれも MAX_RETRY_WAIT_MS でキャップ。
49
+ */
50
+ function parseRetryAfterMs(headers: { get(name: string): string | null }, fallbackMs: number): number {
51
+ const boundedFallbackMs = Math.min(fallbackMs, MAX_RETRY_WAIT_MS);
52
+ const raw = headers.get('Retry-After');
53
+ if (raw == null) return boundedFallbackMs;
54
+ const secs = parseInt(raw, 10);
55
+ if (Number.isFinite(secs) && secs > 0) return Math.min(secs * 1000, MAX_RETRY_WAIT_MS);
56
+ return boundedFallbackMs;
57
+ }
58
+
59
+ export async function fetchJson<T = unknown>(
60
+ url: string,
61
+ { timeoutMs = 2500, retries = DEFAULT_RETRIES, schema }: FetchJsonOptions = {},
62
+ ): Promise<T> {
63
+ let lastErr: unknown;
64
+ for (let i = 0; i <= retries; i++) {
65
+ const ctrl = new AbortController();
66
+ const t = setTimeout(() => ctrl.abort(), timeoutMs);
67
+ try {
68
+ const res = await fetch(url, { signal: ctrl.signal });
69
+ clearTimeout(t);
70
+ if (res.status === 429) {
71
+ const waitMs = parseRetryAfterMs(res.headers, 1000 * 2 ** i);
72
+ if (i < retries) {
73
+ await new Promise((r) => setTimeout(r, waitMs));
74
+ continue;
75
+ }
76
+ throw new Error('レート制限超過 (HTTP 429)。しばらく待ってから再試行してください');
77
+ }
78
+ if (!res.ok) throw new Error(`HTTP ${res.status} ${res.statusText}`);
79
+ const json: unknown = await res.json();
80
+ if (schema) return schema.parse(json) as T;
81
+ return json as T;
82
+ } catch (e) {
83
+ clearTimeout(t);
84
+ lastErr = e;
85
+ if (i < retries) await new Promise((r) => setTimeout(r, 200 * 2 ** i));
86
+ }
87
+ }
88
+ throw lastErr;
89
+ }
90
+
91
+ /** fetchJson の戻り値 + レートリミット情報 */
92
+ export interface FetchJsonResult<T> {
93
+ data: T;
94
+ rateLimit: RateLimitInfo | null;
95
+ }
96
+
97
+ /**
98
+ * fetchJson と同等だが、レスポンスヘッダからレートリミット情報も抽出して返す。
99
+ * ヘッダが存在しない場合は rateLimit: null。
100
+ */
101
+ export async function fetchJsonWithRateLimit<T = unknown>(
102
+ url: string,
103
+ { timeoutMs = 2500, retries = DEFAULT_RETRIES, schema }: FetchJsonOptions = {},
104
+ ): Promise<FetchJsonResult<T>> {
105
+ let lastErr: unknown;
106
+ for (let i = 0; i <= retries; i++) {
107
+ const ctrl = new AbortController();
108
+ const t = setTimeout(() => ctrl.abort(), timeoutMs);
109
+ try {
110
+ const res = await fetch(url, { signal: ctrl.signal });
111
+ clearTimeout(t);
112
+ if (res.status === 429) {
113
+ const waitMs = parseRetryAfterMs(res.headers, 1000 * 2 ** i);
114
+ if (i < retries) {
115
+ await new Promise((r) => setTimeout(r, waitMs));
116
+ continue;
117
+ }
118
+ throw new Error('レート制限超過 (HTTP 429)。しばらく待ってから再試行してください');
119
+ }
120
+ if (!res.ok) throw new Error(`HTTP ${res.status} ${res.statusText}`);
121
+ const rateLimit = extractRateLimit(res.headers);
122
+ const json: unknown = await res.json();
123
+ const data = schema ? (schema.parse(json) as T) : (json as T);
124
+ return { data, rateLimit };
125
+ } catch (e) {
126
+ clearTimeout(t);
127
+ lastErr = e;
128
+ if (i < retries) await new Promise((r) => setTimeout(r, 200 * 2 ** i));
129
+ }
130
+ }
131
+ throw lastErr;
132
+ }
@@ -0,0 +1,39 @@
1
+ /**
2
+ * テクニカル指標の共有デフォルト値。
3
+ * 複数ツール(analyze_indicators, render_chart_svg, handler 等)で
4
+ * 参照される数値を一箇所に集約する。
5
+ */
6
+
7
+ // ── RSI ──
8
+ export const RSI_PERIOD = 14;
9
+ export const RSI_OVERBOUGHT = 70;
10
+ export const RSI_OVERSOLD = 30;
11
+
12
+ // ── Bollinger Bands ──
13
+ export const BB_PERIOD = 20;
14
+ export const BB_STDDEV = 2;
15
+
16
+ // ── SMA ──
17
+ export const SMA_DEFAULT_PERIOD = 25;
18
+
19
+ // ── MACD ──
20
+ export const MACD_FAST = 12;
21
+ export const MACD_SLOW = 26;
22
+ export const MACD_SIGNAL = 9;
23
+
24
+ // ── Stochastic ──
25
+ export const STOCH_PERIOD = 14;
26
+ export const STOCH_SMOOTH_K = 3;
27
+ export const STOCH_SMOOTH_D = 3;
28
+
29
+ // ── OBV ──
30
+ export const OBV_SMA_PERIOD = 20;
31
+ export const OBV_TREND_THRESHOLD = 0.02;
32
+
33
+ // ── 一目均衡表 ──
34
+ export const ICHIMOKU_SHIFT = 26;
35
+ export const ICHIMOKU_MIN_BARS_FOR_CLOUD = 60;
36
+
37
+ // ── キャッシュ ──
38
+ export const INDICATOR_CACHE_TTL_MS = 30_000;
39
+ export const INDICATOR_CACHE_MAX_ENTRIES = 20;
@@ -0,0 +1,41 @@
1
+ export type IndicatorBufferKey =
2
+ | 'SMA_5'
3
+ | 'SMA_20'
4
+ | 'SMA_25'
5
+ | 'SMA_50'
6
+ | 'SMA_75'
7
+ | 'SMA_200'
8
+ | 'EMA_12'
9
+ | 'EMA_26'
10
+ | 'EMA_50'
11
+ | 'EMA_200'
12
+ | 'BB_20'
13
+ | 'RSI_14'
14
+ | 'STOCH'
15
+ | 'ICHIMOKU';
16
+
17
+ const INDICATOR_PERIODS: Record<IndicatorBufferKey, number> = {
18
+ SMA_5: 5,
19
+ SMA_20: 20,
20
+ SMA_25: 25,
21
+ SMA_50: 50,
22
+ SMA_75: 75,
23
+ SMA_200: 200,
24
+ EMA_12: 12,
25
+ EMA_26: 26,
26
+ EMA_50: 50,
27
+ EMA_200: 200,
28
+ BB_20: 20,
29
+ RSI_14: 15,
30
+ STOCH: 20,
31
+ ICHIMOKU: 78,
32
+ };
33
+
34
+ export function getFetchCount(displayCount: number, indicatorKeys: readonly IndicatorBufferKey[] = []): number {
35
+ const maxPeriod = indicatorKeys.reduce((max, key) => {
36
+ const period = INDICATOR_PERIODS[key] || 0;
37
+ return Math.max(max, period);
38
+ }, 0);
39
+ const buffer = maxPeriod > 0 ? maxPeriod - 1 : 0;
40
+ return displayCount + buffer;
41
+ }