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,137 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* preview_cancel_orders — 一括キャンセルのプレビューと確認トークン発行。
|
|
3
|
+
*
|
|
4
|
+
* キャンセル対象の注文ID一覧を表示し、cancel_orders に渡す確認トークンを発行する。
|
|
5
|
+
* 実際のキャンセルは行わない。
|
|
6
|
+
*
|
|
7
|
+
* elicitation 対応ホストでは preview → ユーザー確認 → cancel_orders までを
|
|
8
|
+
* このハンドラ内で完結させる。非対応ホストでは従来通り structuredContent
|
|
9
|
+
* 経由でトークンを渡しフォールバックする(Progressive Enhancement)。
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { formatPair } from '../../lib/formatter.js';
|
|
13
|
+
import { ok, toStructured } from '../../lib/result.js';
|
|
14
|
+
import { generateToken } from '../../src/private/confirmation.js';
|
|
15
|
+
import { PreviewCancelOrdersInputSchema, PreviewCancelOrdersOutputSchema } from '../../src/private/schemas.js';
|
|
16
|
+
import type { ToolDefinition, ToolHandlerExtra } from '../../src/tool-definition.js';
|
|
17
|
+
import cancelOrders from './cancel_orders.js';
|
|
18
|
+
|
|
19
|
+
export default function previewCancelOrders(args: { pair: string; order_ids: number[] }) {
|
|
20
|
+
const { pair, order_ids } = args;
|
|
21
|
+
|
|
22
|
+
const tokenParams = { pair, order_ids };
|
|
23
|
+
const { token, expiresAt } = generateToken('cancel_orders', tokenParams);
|
|
24
|
+
|
|
25
|
+
const lines: string[] = [];
|
|
26
|
+
lines.push(`📋 一括キャンセルプレビュー: ${formatPair(pair)} ${order_ids.length}件`);
|
|
27
|
+
for (const id of order_ids) {
|
|
28
|
+
lines.push(` 注文ID: ${id}`);
|
|
29
|
+
}
|
|
30
|
+
lines.push('');
|
|
31
|
+
lines.push('⚠️ この一括キャンセルはユーザーの最終確認(ホスト UI または elicitation)を経るまで実行されません。');
|
|
32
|
+
|
|
33
|
+
const summary = lines.join('\n');
|
|
34
|
+
|
|
35
|
+
return PreviewCancelOrdersOutputSchema.parse(
|
|
36
|
+
ok(
|
|
37
|
+
summary,
|
|
38
|
+
{ confirmation_token: token, expires_at: expiresAt, preview: { pair, order_ids } },
|
|
39
|
+
{ action: 'cancel_orders' as const },
|
|
40
|
+
),
|
|
41
|
+
);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** クライアントが elicitation/create に対応しているかを判定する。 */
|
|
45
|
+
function clientSupportsElicitation(extra: ToolHandlerExtra | undefined): boolean {
|
|
46
|
+
const server = (extra as { server?: { getClientCapabilities?: () => unknown } } | undefined)?.server;
|
|
47
|
+
const caps = typeof server?.getClientCapabilities === 'function' ? server.getClientCapabilities() : undefined;
|
|
48
|
+
const elicitation = (caps as { elicitation?: unknown } | undefined)?.elicitation;
|
|
49
|
+
return Boolean(elicitation);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/** SDK の elicitInput を呼び出すための最小限の interface */
|
|
53
|
+
interface ElicitCapableServer {
|
|
54
|
+
elicitInput: (params: {
|
|
55
|
+
message: string;
|
|
56
|
+
requestedSchema: Record<string, unknown>;
|
|
57
|
+
}) => Promise<{ action: 'accept' | 'decline' | 'cancel'; content?: Record<string, unknown> }>;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export const toolDef: ToolDefinition = {
|
|
61
|
+
name: 'preview_cancel_orders',
|
|
62
|
+
description: [
|
|
63
|
+
'[Preview Cancel Orders] 一括キャンセルのプレビューと確認トークン発行。実際のキャンセルは行わない。Private API。',
|
|
64
|
+
'cancel_orders を実行するには、まずこのツールで確認トークンを取得する必要がある。',
|
|
65
|
+
'⚠️ confirmation_token は LLM 可視テキストには含めない。ホスト UI または elicitation のユーザー確認を経て cancel_orders が呼ばれる前提。LLM が独断でトークンを引用して cancel_orders を呼ぶと意図しないキャンセルになり得る。',
|
|
66
|
+
].join(' '),
|
|
67
|
+
inputSchema: PreviewCancelOrdersInputSchema,
|
|
68
|
+
// MCP Apps (SEP-1865): 対応ホストでは iframe 内にキャンセル確認 UI を表示する。
|
|
69
|
+
// 非対応ホストでは無視され、従来のテキスト確認フローがそのまま動作する(Progressive Enhancement)。
|
|
70
|
+
_meta: {
|
|
71
|
+
ui: {
|
|
72
|
+
resourceUri: 'ui://cancel/confirm.html',
|
|
73
|
+
},
|
|
74
|
+
},
|
|
75
|
+
handler: async (args, extra) => {
|
|
76
|
+
const typedArgs = args as { pair: string; order_ids: number[] };
|
|
77
|
+
const result = previewCancelOrders(typedArgs);
|
|
78
|
+
if (!result.ok) return result;
|
|
79
|
+
|
|
80
|
+
// elicitation 対応ホストでは preview → ユーザー確認 → cancel_orders までを
|
|
81
|
+
// このハンドラ内で完結させる。
|
|
82
|
+
if (clientSupportsElicitation(extra)) {
|
|
83
|
+
const server = (extra as { server?: ElicitCapableServer } | undefined)?.server;
|
|
84
|
+
if (server && typeof server.elicitInput === 'function') {
|
|
85
|
+
try {
|
|
86
|
+
const elicit = await server.elicitInput({
|
|
87
|
+
message: result.summary,
|
|
88
|
+
requestedSchema: {
|
|
89
|
+
type: 'object',
|
|
90
|
+
properties: {
|
|
91
|
+
confirmed: {
|
|
92
|
+
type: 'boolean',
|
|
93
|
+
title: `これら ${typedArgs.order_ids.length} 件の注文を一括キャンセルする`,
|
|
94
|
+
},
|
|
95
|
+
},
|
|
96
|
+
required: ['confirmed'],
|
|
97
|
+
},
|
|
98
|
+
});
|
|
99
|
+
if (elicit.action !== 'accept' || !elicit.content?.confirmed) {
|
|
100
|
+
return {
|
|
101
|
+
content: [{ type: 'text', text: 'ユーザーが一括キャンセル操作を取り消しました(elicitation)' }],
|
|
102
|
+
structuredContent: toStructured(result),
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
// 内部的に cancel_orders を実行。監査ログには route='elicitation' で記録される。
|
|
106
|
+
const cancelResult = await cancelOrders(
|
|
107
|
+
{
|
|
108
|
+
...typedArgs,
|
|
109
|
+
confirmation_token: result.data.confirmation_token,
|
|
110
|
+
token_expires_at: result.data.expires_at,
|
|
111
|
+
},
|
|
112
|
+
'elicitation',
|
|
113
|
+
);
|
|
114
|
+
const cancelText = cancelResult.ok ? cancelResult.summary : `Error: ${cancelResult.summary}`;
|
|
115
|
+
return {
|
|
116
|
+
content: [{ type: 'text', text: cancelText }],
|
|
117
|
+
structuredContent: toStructured(cancelResult),
|
|
118
|
+
};
|
|
119
|
+
} catch {
|
|
120
|
+
// elicitInput が想定外に失敗した場合はフォールバックに進む。
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// フォールバック: confirmation_token は LLM 可視テキストには含めない。
|
|
126
|
+
const text = [
|
|
127
|
+
result.summary,
|
|
128
|
+
'',
|
|
129
|
+
'※ confirmation_token はホスト UI / structuredContent 経由でのみ受け渡されます。',
|
|
130
|
+
' LLM はトークンを引用したり、ユーザー確認なしに cancel_orders を呼ばないでください。',
|
|
131
|
+
].join('\n');
|
|
132
|
+
return {
|
|
133
|
+
content: [{ type: 'text', text }],
|
|
134
|
+
structuredContent: toStructured(result),
|
|
135
|
+
};
|
|
136
|
+
},
|
|
137
|
+
};
|
|
@@ -0,0 +1,292 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* preview_order — 注文プレビューと確認トークン発行。
|
|
3
|
+
*
|
|
4
|
+
* 注文パラメータのバリデーションを行い、プレビューを表示し、
|
|
5
|
+
* create_order に渡す確認トークンを発行する。実際の発注は行わない。
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { toNum } from '../../lib/conversions.js';
|
|
9
|
+
import { formatPair, formatPrice } from '../../lib/formatter.js';
|
|
10
|
+
import { BITBANK_API_BASE, fetchJson } from '../../lib/http.js';
|
|
11
|
+
import { fail, ok, toStructured } from '../../lib/result.js';
|
|
12
|
+
import { generateToken } from '../../src/private/confirmation.js';
|
|
13
|
+
import { PreviewOrderInputSchema, PreviewOrderOutputSchema } from '../../src/private/schemas.js';
|
|
14
|
+
import type { ToolDefinition, ToolHandlerExtra } from '../../src/tool-definition.js';
|
|
15
|
+
import createOrder from './create_order.js';
|
|
16
|
+
|
|
17
|
+
/** 注文タイプごとの必須パラメータチェック */
|
|
18
|
+
function validateOrderParams(args: {
|
|
19
|
+
type: string;
|
|
20
|
+
price?: string;
|
|
21
|
+
trigger_price?: string;
|
|
22
|
+
post_only?: boolean;
|
|
23
|
+
}): string | null {
|
|
24
|
+
const { type, price, trigger_price, post_only } = args;
|
|
25
|
+
|
|
26
|
+
switch (type) {
|
|
27
|
+
case 'limit':
|
|
28
|
+
if (!price) return 'limit 注文には price(指値価格)が必須です';
|
|
29
|
+
break;
|
|
30
|
+
case 'market':
|
|
31
|
+
if (price) return 'market 注文に price は指定できません(成行で約定します)';
|
|
32
|
+
if (trigger_price) return 'market 注文に trigger_price は指定できません。逆指値は type="stop" を使用してください';
|
|
33
|
+
break;
|
|
34
|
+
case 'stop':
|
|
35
|
+
if (!trigger_price) return 'stop 注文には trigger_price(トリガー価格)が必須です';
|
|
36
|
+
if (price)
|
|
37
|
+
return 'stop 注文に price は指定できません。トリガー到達後に指値で発注したい場合は type="stop_limit" を使用してください';
|
|
38
|
+
break;
|
|
39
|
+
case 'stop_limit':
|
|
40
|
+
if (!trigger_price) return 'stop_limit 注文には trigger_price(トリガー価格)が必須です';
|
|
41
|
+
if (!price) return 'stop_limit 注文には price(トリガー到達後の指値価格)が必須です';
|
|
42
|
+
break;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
if (post_only && type !== 'limit') {
|
|
46
|
+
return 'post_only は limit 注文でのみ有効です';
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
return null;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function isPositiveNumericString(s: string): boolean {
|
|
53
|
+
const n = Number(s);
|
|
54
|
+
return Number.isFinite(n) && n > 0;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
async function validateTriggerPrice(pair: string, side: 'buy' | 'sell', triggerPrice: number): Promise<string | null> {
|
|
58
|
+
try {
|
|
59
|
+
const url = `${BITBANK_API_BASE}/${pair}/ticker`;
|
|
60
|
+
const json = (await fetchJson(url, { timeoutMs: 5000 })) as {
|
|
61
|
+
success?: number;
|
|
62
|
+
data?: { last?: string };
|
|
63
|
+
};
|
|
64
|
+
if (json?.success !== 1 || !json.data?.last) return null;
|
|
65
|
+
const currentPrice = toNum(json.data.last);
|
|
66
|
+
if (currentPrice == null) return null;
|
|
67
|
+
|
|
68
|
+
if (side === 'sell' && triggerPrice >= currentPrice) {
|
|
69
|
+
return [
|
|
70
|
+
`stop sell のトリガー価格(${formatPrice(triggerPrice)})が現在価格(${formatPrice(currentPrice)})以上のため、即時発動してしまいます。`,
|
|
71
|
+
'stop sell は「価格がトリガー以下に下落したとき」に発動します(損切り用)。',
|
|
72
|
+
'R1 上抜けで利確したい場合は limit sell を使用してください。',
|
|
73
|
+
].join('\n');
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
if (side === 'buy' && triggerPrice <= currentPrice) {
|
|
77
|
+
return [
|
|
78
|
+
`stop buy のトリガー価格(${formatPrice(triggerPrice)})が現在価格(${formatPrice(currentPrice)})以下のため、即時発動してしまいます。`,
|
|
79
|
+
'stop buy は「価格がトリガー以上に上昇したとき」に発動します(ブレイクアウト買い用)。',
|
|
80
|
+
'指定価格以下で買いたい場合は limit buy を使用してください。',
|
|
81
|
+
].join('\n');
|
|
82
|
+
}
|
|
83
|
+
} catch {
|
|
84
|
+
return null;
|
|
85
|
+
}
|
|
86
|
+
return null;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export default async function previewOrder(args: {
|
|
90
|
+
pair: string;
|
|
91
|
+
amount: string;
|
|
92
|
+
price?: string;
|
|
93
|
+
side: 'buy' | 'sell';
|
|
94
|
+
type: 'limit' | 'market' | 'stop' | 'stop_limit';
|
|
95
|
+
post_only?: boolean;
|
|
96
|
+
trigger_price?: string;
|
|
97
|
+
position_side?: 'long' | 'short';
|
|
98
|
+
}) {
|
|
99
|
+
const { pair, amount, price, side, type, post_only, trigger_price, position_side } = args;
|
|
100
|
+
|
|
101
|
+
// バリデーション
|
|
102
|
+
const paramError = validateOrderParams({ type, price, trigger_price, post_only });
|
|
103
|
+
if (paramError) {
|
|
104
|
+
return PreviewOrderOutputSchema.parse(fail(paramError, 'validation_error'));
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
if (!isPositiveNumericString(amount)) {
|
|
108
|
+
return PreviewOrderOutputSchema.parse(fail('amount は正の数値を指定してください', 'validation_error'));
|
|
109
|
+
}
|
|
110
|
+
if (price && !isPositiveNumericString(price)) {
|
|
111
|
+
return PreviewOrderOutputSchema.parse(fail('price は正の数値を指定してください', 'validation_error'));
|
|
112
|
+
}
|
|
113
|
+
if (trigger_price && !isPositiveNumericString(trigger_price)) {
|
|
114
|
+
return PreviewOrderOutputSchema.parse(fail('trigger_price は正の数値を指定してください', 'validation_error'));
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// stop / stop_limit: トリガー価格の妥当性チェック
|
|
118
|
+
if ((type === 'stop' || type === 'stop_limit') && trigger_price) {
|
|
119
|
+
const triggerError = await validateTriggerPrice(pair, side, Number(trigger_price));
|
|
120
|
+
if (triggerError) {
|
|
121
|
+
return PreviewOrderOutputSchema.parse(fail(triggerError, 'validation_error'));
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// 確認トークン生成
|
|
126
|
+
const tokenParams: Record<string, unknown> = { pair, amount, side, type };
|
|
127
|
+
if (price) tokenParams.price = price;
|
|
128
|
+
if (post_only != null) tokenParams.post_only = post_only;
|
|
129
|
+
if (trigger_price) tokenParams.trigger_price = trigger_price;
|
|
130
|
+
if (position_side) tokenParams.position_side = position_side;
|
|
131
|
+
|
|
132
|
+
const { token, expiresAt } = generateToken('create_order', tokenParams);
|
|
133
|
+
|
|
134
|
+
// プレビュー表示
|
|
135
|
+
const isJpy = pair.includes('jpy');
|
|
136
|
+
const sideLabel = side === 'buy' ? '買' : '売';
|
|
137
|
+
const fmtPrice = price ? (isJpy ? formatPrice(Number(price)) : price) : '成行';
|
|
138
|
+
const isMargin = !!position_side;
|
|
139
|
+
|
|
140
|
+
// 信用取引の操作ラベル
|
|
141
|
+
let marginLabel = '';
|
|
142
|
+
if (isMargin) {
|
|
143
|
+
const posLabel = position_side === 'long' ? 'ロング' : 'ショート';
|
|
144
|
+
const isOpen = (side === 'buy' && position_side === 'long') || (side === 'sell' && position_side === 'short');
|
|
145
|
+
marginLabel = isOpen ? `信用新規(${posLabel})` : `信用決済(${posLabel})`;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
const lines: string[] = [];
|
|
149
|
+
if (isMargin) {
|
|
150
|
+
lines.push(`📋 ${marginLabel} 注文プレビュー: ${formatPair(pair)}`);
|
|
151
|
+
} else {
|
|
152
|
+
lines.push(`📋 注文プレビュー: ${formatPair(pair)}`);
|
|
153
|
+
}
|
|
154
|
+
lines.push(` 方向: ${sideLabel} / タイプ: ${type}`);
|
|
155
|
+
if (marginLabel) {
|
|
156
|
+
lines.push(` 区分: ${marginLabel}`);
|
|
157
|
+
}
|
|
158
|
+
lines.push(` 数量: ${amount}`);
|
|
159
|
+
lines.push(` 価格: ${fmtPrice}`);
|
|
160
|
+
if (trigger_price) {
|
|
161
|
+
lines.push(` トリガー価格: ${isJpy ? formatPrice(Number(trigger_price)) : trigger_price}`);
|
|
162
|
+
}
|
|
163
|
+
if (post_only) {
|
|
164
|
+
lines.push(' Post Only: 有効');
|
|
165
|
+
}
|
|
166
|
+
if (isMargin) {
|
|
167
|
+
lines.push('');
|
|
168
|
+
lines.push('⚠️ 信用取引です。損失が保証金を超える可能性があります。');
|
|
169
|
+
}
|
|
170
|
+
lines.push('');
|
|
171
|
+
lines.push('⚠️ この注文はユーザーの最終確認(ホスト UI または elicitation)を経るまで発注されません。');
|
|
172
|
+
|
|
173
|
+
const summary = lines.join('\n');
|
|
174
|
+
|
|
175
|
+
const preview: Record<string, unknown> = { pair, amount, side, type };
|
|
176
|
+
if (price) preview.price = price;
|
|
177
|
+
if (trigger_price) preview.trigger_price = trigger_price;
|
|
178
|
+
if (post_only) preview.post_only = post_only;
|
|
179
|
+
if (position_side) preview.position_side = position_side;
|
|
180
|
+
|
|
181
|
+
return PreviewOrderOutputSchema.parse(
|
|
182
|
+
ok(summary, { confirmation_token: token, expires_at: expiresAt, preview }, { action: 'create_order' as const }),
|
|
183
|
+
);
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/**
|
|
187
|
+
* クライアントが elicitation/create に対応しているかを判定する。
|
|
188
|
+
* 非対応ホストでは従来挙動(structuredContent でトークンを返す)にフォールバックする。
|
|
189
|
+
*/
|
|
190
|
+
function clientSupportsElicitation(extra: ToolHandlerExtra | undefined): boolean {
|
|
191
|
+
const server = (extra as { server?: { getClientCapabilities?: () => unknown } } | undefined)?.server;
|
|
192
|
+
const caps = typeof server?.getClientCapabilities === 'function' ? server.getClientCapabilities() : undefined;
|
|
193
|
+
const elicitation = (caps as { elicitation?: unknown } | undefined)?.elicitation;
|
|
194
|
+
return Boolean(elicitation);
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
/** SDK の elicitInput を呼び出すための最小限の interface */
|
|
198
|
+
interface ElicitCapableServer {
|
|
199
|
+
elicitInput: (params: {
|
|
200
|
+
message: string;
|
|
201
|
+
requestedSchema: Record<string, unknown>;
|
|
202
|
+
}) => Promise<{ action: 'accept' | 'decline' | 'cancel'; content?: Record<string, unknown> }>;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
export const toolDef: ToolDefinition = {
|
|
206
|
+
name: 'preview_order',
|
|
207
|
+
description: [
|
|
208
|
+
'[Preview Order] 注文内容をプレビューし確認トークンを発行する。実際の発注は行わない。Private API。',
|
|
209
|
+
'create_order を実行するには、まずこのツールで確認トークンを取得する必要がある。',
|
|
210
|
+
'バリデーション(パラメータチェック、トリガー価格チェック)もここで実施する。',
|
|
211
|
+
'position_side を指定すると信用注文として扱う(ロング新規=buy+long, ロング決済=sell+long, ショート新規=sell+short, ショート決済=buy+short)。',
|
|
212
|
+
'⚠️ confirmation_token は LLM 可視テキストには含めない。ホスト UI または elicitation のユーザー確認を経て create_order が呼ばれる前提。LLM が独断でトークンを引用して create_order を呼ぶと二重発注になり得る。',
|
|
213
|
+
].join(' '),
|
|
214
|
+
inputSchema: PreviewOrderInputSchema,
|
|
215
|
+
// MCP Apps (SEP-1865): 対応ホストでは iframe 内に注文確認 UI を表示する。
|
|
216
|
+
// 非対応ホストでは無視され、従来のテキスト確認フローがそのまま動作する(Progressive Enhancement)。
|
|
217
|
+
_meta: {
|
|
218
|
+
ui: {
|
|
219
|
+
resourceUri: 'ui://order/confirm.html',
|
|
220
|
+
},
|
|
221
|
+
},
|
|
222
|
+
handler: async (args, extra) => {
|
|
223
|
+
const typedArgs = args as {
|
|
224
|
+
pair: string;
|
|
225
|
+
amount: string;
|
|
226
|
+
price?: string;
|
|
227
|
+
side: 'buy' | 'sell';
|
|
228
|
+
type: 'limit' | 'market' | 'stop' | 'stop_limit';
|
|
229
|
+
post_only?: boolean;
|
|
230
|
+
trigger_price?: string;
|
|
231
|
+
position_side?: 'long' | 'short';
|
|
232
|
+
};
|
|
233
|
+
const result = await previewOrder(typedArgs);
|
|
234
|
+
if (!result.ok) return result;
|
|
235
|
+
|
|
236
|
+
// elicitation 対応ホストでは preview → ユーザー確認 → create_order までを
|
|
237
|
+
// このハンドラ内で完結させる(LLM から見ると preview_order 1 回呼び出しで発注完了)。
|
|
238
|
+
// 非対応ホストでは従来通り structuredContent 経由でトークンを渡しフォールバックする。
|
|
239
|
+
if (clientSupportsElicitation(extra)) {
|
|
240
|
+
const server = (extra as { server?: ElicitCapableServer } | undefined)?.server;
|
|
241
|
+
if (server && typeof server.elicitInput === 'function') {
|
|
242
|
+
try {
|
|
243
|
+
const elicit = await server.elicitInput({
|
|
244
|
+
message: result.summary,
|
|
245
|
+
requestedSchema: {
|
|
246
|
+
type: 'object',
|
|
247
|
+
properties: {
|
|
248
|
+
confirmed: { type: 'boolean', title: 'この注文を発注する' },
|
|
249
|
+
},
|
|
250
|
+
required: ['confirmed'],
|
|
251
|
+
},
|
|
252
|
+
});
|
|
253
|
+
if (elicit.action !== 'accept' || !elicit.content?.confirmed) {
|
|
254
|
+
return {
|
|
255
|
+
content: [{ type: 'text', text: 'ユーザーが発注をキャンセルしました(elicitation)' }],
|
|
256
|
+
structuredContent: toStructured(result),
|
|
257
|
+
};
|
|
258
|
+
}
|
|
259
|
+
// 内部的に create_order を実行。監査ログには route='elicitation' で記録される。
|
|
260
|
+
const orderResult = await createOrder(
|
|
261
|
+
{
|
|
262
|
+
...typedArgs,
|
|
263
|
+
confirmation_token: result.data.confirmation_token,
|
|
264
|
+
token_expires_at: result.data.expires_at,
|
|
265
|
+
},
|
|
266
|
+
'elicitation',
|
|
267
|
+
);
|
|
268
|
+
const orderText = orderResult.ok ? orderResult.summary : `Error: ${orderResult.summary}`;
|
|
269
|
+
return {
|
|
270
|
+
content: [{ type: 'text', text: orderText }],
|
|
271
|
+
structuredContent: toStructured(orderResult),
|
|
272
|
+
};
|
|
273
|
+
} catch {
|
|
274
|
+
// elicitInput が想定外に失敗した場合はフォールバックに進む。
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
// フォールバック: confirmation_token は LLM 可視テキストには含めず、
|
|
280
|
+
// structuredContent 側にだけ残す。SEP-1865 UI ボタンや Inspector はこちらを参照する。
|
|
281
|
+
const text = [
|
|
282
|
+
result.summary,
|
|
283
|
+
'',
|
|
284
|
+
'※ confirmation_token はホスト UI / structuredContent 経由でのみ受け渡されます。',
|
|
285
|
+
' LLM はトークンを引用したり、ユーザー確認なしに create_order を呼ばないでください。',
|
|
286
|
+
].join('\n');
|
|
287
|
+
return {
|
|
288
|
+
content: [{ type: 'text', text }],
|
|
289
|
+
structuredContent: toStructured(result),
|
|
290
|
+
};
|
|
291
|
+
},
|
|
292
|
+
};
|