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.
- package/LICENSE +21 -0
- package/README.md +388 -0
- package/assets/lightweight-charts.standalone.js +7 -0
- package/bin/bitbank-lab-mcp.js +20 -0
- package/lib/cache.ts +70 -0
- package/lib/candle-utils.ts +48 -0
- package/lib/candle-validate.ts +434 -0
- package/lib/conversions.ts +25 -0
- package/lib/datetime.ts +157 -0
- package/lib/depth-analysis.ts +51 -0
- package/lib/error.ts +15 -0
- package/lib/formatter.ts +296 -0
- package/lib/get-depth.ts +111 -0
- package/lib/http.ts +132 -0
- package/lib/indicator-config.ts +39 -0
- package/lib/indicator_buffer.ts +41 -0
- package/lib/indicators.ts +579 -0
- package/lib/logger.ts +120 -0
- package/lib/ma-snapshot-utils.ts +277 -0
- package/lib/math.ts +89 -0
- package/lib/pattern-diagrams.ts +562 -0
- package/lib/result.ts +104 -0
- package/lib/validate.ts +154 -0
- package/lib/volatility.ts +132 -0
- package/package.json +79 -0
- package/src/env.ts +4 -0
- package/src/handlers/analyzeCandlePatternsHandler.ts +383 -0
- package/src/handlers/analyzeFibonacciHandler.ts +54 -0
- package/src/handlers/analyzeIndicatorsHandler.ts +682 -0
- package/src/handlers/analyzeMarketSignalHandler.ts +272 -0
- package/src/handlers/analyzeMyPortfolioHandler.ts +800 -0
- package/src/handlers/detectPatternsHandler.ts +77 -0
- package/src/handlers/detectPatternsViewsHandler.ts +518 -0
- package/src/handlers/getTickersJpyHandler.ts +145 -0
- package/src/handlers/getVolatilityMetricsHandler.ts +234 -0
- package/src/handlers/portfolio/calc.ts +549 -0
- package/src/handlers/portfolio/fetch.ts +318 -0
- package/src/handlers/portfolio/types.ts +170 -0
- package/src/handlers/renderChartSvgHandler.ts +69 -0
- package/src/handlers/runBacktestHandler.ts +70 -0
- package/src/http.ts +107 -0
- package/src/private/auth.ts +104 -0
- package/src/private/client.ts +298 -0
- package/src/private/config.ts +25 -0
- package/src/private/confirmation.ts +185 -0
- package/src/private/schemas.ts +866 -0
- package/src/prompts.ts +2296 -0
- package/src/resources/app-resources.ts +79 -0
- package/src/schema/analysis.ts +942 -0
- package/src/schema/backtest.ts +100 -0
- package/src/schema/base.ts +88 -0
- package/src/schema/candle-validate.ts +135 -0
- package/src/schema/chart.ts +399 -0
- package/src/schema/index.ts +11 -0
- package/src/schema/indicators.ts +125 -0
- package/src/schema/market-data.ts +298 -0
- package/src/schema/patterns.ts +382 -0
- package/src/schema/types.ts +97 -0
- package/src/schemas.d.ts +37 -0
- package/src/schemas.ts +7 -0
- package/src/server.ts +405 -0
- package/src/tool-definition.ts +44 -0
- package/src/tool-registry.ts +174 -0
- package/src/types/express-shim.d.ts +9 -0
- package/src/types/schemas.generated.d.ts +23 -0
- package/tools/analyze_bb_snapshot.ts +385 -0
- package/tools/analyze_candle_patterns.ts +810 -0
- package/tools/analyze_currency_strength.ts +273 -0
- package/tools/analyze_ema_snapshot.ts +183 -0
- package/tools/analyze_fibonacci.ts +530 -0
- package/tools/analyze_ichimoku_snapshot.ts +606 -0
- package/tools/analyze_indicators.ts +691 -0
- package/tools/analyze_market_signal.ts +665 -0
- package/tools/analyze_mtf_fibonacci.ts +273 -0
- package/tools/analyze_mtf_sma.ts +175 -0
- package/tools/analyze_sma_snapshot.ts +146 -0
- package/tools/analyze_stoch_snapshot.ts +276 -0
- package/tools/analyze_support_resistance.ts +817 -0
- package/tools/analyze_volume_profile.ts +546 -0
- package/tools/chart/ichimoku-cloud.ts +113 -0
- package/tools/chart/render-depth.ts +139 -0
- package/tools/chart/render-sub-panels.ts +208 -0
- package/tools/chart/svg-utils.ts +102 -0
- package/tools/detect_macd_cross.ts +691 -0
- package/tools/detect_patterns.ts +424 -0
- package/tools/detect_whale_events.ts +181 -0
- package/tools/get_candles.ts +487 -0
- package/tools/get_flow_metrics.ts +596 -0
- package/tools/get_orderbook.ts +540 -0
- package/tools/get_ticker.ts +132 -0
- package/tools/get_tickers_jpy.ts +240 -0
- package/tools/get_transactions.ts +209 -0
- package/tools/get_volatility_metrics.ts +302 -0
- package/tools/patterns/aftermath.ts +212 -0
- package/tools/patterns/config.ts +151 -0
- package/tools/patterns/detect_doubles.ts +650 -0
- package/tools/patterns/detect_hs.ts +635 -0
- package/tools/patterns/detect_pennants.ts +373 -0
- package/tools/patterns/detect_triangles.ts +820 -0
- package/tools/patterns/detect_triples.ts +633 -0
- package/tools/patterns/detect_wedges.ts +1072 -0
- package/tools/patterns/helpers.ts +517 -0
- package/tools/patterns/index.ts +40 -0
- package/tools/patterns/regression.ts +153 -0
- package/tools/patterns/smoothing.ts +168 -0
- package/tools/patterns/swing.ts +91 -0
- package/tools/patterns/types.ts +193 -0
- package/tools/prepare_chart_data.ts +294 -0
- package/tools/prepare_depth_data.ts +189 -0
- package/tools/private/analyze_my_portfolio.ts +21 -0
- package/tools/private/cancel_order.ts +127 -0
- package/tools/private/cancel_orders.ts +121 -0
- package/tools/private/create_order.ts +236 -0
- package/tools/private/get_margin_positions.ts +134 -0
- package/tools/private/get_margin_status.ts +155 -0
- package/tools/private/get_margin_trade_history.ts +156 -0
- package/tools/private/get_my_assets.ts +207 -0
- package/tools/private/get_my_deposit_withdrawal.ts +500 -0
- package/tools/private/get_my_orders.ts +157 -0
- package/tools/private/get_my_trade_history.ts +229 -0
- package/tools/private/get_order.ts +95 -0
- package/tools/private/get_orders_info.ts +90 -0
- package/tools/private/preview_cancel_order.ts +172 -0
- package/tools/private/preview_cancel_orders.ts +137 -0
- package/tools/private/preview_order.ts +292 -0
- package/tools/render_candle_pattern_diagram.ts +389 -0
- package/tools/render_chart_svg.ts +799 -0
- package/tools/render_depth_svg.ts +274 -0
- package/tools/trading_process/index.ts +7 -0
- package/tools/trading_process/lib/backtest_engine.ts +252 -0
- package/tools/trading_process/lib/equity.ts +131 -0
- package/tools/trading_process/lib/fetch_candles.ts +181 -0
- package/tools/trading_process/lib/sma.ts +62 -0
- package/tools/trading_process/lib/strategies/bb_breakout.ts +141 -0
- package/tools/trading_process/lib/strategies/index.ts +52 -0
- package/tools/trading_process/lib/strategies/macd_cross.ts +256 -0
- package/tools/trading_process/lib/strategies/rsi.ts +133 -0
- package/tools/trading_process/lib/strategies/sma_cross.ts +214 -0
- package/tools/trading_process/lib/strategies/types.ts +118 -0
- package/tools/trading_process/lib/svg_to_png.ts +64 -0
- package/tools/trading_process/render_backtest_chart_generic.ts +729 -0
- package/tools/trading_process/run_backtest.ts +243 -0
- package/tools/trading_process/types.ts +85 -0
- package/tools/validate_candle_data.ts +260 -0
- package/tsconfig.json +17 -0
- package/ui/cancel-confirm/dist/cancel-confirm.html +99 -0
- package/ui/order-confirm/dist/order-confirm.html +99 -0
|
@@ -0,0 +1,540 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* get_orderbook — 統合板情報ツール
|
|
3
|
+
*
|
|
4
|
+
* mode で分析粒度を切り替え、内部では単一の /depth API 呼出しで全モードをカバー。
|
|
5
|
+
*
|
|
6
|
+
* | mode | 旧ツール | 概要 |
|
|
7
|
+
* |-------------|---------------------------|--------------------------------------------|
|
|
8
|
+
* | summary | get_orderbook | 上位N層の正規化+累計サイズ+spread |
|
|
9
|
+
* | pressure | get_orderbook_pressure | 帯域(±0.1%/0.5%/1%等)別 買い/売り圧力 |
|
|
10
|
+
* | statistics | get_orderbook_statistics | 範囲分析+流動性ゾーン+大口注文+総合評価 |
|
|
11
|
+
* | raw | get_depth | 生の bids/asks 配列+壁ゾーン自動推定 |
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import { toNum } from '../lib/conversions.js';
|
|
15
|
+
import { toIsoTime } from '../lib/datetime.js';
|
|
16
|
+
import { estimateZones } from '../lib/depth-analysis.js';
|
|
17
|
+
import { formatSummary, formatTimestampJST } from '../lib/formatter.js';
|
|
18
|
+
import { BITBANK_API_BASE, DEFAULT_RETRIES, fetchJsonWithRateLimit } from '../lib/http.js';
|
|
19
|
+
import { fail, failFromError, failFromValidation, ok } from '../lib/result.js';
|
|
20
|
+
import { createMeta, ensurePair, validateLimit } from '../lib/validate.js';
|
|
21
|
+
import type { OrderbookLevelWithCum } from '../src/schemas.js';
|
|
22
|
+
import { GetOrderbookInputSchema } from '../src/schemas.js';
|
|
23
|
+
import type { ToolDefinition } from '../src/tool-definition.js';
|
|
24
|
+
|
|
25
|
+
export type OrderbookMode = 'summary' | 'pressure' | 'statistics' | 'raw';
|
|
26
|
+
|
|
27
|
+
export interface GetOrderbookParams {
|
|
28
|
+
pair?: string;
|
|
29
|
+
mode?: OrderbookMode;
|
|
30
|
+
/** summary mode: 上位N層 (1-200, default 10) */
|
|
31
|
+
topN?: number;
|
|
32
|
+
/** pressure mode: 帯域幅 (default [0.001, 0.005, 0.01]) */
|
|
33
|
+
bandsPct?: number[];
|
|
34
|
+
/** statistics mode: 範囲% (default [0.5, 1.0, 2.0]) */
|
|
35
|
+
ranges?: number[];
|
|
36
|
+
/** statistics mode: 価格ゾーン分割数 (default 10) */
|
|
37
|
+
priceZones?: number;
|
|
38
|
+
/** raw mode: 最大レベル数 (default 200) */
|
|
39
|
+
maxLevels?: number;
|
|
40
|
+
/** タイムアウト */
|
|
41
|
+
timeoutMs?: number;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// ─── ヘルパー ───
|
|
45
|
+
|
|
46
|
+
type RawLevel = [string, string]; // [price, size] from API
|
|
47
|
+
type NumLevel = [number, number]; // [price, size] parsed
|
|
48
|
+
|
|
49
|
+
function toLevelsWithCum(levels: NumLevel[], n: number): OrderbookLevelWithCum[] {
|
|
50
|
+
const out = levels.slice(0, n).map(([price, size]) => ({ price, size, cumSize: 0 }));
|
|
51
|
+
let cum = 0;
|
|
52
|
+
for (const lvl of out) {
|
|
53
|
+
cum += Number.isFinite(lvl.size) ? lvl.size : 0;
|
|
54
|
+
lvl.cumSize = Number(cum.toFixed(8));
|
|
55
|
+
}
|
|
56
|
+
return out;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// ─── mode=summary ───
|
|
60
|
+
|
|
61
|
+
function buildSummary(pair: string, bidsNum: NumLevel[], asksNum: NumLevel[], topN: number, timestamp: number) {
|
|
62
|
+
const bids = toLevelsWithCum(bidsNum, topN);
|
|
63
|
+
const asks = toLevelsWithCum(asksNum, topN);
|
|
64
|
+
|
|
65
|
+
const bestAsk = asks[0]?.price ?? null;
|
|
66
|
+
const bestBid = bids[0]?.price ?? null;
|
|
67
|
+
const spread = bestAsk != null && bestBid != null ? Number((bestAsk - bestBid).toFixed(0)) : null;
|
|
68
|
+
const mid = bestAsk != null && bestBid != null ? Number(((bestAsk + bestBid) / 2).toFixed(2)) : null;
|
|
69
|
+
|
|
70
|
+
const summary = formatSummary({
|
|
71
|
+
pair,
|
|
72
|
+
latest: mid ?? undefined,
|
|
73
|
+
extra: `bid=${bestBid ?? 'N/A'} ask=${bestAsk ?? 'N/A'} spread=${spread ?? 'N/A'}`,
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
const text = [
|
|
77
|
+
`📸 ${formatTimestampJST(timestamp)}`,
|
|
78
|
+
'',
|
|
79
|
+
summary,
|
|
80
|
+
'',
|
|
81
|
+
`📊 板情報 (上位${topN}層):`,
|
|
82
|
+
`中値: ${mid?.toLocaleString('ja-JP') ?? 'N/A'}円`,
|
|
83
|
+
`スプレッド: ${spread?.toLocaleString('ja-JP') ?? 'N/A'}円`,
|
|
84
|
+
'',
|
|
85
|
+
`🟢 買い板 (Bids): ${bids.length}層`,
|
|
86
|
+
...bids.map(
|
|
87
|
+
(b, i) => ` ${i + 1}. ${b.price.toLocaleString('ja-JP')}円 ${b.size.toFixed(4)} (cum:${b.cumSize.toFixed(4)})`,
|
|
88
|
+
),
|
|
89
|
+
'',
|
|
90
|
+
`🔴 売り板 (Asks): ${asks.length}層`,
|
|
91
|
+
...asks.map(
|
|
92
|
+
(a, i) => ` ${i + 1}. ${a.price.toLocaleString('ja-JP')}円 ${a.size.toFixed(4)} (cum:${a.cumSize.toFixed(4)})`,
|
|
93
|
+
),
|
|
94
|
+
]
|
|
95
|
+
.filter(Boolean)
|
|
96
|
+
.join('\n');
|
|
97
|
+
|
|
98
|
+
const data = {
|
|
99
|
+
mode: 'summary' as const,
|
|
100
|
+
normalized: {
|
|
101
|
+
pair,
|
|
102
|
+
bestBid,
|
|
103
|
+
bestAsk,
|
|
104
|
+
spread,
|
|
105
|
+
mid,
|
|
106
|
+
bids,
|
|
107
|
+
asks,
|
|
108
|
+
timestamp,
|
|
109
|
+
isoTime: toIsoTime(timestamp),
|
|
110
|
+
},
|
|
111
|
+
};
|
|
112
|
+
return { text, data, mid };
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// ─── mode=pressure ───
|
|
116
|
+
|
|
117
|
+
function buildPressure(pair: string, bidsRaw: RawLevel[], asksRaw: RawLevel[], bandsPct: number[], timestamp: number) {
|
|
118
|
+
const bestAsk = Number(asksRaw?.[0]?.[0] ?? NaN);
|
|
119
|
+
const bestBid = Number(bidsRaw?.[0]?.[0] ?? NaN);
|
|
120
|
+
const baseMid = Number.isFinite(bestAsk) && Number.isFinite(bestBid) ? (bestAsk + bestBid) / 2 : null;
|
|
121
|
+
|
|
122
|
+
function sumInBand(levels: RawLevel[], low: number, high: number) {
|
|
123
|
+
let s = 0;
|
|
124
|
+
for (const [p, q] of levels) {
|
|
125
|
+
const price = Number(p),
|
|
126
|
+
qty = Number(q);
|
|
127
|
+
if (Number.isFinite(price) && Number.isFinite(qty) && price >= low && price <= high) s += qty;
|
|
128
|
+
}
|
|
129
|
+
return s;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
const eps = 1e-9;
|
|
133
|
+
const bands = bandsPct.map((w) => {
|
|
134
|
+
if (baseMid == null || !Number.isFinite(baseMid)) {
|
|
135
|
+
return {
|
|
136
|
+
widthPct: w,
|
|
137
|
+
baseMid: null,
|
|
138
|
+
baseBidSize: 0,
|
|
139
|
+
baseAskSize: 0,
|
|
140
|
+
bidDelta: 0,
|
|
141
|
+
askDelta: 0,
|
|
142
|
+
netDelta: 0,
|
|
143
|
+
netDeltaPct: null as number | null,
|
|
144
|
+
tag: null as 'notice' | 'warning' | 'strong' | null,
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
const bidLow = (baseMid as number) * (1 - w);
|
|
148
|
+
const bidHigh = baseMid as number;
|
|
149
|
+
const askLow = baseMid as number;
|
|
150
|
+
const askHigh = (baseMid as number) * (1 + w);
|
|
151
|
+
|
|
152
|
+
const buyVol = sumInBand(bidsRaw, bidLow, bidHigh);
|
|
153
|
+
const sellVol = sumInBand(asksRaw, askLow, askHigh);
|
|
154
|
+
|
|
155
|
+
const net = Number((buyVol - sellVol).toFixed(8));
|
|
156
|
+
const pressure = Number(((buyVol - sellVol) / (buyVol + sellVol + eps)).toFixed(4));
|
|
157
|
+
|
|
158
|
+
const v = Math.abs(pressure);
|
|
159
|
+
const tag: 'notice' | 'warning' | 'strong' | null =
|
|
160
|
+
v >= 0.2 ? 'strong' : v >= 0.1 ? 'warning' : v >= 0.05 ? 'notice' : null;
|
|
161
|
+
|
|
162
|
+
return {
|
|
163
|
+
widthPct: w,
|
|
164
|
+
baseMid: baseMid as number,
|
|
165
|
+
baseBidSize: Number(buyVol.toFixed(8)),
|
|
166
|
+
baseAskSize: Number(sellVol.toFixed(8)),
|
|
167
|
+
bidDelta: Number(buyVol.toFixed(8)),
|
|
168
|
+
askDelta: Number((-sellVol).toFixed(8)),
|
|
169
|
+
netDelta: net,
|
|
170
|
+
netDeltaPct: pressure,
|
|
171
|
+
tag,
|
|
172
|
+
};
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
const strongestTag: 'notice' | 'warning' | 'strong' | null = bands.some((b) => b.tag === 'strong')
|
|
176
|
+
? 'strong'
|
|
177
|
+
: bands.some((b) => b.tag === 'warning')
|
|
178
|
+
? 'warning'
|
|
179
|
+
: bands.some((b) => b.tag === 'notice')
|
|
180
|
+
? 'notice'
|
|
181
|
+
: null;
|
|
182
|
+
|
|
183
|
+
const summary = formatSummary({
|
|
184
|
+
pair,
|
|
185
|
+
latest: baseMid ?? undefined,
|
|
186
|
+
extra: `bands=${bandsPct.join(',')}; tag=${strongestTag ?? 'none'}`,
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
const text = [
|
|
190
|
+
`📸 ${formatTimestampJST(timestamp)}`,
|
|
191
|
+
'',
|
|
192
|
+
summary,
|
|
193
|
+
'',
|
|
194
|
+
'📊 板圧力分析:',
|
|
195
|
+
...bands.map(
|
|
196
|
+
(b) =>
|
|
197
|
+
`±${(b.widthPct * 100).toFixed(2)}%: 買い ${b.baseBidSize.toFixed(2)} BTC / 売り ${b.baseAskSize.toFixed(2)} BTC (圧力: ${((b.netDeltaPct ?? 0) * 100).toFixed(1)}%)${b.tag ? ` [${b.tag}]` : ''}`,
|
|
198
|
+
),
|
|
199
|
+
'',
|
|
200
|
+
`💡 総合評価: ${strongestTag ?? '均衡'}`,
|
|
201
|
+
]
|
|
202
|
+
.filter(Boolean)
|
|
203
|
+
.join('\n');
|
|
204
|
+
|
|
205
|
+
const data = {
|
|
206
|
+
mode: 'pressure' as const,
|
|
207
|
+
bands,
|
|
208
|
+
aggregates: { netDelta: Number(bands.reduce((s, b) => s + b.netDelta, 0).toFixed(8)), strongestTag },
|
|
209
|
+
};
|
|
210
|
+
return { text, data, mid: baseMid };
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
// ─── mode=statistics ───
|
|
214
|
+
|
|
215
|
+
function buildStatistics(
|
|
216
|
+
pair: string,
|
|
217
|
+
bidsNum: NumLevel[],
|
|
218
|
+
asksNum: NumLevel[],
|
|
219
|
+
ranges: number[],
|
|
220
|
+
priceZones: number,
|
|
221
|
+
timestamp: number,
|
|
222
|
+
) {
|
|
223
|
+
const bestBid = bidsNum.length ? Math.max(...bidsNum.map(([p]) => p)) : null;
|
|
224
|
+
const bestAsk = asksNum.length ? Math.min(...asksNum.map(([p]) => p)) : null;
|
|
225
|
+
const mid = bestBid != null && bestAsk != null ? (bestBid + bestAsk) / 2 : null;
|
|
226
|
+
|
|
227
|
+
const basic = {
|
|
228
|
+
currentPrice: mid != null ? Math.round(mid) : null,
|
|
229
|
+
bestBid: bestBid != null ? Number(bestBid) : null,
|
|
230
|
+
bestAsk: bestAsk != null ? Number(bestAsk) : null,
|
|
231
|
+
spread: bestBid != null && bestAsk != null ? Number(bestAsk) - Number(bestBid) : null,
|
|
232
|
+
spreadPct: bestBid != null && bestAsk != null && mid ? (Number(bestAsk) - Number(bestBid)) / Number(mid) : null,
|
|
233
|
+
};
|
|
234
|
+
|
|
235
|
+
function sumWithinPct(levels: NumLevel[], pct: number, side: 'bid' | 'ask') {
|
|
236
|
+
if (!mid) return { vol: 0, val: 0 };
|
|
237
|
+
const minP = mid * (1 - pct / 100);
|
|
238
|
+
const maxP = mid * (1 + pct / 100);
|
|
239
|
+
let vol = 0;
|
|
240
|
+
let val = 0;
|
|
241
|
+
for (const [price, size] of levels) {
|
|
242
|
+
if (side === 'bid' && price >= minP && price <= mid) {
|
|
243
|
+
vol += size;
|
|
244
|
+
val += size * price;
|
|
245
|
+
}
|
|
246
|
+
if (side === 'ask' && price <= maxP && price >= mid) {
|
|
247
|
+
vol += size;
|
|
248
|
+
val += size * price;
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
return { vol, val };
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
const rangesOut = ranges.map((pct) => {
|
|
255
|
+
const b = sumWithinPct(bidsNum, pct, 'bid');
|
|
256
|
+
const a = sumWithinPct(asksNum, pct, 'ask');
|
|
257
|
+
const ratio = a.vol > 0 ? b.vol / a.vol : b.vol > 0 ? Infinity : 0;
|
|
258
|
+
const interpretation = ratio > 1.2 ? '買い板が厚い(下値堅い)' : ratio < 0.8 ? '売り板が厚い(上値重い)' : '均衡';
|
|
259
|
+
return {
|
|
260
|
+
pct,
|
|
261
|
+
bidVolume: Number(b.vol.toFixed(4)),
|
|
262
|
+
askVolume: Number(a.vol.toFixed(4)),
|
|
263
|
+
bidValue: Math.round(b.val),
|
|
264
|
+
askValue: Math.round(a.val),
|
|
265
|
+
ratio: Number(ratio.toFixed(2)),
|
|
266
|
+
interpretation,
|
|
267
|
+
};
|
|
268
|
+
});
|
|
269
|
+
|
|
270
|
+
// Liquidity zones
|
|
271
|
+
const maxPct = Math.max(...ranges);
|
|
272
|
+
const minPrice = mid ? mid * (1 - maxPct / 100) : 0;
|
|
273
|
+
const maxPrice = mid ? mid * (1 + maxPct / 100) : 0;
|
|
274
|
+
const step = priceZones > 0 && mid ? (maxPrice - minPrice) / priceZones : 0;
|
|
275
|
+
const zones: Array<{
|
|
276
|
+
priceRange: string;
|
|
277
|
+
bidVolume: number;
|
|
278
|
+
askVolume: number;
|
|
279
|
+
dominance: 'bid' | 'ask' | 'balanced';
|
|
280
|
+
note?: string;
|
|
281
|
+
}> = [];
|
|
282
|
+
if (step > 0) {
|
|
283
|
+
for (let i = 0; i < priceZones; i++) {
|
|
284
|
+
const lo = minPrice + i * step;
|
|
285
|
+
const hi = lo + step;
|
|
286
|
+
const bVol = bidsNum.filter(([p]) => p >= lo && p < hi).reduce((s, [, sz]) => s + sz, 0);
|
|
287
|
+
const aVol = asksNum.filter(([p]) => p >= lo && p < hi).reduce((s, [, sz]) => s + sz, 0);
|
|
288
|
+
const dom = bVol > aVol * 1.2 ? 'bid' : aVol > bVol * 1.2 ? 'ask' : 'balanced';
|
|
289
|
+
const note = dom === 'bid' ? '強い買いサポート' : dom === 'ask' ? '強い売り圧力' : undefined;
|
|
290
|
+
zones.push({
|
|
291
|
+
priceRange: `${Math.round(lo).toLocaleString('ja-JP')} - ${Math.round(hi).toLocaleString('ja-JP')}`,
|
|
292
|
+
bidVolume: Number(bVol.toFixed(4)),
|
|
293
|
+
askVolume: Number(aVol.toFixed(4)),
|
|
294
|
+
dominance: dom,
|
|
295
|
+
note,
|
|
296
|
+
});
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
// Large orders
|
|
301
|
+
const threshold = 0.1;
|
|
302
|
+
const largeBids = bidsNum
|
|
303
|
+
.filter(([, sz]) => sz >= threshold)
|
|
304
|
+
.slice(0, 20)
|
|
305
|
+
.map(([p, sz]) => ({
|
|
306
|
+
price: Math.round(p),
|
|
307
|
+
size: Number(sz.toFixed(3)),
|
|
308
|
+
distance: mid ? Number((((p - mid) / mid) * 100).toFixed(2)) : null,
|
|
309
|
+
}));
|
|
310
|
+
const largeAsks = asksNum
|
|
311
|
+
.filter(([, sz]) => sz >= threshold)
|
|
312
|
+
.slice(0, 20)
|
|
313
|
+
.map(([p, sz]) => ({
|
|
314
|
+
price: Math.round(p),
|
|
315
|
+
size: Number(sz.toFixed(3)),
|
|
316
|
+
distance: mid ? Number((((p - mid) / mid) * 100).toFixed(2)) : null,
|
|
317
|
+
}));
|
|
318
|
+
|
|
319
|
+
// Overall assessment
|
|
320
|
+
const lastRatio = rangesOut[0]?.ratio ?? 1;
|
|
321
|
+
const overall = lastRatio > 1.1 ? '買い優勢' : lastRatio < 0.9 ? '売り優勢' : '均衡';
|
|
322
|
+
const strength = Math.abs(lastRatio - 1) > 0.3 ? 'strong' : Math.abs(lastRatio - 1) > 0.1 ? 'moderate' : 'weak';
|
|
323
|
+
const liquidity =
|
|
324
|
+
(rangesOut[0]?.bidVolume ?? 0) + (rangesOut[0]?.askVolume ?? 0) > 20
|
|
325
|
+
? 'high'
|
|
326
|
+
: (rangesOut[0]?.bidVolume ?? 0) + (rangesOut[0]?.askVolume ?? 0) > 5
|
|
327
|
+
? 'medium'
|
|
328
|
+
: 'low';
|
|
329
|
+
const recommendation =
|
|
330
|
+
overall === '買い優勢'
|
|
331
|
+
? '下値が堅く、買いエントリーに適した環境。'
|
|
332
|
+
: overall === '売り優勢'
|
|
333
|
+
? '上値が重く、押し目待ち・警戒。'
|
|
334
|
+
: '均衡圏、レンジ想定。';
|
|
335
|
+
|
|
336
|
+
const text = [
|
|
337
|
+
`📸 ${formatTimestampJST(timestamp)}`,
|
|
338
|
+
'',
|
|
339
|
+
`=== ${String(pair).toUpperCase()} 板統計分析 ===`,
|
|
340
|
+
`💰 現在価格: ${basic.currentPrice != null ? `${basic.currentPrice.toLocaleString('ja-JP')}円` : 'n/a'}`,
|
|
341
|
+
basic.spread != null ? ` スプレッド: ${basic.spread}円 (${((basic.spreadPct || 0) * 100).toFixed(6)}%)` : '',
|
|
342
|
+
'',
|
|
343
|
+
'📊 板の厚み分析:',
|
|
344
|
+
...rangesOut.map(
|
|
345
|
+
(r) =>
|
|
346
|
+
`±${r.pct}%レンジ: 買い ${r.bidVolume} BTC / 売り ${r.askVolume} BTC (比率 ${r.ratio}) → ${r.interpretation}`,
|
|
347
|
+
),
|
|
348
|
+
'',
|
|
349
|
+
'📈 価格帯別の流動性分布:',
|
|
350
|
+
...zones.map(
|
|
351
|
+
(z) => `${z.priceRange}円: 買い ${z.bidVolume} / 売り ${z.askVolume} (${z.dominance}) ${z.note || ''}`,
|
|
352
|
+
),
|
|
353
|
+
'',
|
|
354
|
+
'🐋 大口注文:',
|
|
355
|
+
...largeBids.map(
|
|
356
|
+
(o) =>
|
|
357
|
+
`買い板: ${o.price.toLocaleString('ja-JP')}円に${o.size} BTC (${o.distance != null ? `${(o.distance >= 0 ? '+' : '') + o.distance}%` : ''})`,
|
|
358
|
+
),
|
|
359
|
+
...largeAsks.map(
|
|
360
|
+
(o) =>
|
|
361
|
+
`売り板: ${o.price.toLocaleString('ja-JP')}円に${o.size} BTC (${o.distance != null ? `${(o.distance >= 0 ? '+' : '') + o.distance}%` : ''})`,
|
|
362
|
+
),
|
|
363
|
+
'',
|
|
364
|
+
`💡 総合評価: ${overall}(${strength})`,
|
|
365
|
+
recommendation,
|
|
366
|
+
]
|
|
367
|
+
.filter(Boolean)
|
|
368
|
+
.join('\n');
|
|
369
|
+
|
|
370
|
+
const data = {
|
|
371
|
+
mode: 'statistics' as const,
|
|
372
|
+
basic,
|
|
373
|
+
ranges: rangesOut,
|
|
374
|
+
liquidityZones: zones,
|
|
375
|
+
largeOrders: { bids: largeBids, asks: largeAsks, threshold },
|
|
376
|
+
summary: { overall, strength, liquidity, recommendation },
|
|
377
|
+
};
|
|
378
|
+
return { text, data, mid };
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
// ─── mode=raw ───
|
|
382
|
+
|
|
383
|
+
function buildRaw(
|
|
384
|
+
pair: string,
|
|
385
|
+
rawJson: Record<string, unknown>,
|
|
386
|
+
bidsRaw: RawLevel[],
|
|
387
|
+
asksRaw: RawLevel[],
|
|
388
|
+
timestamp: number,
|
|
389
|
+
) {
|
|
390
|
+
const bestAsk = asksRaw[0]?.[0] != null ? Number(asksRaw[0][0]) : null;
|
|
391
|
+
const bestBid = bidsRaw[0]?.[0] != null ? Number(bidsRaw[0][0]) : null;
|
|
392
|
+
const mid = bestBid != null && bestAsk != null ? Number(((Number(bestBid) + Number(bestAsk)) / 2).toFixed(2)) : null;
|
|
393
|
+
|
|
394
|
+
const bidsNum: NumLevel[] = bidsRaw.map(([p, s]) => [Number(p), Number(s)]);
|
|
395
|
+
const asksNum: NumLevel[] = asksRaw.map(([p, s]) => [Number(p), Number(s)]);
|
|
396
|
+
|
|
397
|
+
const summary = formatSummary({
|
|
398
|
+
pair,
|
|
399
|
+
latest: mid ?? undefined,
|
|
400
|
+
extra: `levels: bids=${bidsRaw.length} asks=${asksRaw.length}`,
|
|
401
|
+
});
|
|
402
|
+
|
|
403
|
+
// raw mode: 全レベルをテキストに含める(LLM が structuredContent.data を読めない対策)
|
|
404
|
+
const text = [
|
|
405
|
+
`📸 ${formatTimestampJST(timestamp)}`,
|
|
406
|
+
'',
|
|
407
|
+
summary,
|
|
408
|
+
`板の層数: 買い ${bidsRaw.length}層 / 売り ${asksRaw.length}層`,
|
|
409
|
+
mid ? `中値: ${mid.toLocaleString('ja-JP')}円` : '',
|
|
410
|
+
'',
|
|
411
|
+
`🟢 買い板 (全${bidsRaw.length}層):`,
|
|
412
|
+
...bidsRaw.map(([p, s], i) => ` ${i + 1}. ${Number(p).toLocaleString('ja-JP')}円 ${s}`),
|
|
413
|
+
'',
|
|
414
|
+
`🔴 売り板 (全${asksRaw.length}層):`,
|
|
415
|
+
...asksRaw.map(([p, s], i) => ` ${i + 1}. ${Number(p).toLocaleString('ja-JP')}円 ${s}`),
|
|
416
|
+
]
|
|
417
|
+
.filter(Boolean)
|
|
418
|
+
.join('\n');
|
|
419
|
+
|
|
420
|
+
const d = rawJson;
|
|
421
|
+
const data = {
|
|
422
|
+
mode: 'raw' as const,
|
|
423
|
+
asks: asksRaw,
|
|
424
|
+
bids: bidsRaw,
|
|
425
|
+
asks_over: d.asks_over,
|
|
426
|
+
asks_under: d.asks_under,
|
|
427
|
+
bids_over: d.bids_over,
|
|
428
|
+
bids_under: d.bids_under,
|
|
429
|
+
ask_market: d.ask_market,
|
|
430
|
+
bid_market: d.bid_market,
|
|
431
|
+
timestamp,
|
|
432
|
+
sequenceId: toNum(d.sequenceId) ?? toNum(d.sequence_id) ?? undefined,
|
|
433
|
+
overlays: {
|
|
434
|
+
depth_zones: [...estimateZones(bidsNum.slice(0, 50), 'bid'), ...estimateZones(asksNum.slice(0, 50), 'ask')],
|
|
435
|
+
},
|
|
436
|
+
};
|
|
437
|
+
|
|
438
|
+
return { text, data, mid };
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
// ─── メインエントリ ───
|
|
442
|
+
|
|
443
|
+
export default async function getOrderbook(params: GetOrderbookParams | string = {}) {
|
|
444
|
+
// 後方互換: 旧シグネチャ getOrderbook(pair, topN) 対応
|
|
445
|
+
let opts: GetOrderbookParams;
|
|
446
|
+
if (typeof params === 'string') {
|
|
447
|
+
opts = { pair: params, mode: 'summary' };
|
|
448
|
+
} else {
|
|
449
|
+
opts = params;
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
const {
|
|
453
|
+
pair = 'btc_jpy',
|
|
454
|
+
mode = 'summary',
|
|
455
|
+
topN = 10,
|
|
456
|
+
bandsPct = [0.001, 0.005, 0.01],
|
|
457
|
+
ranges = [0.5, 1.0, 2.0],
|
|
458
|
+
priceZones = 10,
|
|
459
|
+
maxLevels = 200,
|
|
460
|
+
timeoutMs = 3000,
|
|
461
|
+
} = opts;
|
|
462
|
+
|
|
463
|
+
const chk = ensurePair(pair);
|
|
464
|
+
if (!chk.ok) return failFromValidation(chk);
|
|
465
|
+
|
|
466
|
+
if (mode === 'summary') {
|
|
467
|
+
const limitCheck = validateLimit(topN, 1, 200, 'topN');
|
|
468
|
+
if (!limitCheck.ok) return failFromValidation(limitCheck);
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
// ─── 単一 API 呼出し ───
|
|
472
|
+
const url = `${BITBANK_API_BASE}/${chk.pair}/depth`;
|
|
473
|
+
try {
|
|
474
|
+
const { data: json, rateLimit } = await fetchJsonWithRateLimit(url, { timeoutMs, retries: DEFAULT_RETRIES });
|
|
475
|
+
const jsonObj = json as { data?: Record<string, unknown> };
|
|
476
|
+
const d = jsonObj?.data ?? {};
|
|
477
|
+
if (!Array.isArray(d.asks) || !Array.isArray(d.bids)) {
|
|
478
|
+
return fail('上流レスポンスに bids/asks が含まれていません', 'upstream');
|
|
479
|
+
}
|
|
480
|
+
const rawAsks: RawLevel[] = (d.asks as RawLevel[]).slice(0, maxLevels);
|
|
481
|
+
const rawBids: RawLevel[] = (d.bids as RawLevel[]).slice(0, maxLevels);
|
|
482
|
+
const timestamp = toNum(d.timestamp ?? d.timestamp_ms) ?? Date.now();
|
|
483
|
+
|
|
484
|
+
// NumLevel 変換(summary / statistics で使用)
|
|
485
|
+
const bidsNum: NumLevel[] = rawBids.map(([p, s]) => [Number(p), Number(s)]);
|
|
486
|
+
const asksNum: NumLevel[] = rawAsks.map(([p, s]) => [Number(p), Number(s)]);
|
|
487
|
+
|
|
488
|
+
let result: { text: string; data: Record<string, unknown>; mid: number | null };
|
|
489
|
+
|
|
490
|
+
switch (mode) {
|
|
491
|
+
case 'pressure':
|
|
492
|
+
result = buildPressure(chk.pair, rawBids, rawAsks, bandsPct, timestamp);
|
|
493
|
+
break;
|
|
494
|
+
case 'statistics':
|
|
495
|
+
result = buildStatistics(chk.pair, bidsNum, asksNum, ranges, priceZones, timestamp);
|
|
496
|
+
break;
|
|
497
|
+
case 'raw':
|
|
498
|
+
result = buildRaw(chk.pair, d, rawBids, rawAsks, timestamp);
|
|
499
|
+
break;
|
|
500
|
+
default:
|
|
501
|
+
result = buildSummary(chk.pair, bidsNum, asksNum, topN, timestamp);
|
|
502
|
+
break;
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
const boundary =
|
|
506
|
+
`\n\n---\n📌 含まれるもの: 現時点の板スナップショット(mode=${mode})` +
|
|
507
|
+
`\n📌 含まれないもの: 板の時系列変化、約定履歴、テクニカル指標、出来高フロー` +
|
|
508
|
+
`\n📌 補完ツール: get_flow_metrics(出来高フロー・CVD), get_transactions(約定履歴), analyze_indicators(指標)`;
|
|
509
|
+
result.text += boundary;
|
|
510
|
+
|
|
511
|
+
const meta = createMeta(chk.pair, { mode, topN, ...(rateLimit ? { rateLimit } : {}) });
|
|
512
|
+
return ok(result.text, result.data, meta);
|
|
513
|
+
} catch (err: unknown) {
|
|
514
|
+
return failFromError(err, { timeoutMs, defaultType: 'network', defaultMessage: 'ネットワークエラー' });
|
|
515
|
+
}
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
// ── MCP ツール定義(tool-registry から自動収集) ──
|
|
519
|
+
export const toolDef: ToolDefinition = {
|
|
520
|
+
name: 'get_orderbook',
|
|
521
|
+
description: `[Order Book / Depth / Spread] 板情報(order book / depth / bid-ask spread)の統合ツール。
|
|
522
|
+
|
|
523
|
+
【mode】summary(デフォルト): 上位N層+spread / pressure: 帯域別の買い/売り圧力 / statistics: 流動性ゾーン+大口注文 / raw: 生bids/asks+壁ゾーン推定。`,
|
|
524
|
+
inputSchema: GetOrderbookInputSchema,
|
|
525
|
+
handler: async ({
|
|
526
|
+
pair,
|
|
527
|
+
mode,
|
|
528
|
+
topN,
|
|
529
|
+
bandsPct,
|
|
530
|
+
ranges,
|
|
531
|
+
priceZones,
|
|
532
|
+
}: {
|
|
533
|
+
pair?: string;
|
|
534
|
+
mode?: 'summary' | 'pressure' | 'statistics' | 'raw';
|
|
535
|
+
topN?: number;
|
|
536
|
+
bandsPct?: number[];
|
|
537
|
+
ranges?: number[];
|
|
538
|
+
priceZones?: number;
|
|
539
|
+
}) => getOrderbook({ pair, mode, topN, bandsPct, ranges, priceZones }),
|
|
540
|
+
};
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
import { toNum } from '../lib/conversions.js';
|
|
2
|
+
import { toDisplayTime, toIsoTime } from '../lib/datetime.js';
|
|
3
|
+
import { formatPair, formatPercent, formatPrice } from '../lib/formatter.js';
|
|
4
|
+
import { BITBANK_API_BASE, DEFAULT_RETRIES, fetchJsonWithRateLimit } from '../lib/http.js';
|
|
5
|
+
import { fail, failFromError, failFromValidation, ok, parseAsResult } from '../lib/result.js';
|
|
6
|
+
import { createMeta, ensurePair } from '../lib/validate.js';
|
|
7
|
+
import type { FailResult, GetTickerData, GetTickerMeta, OkResult } from '../src/schemas.js';
|
|
8
|
+
import { GetTickerInputSchema, GetTickerOutputSchema } from '../src/schemas.js';
|
|
9
|
+
import type { ToolDefinition } from '../src/tool-definition.js';
|
|
10
|
+
|
|
11
|
+
export interface GetTickerOptions {
|
|
12
|
+
timeoutMs?: number;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* ticker データから content 用のサマリ文字列を生成
|
|
17
|
+
*/
|
|
18
|
+
function formatTickerSummary(pair: string, d: Record<string, unknown>): string {
|
|
19
|
+
const pairDisplay = formatPair(pair);
|
|
20
|
+
const _isJpy = pair.toLowerCase().includes('jpy');
|
|
21
|
+
|
|
22
|
+
const last = toNum(d.last);
|
|
23
|
+
const open = toNum(d.open);
|
|
24
|
+
const high = toNum(d.high);
|
|
25
|
+
const low = toNum(d.low);
|
|
26
|
+
const buy = toNum(d.buy);
|
|
27
|
+
const sell = toNum(d.sell);
|
|
28
|
+
const vol = toNum(d.vol);
|
|
29
|
+
|
|
30
|
+
// 通貨単位
|
|
31
|
+
const baseCurrency = pair.split('_')[0]?.toUpperCase() ?? '';
|
|
32
|
+
|
|
33
|
+
// 価格フォーマット(ペア依存)
|
|
34
|
+
const fmtPx = (v: number | null) => formatPrice(v, pair);
|
|
35
|
+
|
|
36
|
+
// 変動率計算
|
|
37
|
+
let changeStr = '';
|
|
38
|
+
if (last !== null && open !== null && open !== 0) {
|
|
39
|
+
const changePct = ((last - open) / open) * 100;
|
|
40
|
+
changeStr = formatPercent(changePct, { sign: true, digits: 2 });
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// スプレッド計算
|
|
44
|
+
let spreadStr = '';
|
|
45
|
+
if (buy !== null && sell !== null) {
|
|
46
|
+
spreadStr = fmtPx(sell - buy);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// 出来高フォーマット(通貨ベース単位なのでカスタム)
|
|
50
|
+
const formatVolume = (v: number | null): string => {
|
|
51
|
+
if (v === null) return 'N/A';
|
|
52
|
+
if (v >= 1000) {
|
|
53
|
+
return `${(v / 1000).toFixed(2)}K ${baseCurrency}`;
|
|
54
|
+
}
|
|
55
|
+
return `${v.toFixed(4)} ${baseCurrency}`;
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
// サマリ構築
|
|
59
|
+
const lines: string[] = [];
|
|
60
|
+
lines.push(`${pairDisplay} 現在値: ${fmtPx(last)}`);
|
|
61
|
+
lines.push(`24h: 始値 ${fmtPx(open)} / 高値 ${fmtPx(high)} / 安値 ${fmtPx(low)}`);
|
|
62
|
+
if (changeStr) {
|
|
63
|
+
lines.push(`24h変動: ${changeStr}`);
|
|
64
|
+
}
|
|
65
|
+
lines.push(`出来高: ${formatVolume(vol)}`);
|
|
66
|
+
lines.push(`Bid: ${fmtPx(buy)} / Ask: ${fmtPx(sell)}${spreadStr ? `(スプレッド: ${spreadStr})` : ''}`);
|
|
67
|
+
|
|
68
|
+
const tsNum = toNum(d.timestamp);
|
|
69
|
+
const timeStr = tsNum != null ? toDisplayTime(tsNum) : null;
|
|
70
|
+
if (timeStr) lines.push(`📸 ${timeStr} 時点`);
|
|
71
|
+
|
|
72
|
+
return lines.join('\n');
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export default async function getTicker(
|
|
76
|
+
pair: string,
|
|
77
|
+
{ timeoutMs = 5000 }: GetTickerOptions = {},
|
|
78
|
+
): Promise<OkResult<GetTickerData, GetTickerMeta> | FailResult> {
|
|
79
|
+
const chk = ensurePair(pair);
|
|
80
|
+
if (!chk.ok) return failFromValidation(chk);
|
|
81
|
+
|
|
82
|
+
const url = `${BITBANK_API_BASE}/${chk.pair}/ticker`;
|
|
83
|
+
|
|
84
|
+
try {
|
|
85
|
+
const { data: json, rateLimit } = await fetchJsonWithRateLimit(url, { timeoutMs, retries: DEFAULT_RETRIES });
|
|
86
|
+
const jsonObj = json as { success?: number; data?: Record<string, unknown> };
|
|
87
|
+
|
|
88
|
+
// 上流レスポンスの構造バリデーション
|
|
89
|
+
if (jsonObj?.success !== 1 || !jsonObj?.data || typeof jsonObj.data !== 'object') {
|
|
90
|
+
return fail('上流レスポンスが不正です', 'upstream');
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
const d = jsonObj.data;
|
|
94
|
+
const summary = formatTickerSummary(chk.pair, d);
|
|
95
|
+
|
|
96
|
+
const tsNum = toNum(d.timestamp);
|
|
97
|
+
const data: GetTickerData = {
|
|
98
|
+
raw: json,
|
|
99
|
+
normalized: {
|
|
100
|
+
pair: chk.pair,
|
|
101
|
+
last: toNum(d.last),
|
|
102
|
+
buy: toNum(d.buy),
|
|
103
|
+
sell: toNum(d.sell),
|
|
104
|
+
open: toNum(d.open),
|
|
105
|
+
high: toNum(d.high),
|
|
106
|
+
low: toNum(d.low),
|
|
107
|
+
volume: toNum(d.vol),
|
|
108
|
+
timestamp: tsNum,
|
|
109
|
+
isoTime: tsNum != null ? toIsoTime(tsNum) : null,
|
|
110
|
+
},
|
|
111
|
+
};
|
|
112
|
+
|
|
113
|
+
const meta = createMeta(chk.pair, rateLimit ? { rateLimit } : {});
|
|
114
|
+
return parseAsResult<GetTickerData, GetTickerMeta>(GetTickerOutputSchema, ok(summary, data, meta));
|
|
115
|
+
} catch (err: unknown) {
|
|
116
|
+
return failFromError(err, {
|
|
117
|
+
schema: GetTickerOutputSchema,
|
|
118
|
+
timeoutMs,
|
|
119
|
+
defaultType: 'network',
|
|
120
|
+
defaultMessage: 'ネットワークエラー',
|
|
121
|
+
});
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// ── MCP ツール定義(tool-registry から自動収集) ──
|
|
126
|
+
export const toolDef: ToolDefinition = {
|
|
127
|
+
name: 'get_ticker',
|
|
128
|
+
description:
|
|
129
|
+
'[Ticker / Price] 単一ペアのティッカー(ticker / price / 24h change)を取得。現在価格・出来高・24h高安。',
|
|
130
|
+
inputSchema: GetTickerInputSchema,
|
|
131
|
+
handler: async ({ pair }: { pair?: string }) => getTicker(pair ?? 'btc_jpy'),
|
|
132
|
+
};
|