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,229 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* get_my_trade_history — 自分の約定履歴を取得する Private API ツール。
|
|
3
|
+
*
|
|
4
|
+
* bitbank Private API `/v1/user/spot/trade_history` を呼び出し、
|
|
5
|
+
* LLM が分析しやすい形に整形して返す。
|
|
6
|
+
*
|
|
7
|
+
* - 自動ページネーション: count > PAGE_SIZE (1000) の場合、cursor ベースで
|
|
8
|
+
* 複数回リクエストし全件取得を試みる(最大 MAX_PAGES ページ)。
|
|
9
|
+
* - isComplete フラグ: 全件取得できたかどうかを meta に含める。
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { nowIso, parseIso8601, toIsoMs } from '../../lib/datetime.js';
|
|
13
|
+
import { formatPair, formatPrice } from '../../lib/formatter.js';
|
|
14
|
+
import { fail, ok } from '../../lib/result.js';
|
|
15
|
+
import { type BitbankPrivateClient, getDefaultClient, PrivateApiError } from '../../src/private/client.js';
|
|
16
|
+
import { GetMyTradeHistoryInputSchema, GetMyTradeHistoryOutputSchema } from '../../src/private/schemas.js';
|
|
17
|
+
import type { ToolDefinition } from '../../src/tool-definition.js';
|
|
18
|
+
|
|
19
|
+
/** bitbank /v1/user/spot/trade_history のレスポンス型 */
|
|
20
|
+
interface RawTrade {
|
|
21
|
+
trade_id: number;
|
|
22
|
+
pair: string;
|
|
23
|
+
order_id: number;
|
|
24
|
+
side: string;
|
|
25
|
+
position_side?: string;
|
|
26
|
+
type: string;
|
|
27
|
+
amount: string;
|
|
28
|
+
price: string;
|
|
29
|
+
maker_taker: string;
|
|
30
|
+
fee_amount_base: string;
|
|
31
|
+
fee_amount_quote: string;
|
|
32
|
+
profit_loss?: string;
|
|
33
|
+
interest?: string;
|
|
34
|
+
executed_at: number;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// ── ページネーション設定 ──
|
|
38
|
+
const PAGE_SIZE = 1000;
|
|
39
|
+
const MAX_PAGES = 10;
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* cursor ベースの自動ページネーション(asc 順で取得し、最後の executed_at + 1 を since に)。
|
|
43
|
+
* since/end が外部指定されている場合でもページネーションする。
|
|
44
|
+
*/
|
|
45
|
+
async function paginateTrades(
|
|
46
|
+
client: BitbankPrivateClient,
|
|
47
|
+
baseParams: Record<string, string>,
|
|
48
|
+
limit: number,
|
|
49
|
+
): Promise<{ trades: RawTrade[]; isComplete: boolean }> {
|
|
50
|
+
const all: RawTrade[] = [];
|
|
51
|
+
let since: string | undefined = baseParams.since;
|
|
52
|
+
|
|
53
|
+
for (let page = 0; page < MAX_PAGES; page++) {
|
|
54
|
+
const params: Record<string, string> = {
|
|
55
|
+
...baseParams,
|
|
56
|
+
count: String(PAGE_SIZE),
|
|
57
|
+
order: 'asc',
|
|
58
|
+
...(since ? { since } : {}),
|
|
59
|
+
};
|
|
60
|
+
const rawData = await client.get<{ trades: RawTrade[] }>(
|
|
61
|
+
'/v1/user/spot/trade_history',
|
|
62
|
+
Object.keys(params).length > 0 ? params : undefined,
|
|
63
|
+
);
|
|
64
|
+
const batch = rawData.trades || [];
|
|
65
|
+
all.push(...batch);
|
|
66
|
+
|
|
67
|
+
// 取得件数が PAGE_SIZE 未満 → 全件取得完了
|
|
68
|
+
if (batch.length < PAGE_SIZE) {
|
|
69
|
+
return { trades: all.slice(0, limit), isComplete: true };
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// limit に達したら打ち切り(全件取得済みかは不明)
|
|
73
|
+
if (all.length >= limit) {
|
|
74
|
+
return { trades: all.slice(0, limit), isComplete: true };
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// 次ページ: 最後の約定の executed_at + 1ms を since に
|
|
78
|
+
const lastTs = batch[batch.length - 1]?.executed_at;
|
|
79
|
+
if (!lastTs) break;
|
|
80
|
+
since = String(lastTs + 1);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// MAX_PAGES 到達 → 打ち切り
|
|
84
|
+
return { trades: all.slice(0, limit), isComplete: false };
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export default async function getMyTradeHistory(args: {
|
|
88
|
+
pair?: string;
|
|
89
|
+
count?: number;
|
|
90
|
+
order?: 'asc' | 'desc';
|
|
91
|
+
since?: string;
|
|
92
|
+
end?: string;
|
|
93
|
+
}) {
|
|
94
|
+
const { pair, count = 100, order = 'desc', since, end } = args;
|
|
95
|
+
const client = getDefaultClient();
|
|
96
|
+
|
|
97
|
+
try {
|
|
98
|
+
// クエリパラメータを組み立て
|
|
99
|
+
const baseParams: Record<string, string> = {};
|
|
100
|
+
if (pair) baseParams.pair = pair;
|
|
101
|
+
|
|
102
|
+
// ISO8601 → unix ms 変換(strict parse で不正日時を弾く)
|
|
103
|
+
if (since) {
|
|
104
|
+
const parsed = parseIso8601(since);
|
|
105
|
+
if (!parsed) {
|
|
106
|
+
return GetMyTradeHistoryOutputSchema.parse(fail(`since の日時形式が不正です: ${since}`, 'validation_error'));
|
|
107
|
+
}
|
|
108
|
+
baseParams.since = String(parsed.valueOf());
|
|
109
|
+
}
|
|
110
|
+
if (end) {
|
|
111
|
+
const parsed = parseIso8601(end);
|
|
112
|
+
if (!parsed) {
|
|
113
|
+
return GetMyTradeHistoryOutputSchema.parse(fail(`end の日時形式が不正です: ${end}`, 'validation_error'));
|
|
114
|
+
}
|
|
115
|
+
baseParams.end = String(parsed.valueOf());
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
let rawTrades: RawTrade[];
|
|
119
|
+
let isComplete: boolean;
|
|
120
|
+
|
|
121
|
+
if (count <= PAGE_SIZE) {
|
|
122
|
+
// 単発リクエストで十分なケース
|
|
123
|
+
const params = { ...baseParams, count: String(count), order };
|
|
124
|
+
const rawData = await client.get<{ trades: RawTrade[] }>(
|
|
125
|
+
'/v1/user/spot/trade_history',
|
|
126
|
+
Object.keys(params).length > 0 ? params : undefined,
|
|
127
|
+
);
|
|
128
|
+
rawTrades = rawData.trades;
|
|
129
|
+
// 取得件数が count 未満なら全件取得済み
|
|
130
|
+
isComplete = rawTrades.length < count;
|
|
131
|
+
} else {
|
|
132
|
+
// 自動ページネーション
|
|
133
|
+
const result = await paginateTrades(client, baseParams, count);
|
|
134
|
+
rawTrades = result.trades;
|
|
135
|
+
isComplete = result.isComplete;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
const timestamp = nowIso();
|
|
139
|
+
|
|
140
|
+
// 約定データの整形
|
|
141
|
+
const trades = rawTrades.map((t) => ({
|
|
142
|
+
trade_id: t.trade_id,
|
|
143
|
+
pair: t.pair,
|
|
144
|
+
order_id: t.order_id,
|
|
145
|
+
side: t.side,
|
|
146
|
+
type: t.type,
|
|
147
|
+
amount: t.amount,
|
|
148
|
+
price: t.price,
|
|
149
|
+
maker_taker: t.maker_taker,
|
|
150
|
+
fee_amount_base: t.fee_amount_base,
|
|
151
|
+
fee_amount_quote: t.fee_amount_quote,
|
|
152
|
+
executed_at: toIsoMs(t.executed_at) ?? String(t.executed_at),
|
|
153
|
+
}));
|
|
154
|
+
|
|
155
|
+
// desc 指定時は逆順にソート(ページネーションは asc で取得するため)
|
|
156
|
+
if (order === 'desc') {
|
|
157
|
+
trades.reverse();
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// サマリー文字列の生成
|
|
161
|
+
const lines: string[] = [];
|
|
162
|
+
const pairLabel = pair ? formatPair(pair) : '全ペア';
|
|
163
|
+
lines.push(`約定履歴: ${pairLabel} ${trades.length}件`);
|
|
164
|
+
if (!isComplete) {
|
|
165
|
+
lines.push('※ 全件ではなく一部のみ取得されています。API件数上限に達した可能性があります');
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
if (trades.length > 0) {
|
|
169
|
+
lines.push('');
|
|
170
|
+
|
|
171
|
+
// サマリーに表示する約定(最大10件)
|
|
172
|
+
// desc(デフォルト): 先頭が直近なのでそのまま slice
|
|
173
|
+
// asc: 末尾が直近なので末尾10件を取得
|
|
174
|
+
const displayTrades = order === 'asc' ? trades.slice(-10) : trades.slice(0, 10);
|
|
175
|
+
for (const t of displayTrades) {
|
|
176
|
+
const sideLabel = t.side === 'buy' ? '買' : '売';
|
|
177
|
+
const isJpy = t.pair.includes('jpy');
|
|
178
|
+
const price = isJpy ? formatPrice(Number(t.price)) : t.price;
|
|
179
|
+
lines.push(
|
|
180
|
+
`[trade: ${t.trade_id} / order: ${t.order_id}] ${t.executed_at} ${formatPair(t.pair)} ${sideLabel} ${t.amount} @ ${price} (${t.maker_taker})`,
|
|
181
|
+
);
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
if (trades.length > 10) {
|
|
185
|
+
lines.push(`... 他 ${trades.length - 10}件`);
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
// 集計情報
|
|
189
|
+
const buyCount = trades.filter((t) => t.side === 'buy').length;
|
|
190
|
+
const sellCount = trades.filter((t) => t.side === 'sell').length;
|
|
191
|
+
lines.push('');
|
|
192
|
+
lines.push(`集計: 買 ${buyCount}件 / 売 ${sellCount}件`);
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
const summary = lines.join('\n');
|
|
196
|
+
|
|
197
|
+
const data = {
|
|
198
|
+
trades,
|
|
199
|
+
timestamp,
|
|
200
|
+
};
|
|
201
|
+
|
|
202
|
+
const meta = {
|
|
203
|
+
fetchedAt: timestamp,
|
|
204
|
+
tradeCount: trades.length,
|
|
205
|
+
pair: pair || undefined,
|
|
206
|
+
isComplete,
|
|
207
|
+
...(client.lastRateLimit ? { rateLimit: client.lastRateLimit } : {}),
|
|
208
|
+
};
|
|
209
|
+
|
|
210
|
+
return GetMyTradeHistoryOutputSchema.parse(ok(summary, data, meta));
|
|
211
|
+
} catch (err) {
|
|
212
|
+
if (err instanceof PrivateApiError) {
|
|
213
|
+
return GetMyTradeHistoryOutputSchema.parse(fail(err.message, err.errorType));
|
|
214
|
+
}
|
|
215
|
+
return GetMyTradeHistoryOutputSchema.parse(
|
|
216
|
+
fail(err instanceof Error ? err.message : '約定履歴取得中に予期しないエラーが発生しました', 'upstream_error'),
|
|
217
|
+
);
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
// ── MCP ツール定義(tool-registry から自動収集) ──
|
|
222
|
+
export const toolDef: ToolDefinition = {
|
|
223
|
+
name: 'get_my_trade_history',
|
|
224
|
+
description:
|
|
225
|
+
'[My Trades / Trade History / Fills] 自分の約定履歴(my trades / trade history / fills / executions)を取得。通貨ペア・期間・件数でフィルタ可能。Private API。',
|
|
226
|
+
inputSchema: GetMyTradeHistoryInputSchema,
|
|
227
|
+
handler: async (args: { pair?: string; count?: number; order?: 'asc' | 'desc'; since?: string; end?: string }) =>
|
|
228
|
+
getMyTradeHistory(args),
|
|
229
|
+
};
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* get_order — 注文詳細を取得する Private API ツール。
|
|
3
|
+
*
|
|
4
|
+
* bitbank Private API `GET /v1/user/spot/order` を呼び出し、
|
|
5
|
+
* 指定した注文IDの詳細情報を返す。
|
|
6
|
+
*
|
|
7
|
+
* ※ 約定・キャンセルから3ヶ月以上経過した注文は取得不可(エラー 50009)。
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { nowIso, toIsoMs } from '../../lib/datetime.js';
|
|
11
|
+
import { formatPair, formatPrice } from '../../lib/formatter.js';
|
|
12
|
+
import { fail, ok, toStructured } from '../../lib/result.js';
|
|
13
|
+
import { getDefaultClient, PrivateApiError } from '../../src/private/client.js';
|
|
14
|
+
import type { OrderResponse } from '../../src/private/schemas.js';
|
|
15
|
+
import { GetOrderInputSchema, GetOrderOutputSchema } from '../../src/private/schemas.js';
|
|
16
|
+
import type { ToolDefinition } from '../../src/tool-definition.js';
|
|
17
|
+
|
|
18
|
+
/** 注文情報を人間可読な文字列に整形 */
|
|
19
|
+
function formatOrderSummary(o: OrderResponse, pair: string): string {
|
|
20
|
+
const sideLabel = o.side === 'buy' ? '買' : '売';
|
|
21
|
+
const isJpy = pair.includes('jpy');
|
|
22
|
+
const price = o.price ? (isJpy ? formatPrice(Number(o.price)) : o.price) : '成行';
|
|
23
|
+
const amount = o.start_amount ?? o.executed_amount;
|
|
24
|
+
const lines: string[] = [];
|
|
25
|
+
|
|
26
|
+
lines.push(`注文詳細: ${formatPair(pair)}`);
|
|
27
|
+
lines.push(` 注文ID: ${o.order_id}`);
|
|
28
|
+
lines.push(` 方向: ${sideLabel} / タイプ: ${o.type}`);
|
|
29
|
+
lines.push(` 数量: ${amount} / 未約定: ${o.remaining_amount ?? '0'} / 約定済: ${o.executed_amount}`);
|
|
30
|
+
lines.push(` 価格: ${price}`);
|
|
31
|
+
if (o.average_price && o.average_price !== '0') {
|
|
32
|
+
lines.push(` 平均約定価格: ${isJpy ? formatPrice(Number(o.average_price)) : o.average_price}`);
|
|
33
|
+
}
|
|
34
|
+
if (o.trigger_price) {
|
|
35
|
+
lines.push(` トリガー価格: ${isJpy ? formatPrice(Number(o.trigger_price)) : o.trigger_price}`);
|
|
36
|
+
}
|
|
37
|
+
lines.push(` ステータス: ${o.status}`);
|
|
38
|
+
lines.push(` 注文日時: ${toIsoMs(o.ordered_at) ?? String(o.ordered_at)}`);
|
|
39
|
+
if (o.canceled_at) {
|
|
40
|
+
lines.push(` キャンセル日時: ${toIsoMs(o.canceled_at) ?? String(o.canceled_at)}`);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
return lines.join('\n');
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export default async function getOrder(args: { pair: string; order_id: number }) {
|
|
47
|
+
const { pair, order_id } = args;
|
|
48
|
+
const client = getDefaultClient();
|
|
49
|
+
|
|
50
|
+
try {
|
|
51
|
+
const rawOrder = await client.get<OrderResponse>('/v1/user/spot/order', {
|
|
52
|
+
pair,
|
|
53
|
+
order_id: String(order_id),
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
const timestamp = nowIso();
|
|
57
|
+
const summary = formatOrderSummary(rawOrder, pair);
|
|
58
|
+
|
|
59
|
+
return GetOrderOutputSchema.parse(
|
|
60
|
+
ok(
|
|
61
|
+
summary,
|
|
62
|
+
{ order: rawOrder, timestamp },
|
|
63
|
+
{
|
|
64
|
+
fetchedAt: timestamp,
|
|
65
|
+
orderId: order_id,
|
|
66
|
+
pair,
|
|
67
|
+
...(client.lastRateLimit ? { rateLimit: client.lastRateLimit } : {}),
|
|
68
|
+
},
|
|
69
|
+
),
|
|
70
|
+
);
|
|
71
|
+
} catch (err) {
|
|
72
|
+
if (err instanceof PrivateApiError) {
|
|
73
|
+
return GetOrderOutputSchema.parse(fail(err.message, err.errorType));
|
|
74
|
+
}
|
|
75
|
+
return GetOrderOutputSchema.parse(
|
|
76
|
+
fail(err instanceof Error ? err.message : '注文情報取得中に予期しないエラーが発生しました', 'upstream_error'),
|
|
77
|
+
);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export const toolDef: ToolDefinition = {
|
|
82
|
+
name: 'get_order',
|
|
83
|
+
description:
|
|
84
|
+
'[Order Detail / Order Status] 指定した注文IDの詳細情報を取得。ステータス・約定状況・価格を確認できる。Private API。',
|
|
85
|
+
inputSchema: GetOrderInputSchema,
|
|
86
|
+
handler: async (args) => {
|
|
87
|
+
const result = await getOrder(args as { pair: string; order_id: number });
|
|
88
|
+
if (!result.ok) return result;
|
|
89
|
+
const text = `${result.summary}\n${JSON.stringify(result.data, null, 2)}`;
|
|
90
|
+
return {
|
|
91
|
+
content: [{ type: 'text', text }],
|
|
92
|
+
structuredContent: toStructured(result),
|
|
93
|
+
};
|
|
94
|
+
},
|
|
95
|
+
};
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* get_orders_info — 複数注文の詳細を一括取得する Private API ツール。
|
|
3
|
+
*
|
|
4
|
+
* bitbank Private API `POST /v1/user/spot/orders_info` を呼び出し、
|
|
5
|
+
* 指定した複数の注文IDの詳細情報を返す。
|
|
6
|
+
*
|
|
7
|
+
* ※ 約定・キャンセルから3ヶ月以上経過した注文は結果に含まれない(エラーにはならない)。
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { nowIso, toIsoMs } from '../../lib/datetime.js';
|
|
11
|
+
import { formatPair, formatPrice } from '../../lib/formatter.js';
|
|
12
|
+
import { fail, ok, toStructured } from '../../lib/result.js';
|
|
13
|
+
import { getDefaultClient, PrivateApiError } from '../../src/private/client.js';
|
|
14
|
+
import type { OrderResponse } from '../../src/private/schemas.js';
|
|
15
|
+
import { GetOrdersInfoInputSchema, GetOrdersInfoOutputSchema } from '../../src/private/schemas.js';
|
|
16
|
+
import type { ToolDefinition } from '../../src/tool-definition.js';
|
|
17
|
+
|
|
18
|
+
export default async function getOrdersInfo(args: { pair: string; order_ids: number[] }) {
|
|
19
|
+
const { pair, order_ids } = args;
|
|
20
|
+
const client = getDefaultClient();
|
|
21
|
+
|
|
22
|
+
try {
|
|
23
|
+
const rawData = await client.post<{ orders: OrderResponse[] }>('/v1/user/spot/orders_info', {
|
|
24
|
+
pair,
|
|
25
|
+
order_ids,
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
const timestamp = nowIso();
|
|
29
|
+
const orders = rawData.orders;
|
|
30
|
+
const isJpy = pair.includes('jpy');
|
|
31
|
+
|
|
32
|
+
const lines: string[] = [];
|
|
33
|
+
lines.push(`注文情報: ${formatPair(pair)} ${orders.length}件`);
|
|
34
|
+
|
|
35
|
+
if (orders.length > 0) {
|
|
36
|
+
lines.push('');
|
|
37
|
+
for (const o of orders) {
|
|
38
|
+
const sideLabel = o.side === 'buy' ? '買' : '売';
|
|
39
|
+
const price = o.price ? (isJpy ? formatPrice(Number(o.price)) : o.price) : '成行';
|
|
40
|
+
const amount = o.start_amount ?? o.executed_amount;
|
|
41
|
+
lines.push(
|
|
42
|
+
`#${o.order_id} ${sideLabel}${o.type} ${amount} @ ${price} [${o.status}] (${toIsoMs(o.ordered_at) ?? String(o.ordered_at)})`,
|
|
43
|
+
);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
if (orders.length < order_ids.length) {
|
|
48
|
+
lines.push('');
|
|
49
|
+
lines.push(`※ ${order_ids.length - orders.length}件は3ヶ月以上前の注文のため取得できませんでした`);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const summary = lines.join('\n');
|
|
53
|
+
|
|
54
|
+
return GetOrdersInfoOutputSchema.parse(
|
|
55
|
+
ok(
|
|
56
|
+
summary,
|
|
57
|
+
{ orders, timestamp },
|
|
58
|
+
{
|
|
59
|
+
fetchedAt: timestamp,
|
|
60
|
+
orderCount: orders.length,
|
|
61
|
+
pair,
|
|
62
|
+
...(client.lastRateLimit ? { rateLimit: client.lastRateLimit } : {}),
|
|
63
|
+
},
|
|
64
|
+
),
|
|
65
|
+
);
|
|
66
|
+
} catch (err) {
|
|
67
|
+
if (err instanceof PrivateApiError) {
|
|
68
|
+
return GetOrdersInfoOutputSchema.parse(fail(err.message, err.errorType));
|
|
69
|
+
}
|
|
70
|
+
return GetOrdersInfoOutputSchema.parse(
|
|
71
|
+
fail(err instanceof Error ? err.message : '注文情報取得中に予期しないエラーが発生しました', 'upstream_error'),
|
|
72
|
+
);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export const toolDef: ToolDefinition = {
|
|
77
|
+
name: 'get_orders_info',
|
|
78
|
+
description:
|
|
79
|
+
'[Orders Info / Bulk Order Status] 複数の注文IDの詳細情報を一括取得。ステータス・約定状況を一度に確認できる。Private API。',
|
|
80
|
+
inputSchema: GetOrdersInfoInputSchema,
|
|
81
|
+
handler: async (args) => {
|
|
82
|
+
const result = await getOrdersInfo(args as { pair: string; order_ids: number[] });
|
|
83
|
+
if (!result.ok) return result;
|
|
84
|
+
const text = `${result.summary}\n${JSON.stringify(result.data, null, 2)}`;
|
|
85
|
+
return {
|
|
86
|
+
content: [{ type: 'text', text }],
|
|
87
|
+
structuredContent: toStructured(result),
|
|
88
|
+
};
|
|
89
|
+
},
|
|
90
|
+
};
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* preview_cancel_order — 注文キャンセルのプレビューと確認トークン発行。
|
|
3
|
+
*
|
|
4
|
+
* キャンセル対象の注文情報を表示し、cancel_order に渡す確認トークンを発行する。
|
|
5
|
+
* 実際のキャンセルは行わない。
|
|
6
|
+
*
|
|
7
|
+
* elicitation 対応ホストでは preview → ユーザー確認 → cancel_order までを
|
|
8
|
+
* このハンドラ内で完結させる(LLM から見ると preview_cancel_order 1 回呼び出しで
|
|
9
|
+
* キャンセル完了)。非対応ホストでは従来通り structuredContent 経由でトークンを
|
|
10
|
+
* 渡しフォールバックする(Progressive Enhancement)。
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import { formatPair, formatPrice } from '../../lib/formatter.js';
|
|
14
|
+
import { ok, toStructured } from '../../lib/result.js';
|
|
15
|
+
import { generateToken } from '../../src/private/confirmation.js';
|
|
16
|
+
import type { OrderResponse } from '../../src/private/schemas.js';
|
|
17
|
+
import { PreviewCancelOrderInputSchema, PreviewCancelOrderOutputSchema } from '../../src/private/schemas.js';
|
|
18
|
+
import type { ToolDefinition, ToolHandlerExtra } from '../../src/tool-definition.js';
|
|
19
|
+
import cancelOrder from './cancel_order.js';
|
|
20
|
+
import getOrder from './get_order.js';
|
|
21
|
+
|
|
22
|
+
/** 注文詳細をキャンセルプレビューのサマリ行に整形する */
|
|
23
|
+
function formatOrderDetailLines(order: OrderResponse, pair: string): string[] {
|
|
24
|
+
const sideLabel = order.side === 'buy' ? '買' : '売';
|
|
25
|
+
const isJpy = pair.includes('jpy');
|
|
26
|
+
const price = order.price ? (isJpy ? formatPrice(Number(order.price)) : order.price) : '成行';
|
|
27
|
+
const amount = order.start_amount ?? order.executed_amount ?? '?';
|
|
28
|
+
const lines: string[] = [];
|
|
29
|
+
lines.push(` 方向: ${sideLabel} / タイプ: ${order.type}`);
|
|
30
|
+
lines.push(` 数量: ${amount}(残: ${order.remaining_amount ?? '0'} / 約定: ${order.executed_amount})`);
|
|
31
|
+
lines.push(` 価格: ${price}`);
|
|
32
|
+
if (order.trigger_price) {
|
|
33
|
+
lines.push(` トリガー価格: ${isJpy ? formatPrice(Number(order.trigger_price)) : order.trigger_price}`);
|
|
34
|
+
}
|
|
35
|
+
if (order.average_price && order.average_price !== '0') {
|
|
36
|
+
lines.push(` 平均約定価格: ${isJpy ? formatPrice(Number(order.average_price)) : order.average_price}`);
|
|
37
|
+
}
|
|
38
|
+
lines.push(` ステータス: ${order.status}`);
|
|
39
|
+
return lines;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export default async function previewCancelOrder(args: { pair: string; order_id: number }) {
|
|
43
|
+
const { pair, order_id } = args;
|
|
44
|
+
|
|
45
|
+
// 注文詳細を取得して preview にも同梱する。失敗してもキャンセル自体は可能なので、
|
|
46
|
+
// エラーは握りつぶしてフォールバック表示にとどめる(ネットワーク不調や認証異常で
|
|
47
|
+
// キャンセル不能になる方が UX として悪いため)。
|
|
48
|
+
let orderDetail: OrderResponse | undefined;
|
|
49
|
+
const detailResult = await getOrder({ pair, order_id });
|
|
50
|
+
if (detailResult.ok) {
|
|
51
|
+
orderDetail = detailResult.data.order;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const tokenParams = { pair, order_id };
|
|
55
|
+
const { token, expiresAt } = generateToken('cancel_order', tokenParams);
|
|
56
|
+
|
|
57
|
+
const lines: string[] = [];
|
|
58
|
+
lines.push(`📋 キャンセルプレビュー: ${formatPair(pair)}`);
|
|
59
|
+
lines.push(` 注文ID: ${order_id}`);
|
|
60
|
+
if (orderDetail) {
|
|
61
|
+
lines.push(...formatOrderDetailLines(orderDetail, pair));
|
|
62
|
+
}
|
|
63
|
+
lines.push('');
|
|
64
|
+
lines.push('⚠️ このキャンセルはユーザーの最終確認(ホスト UI または elicitation)を経るまで実行されません。');
|
|
65
|
+
|
|
66
|
+
const summary = lines.join('\n');
|
|
67
|
+
|
|
68
|
+
const data: Record<string, unknown> = {
|
|
69
|
+
confirmation_token: token,
|
|
70
|
+
expires_at: expiresAt,
|
|
71
|
+
preview: { pair, order_id },
|
|
72
|
+
};
|
|
73
|
+
if (orderDetail) data.order = orderDetail;
|
|
74
|
+
|
|
75
|
+
return PreviewCancelOrderOutputSchema.parse(ok(summary, data, { action: 'cancel_order' as const }));
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* クライアントが elicitation/create に対応しているかを判定する。
|
|
80
|
+
* 非対応ホストでは従来挙動(structuredContent でトークンを返す)にフォールバックする。
|
|
81
|
+
*/
|
|
82
|
+
function clientSupportsElicitation(extra: ToolHandlerExtra | undefined): boolean {
|
|
83
|
+
const server = (extra as { server?: { getClientCapabilities?: () => unknown } } | undefined)?.server;
|
|
84
|
+
const caps = typeof server?.getClientCapabilities === 'function' ? server.getClientCapabilities() : undefined;
|
|
85
|
+
const elicitation = (caps as { elicitation?: unknown } | undefined)?.elicitation;
|
|
86
|
+
return Boolean(elicitation);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/** SDK の elicitInput を呼び出すための最小限の interface */
|
|
90
|
+
interface ElicitCapableServer {
|
|
91
|
+
elicitInput: (params: {
|
|
92
|
+
message: string;
|
|
93
|
+
requestedSchema: Record<string, unknown>;
|
|
94
|
+
}) => Promise<{ action: 'accept' | 'decline' | 'cancel'; content?: Record<string, unknown> }>;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
export const toolDef: ToolDefinition = {
|
|
98
|
+
name: 'preview_cancel_order',
|
|
99
|
+
description: [
|
|
100
|
+
'[Preview Cancel Order] 注文キャンセルのプレビューと確認トークン発行。実際のキャンセルは行わない。Private API。',
|
|
101
|
+
'cancel_order を実行するには、まずこのツールで確認トークンを取得する必要がある。',
|
|
102
|
+
'⚠️ confirmation_token は LLM 可視テキストには含めない。ホスト UI または elicitation のユーザー確認を経て cancel_order が呼ばれる前提。LLM が独断でトークンを引用して cancel_order を呼ぶと意図しないキャンセルになり得る。',
|
|
103
|
+
].join(' '),
|
|
104
|
+
inputSchema: PreviewCancelOrderInputSchema,
|
|
105
|
+
// MCP Apps (SEP-1865): 対応ホストでは iframe 内にキャンセル確認 UI を表示する。
|
|
106
|
+
// 非対応ホストでは無視され、従来のテキスト確認フローがそのまま動作する(Progressive Enhancement)。
|
|
107
|
+
_meta: {
|
|
108
|
+
ui: {
|
|
109
|
+
resourceUri: 'ui://cancel/confirm.html',
|
|
110
|
+
},
|
|
111
|
+
},
|
|
112
|
+
handler: async (args, extra) => {
|
|
113
|
+
const typedArgs = args as { pair: string; order_id: number };
|
|
114
|
+
const result = await previewCancelOrder(typedArgs);
|
|
115
|
+
if (!result.ok) return result;
|
|
116
|
+
|
|
117
|
+
// elicitation 対応ホストでは preview → ユーザー確認 → cancel_order までを
|
|
118
|
+
// このハンドラ内で完結させる。
|
|
119
|
+
if (clientSupportsElicitation(extra)) {
|
|
120
|
+
const server = (extra as { server?: ElicitCapableServer } | undefined)?.server;
|
|
121
|
+
if (server && typeof server.elicitInput === 'function') {
|
|
122
|
+
try {
|
|
123
|
+
const elicit = await server.elicitInput({
|
|
124
|
+
message: result.summary,
|
|
125
|
+
requestedSchema: {
|
|
126
|
+
type: 'object',
|
|
127
|
+
properties: {
|
|
128
|
+
confirmed: { type: 'boolean', title: 'この注文をキャンセルする' },
|
|
129
|
+
},
|
|
130
|
+
required: ['confirmed'],
|
|
131
|
+
},
|
|
132
|
+
});
|
|
133
|
+
if (elicit.action !== 'accept' || !elicit.content?.confirmed) {
|
|
134
|
+
return {
|
|
135
|
+
content: [{ type: 'text', text: 'ユーザーがキャンセル操作を取り消しました(elicitation)' }],
|
|
136
|
+
structuredContent: toStructured(result),
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
// 内部的に cancel_order を実行。監査ログには route='elicitation' で記録される。
|
|
140
|
+
const cancelResult = await cancelOrder(
|
|
141
|
+
{
|
|
142
|
+
...typedArgs,
|
|
143
|
+
confirmation_token: result.data.confirmation_token,
|
|
144
|
+
token_expires_at: result.data.expires_at,
|
|
145
|
+
},
|
|
146
|
+
'elicitation',
|
|
147
|
+
);
|
|
148
|
+
const cancelText = cancelResult.ok ? cancelResult.summary : `Error: ${cancelResult.summary}`;
|
|
149
|
+
return {
|
|
150
|
+
content: [{ type: 'text', text: cancelText }],
|
|
151
|
+
structuredContent: toStructured(cancelResult),
|
|
152
|
+
};
|
|
153
|
+
} catch {
|
|
154
|
+
// elicitInput が想定外に失敗した場合はフォールバックに進む。
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// フォールバック: confirmation_token は LLM 可視テキストには含めず、
|
|
160
|
+
// structuredContent 側にだけ残す。SEP-1865 UI ボタンや Inspector はこちらを参照する。
|
|
161
|
+
const text = [
|
|
162
|
+
result.summary,
|
|
163
|
+
'',
|
|
164
|
+
'※ confirmation_token はホスト UI / structuredContent 経由でのみ受け渡されます。',
|
|
165
|
+
' LLM はトークンを引用したり、ユーザー確認なしに cancel_order を呼ばないでください。',
|
|
166
|
+
].join('\n');
|
|
167
|
+
return {
|
|
168
|
+
content: [{ type: 'text', text }],
|
|
169
|
+
structuredContent: toStructured(result),
|
|
170
|
+
};
|
|
171
|
+
},
|
|
172
|
+
};
|