bitbank-lab-mcp 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (147) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +388 -0
  3. package/assets/lightweight-charts.standalone.js +7 -0
  4. package/bin/bitbank-lab-mcp.js +20 -0
  5. package/lib/cache.ts +70 -0
  6. package/lib/candle-utils.ts +48 -0
  7. package/lib/candle-validate.ts +434 -0
  8. package/lib/conversions.ts +25 -0
  9. package/lib/datetime.ts +157 -0
  10. package/lib/depth-analysis.ts +51 -0
  11. package/lib/error.ts +15 -0
  12. package/lib/formatter.ts +296 -0
  13. package/lib/get-depth.ts +111 -0
  14. package/lib/http.ts +132 -0
  15. package/lib/indicator-config.ts +39 -0
  16. package/lib/indicator_buffer.ts +41 -0
  17. package/lib/indicators.ts +579 -0
  18. package/lib/logger.ts +120 -0
  19. package/lib/ma-snapshot-utils.ts +277 -0
  20. package/lib/math.ts +89 -0
  21. package/lib/pattern-diagrams.ts +562 -0
  22. package/lib/result.ts +104 -0
  23. package/lib/validate.ts +154 -0
  24. package/lib/volatility.ts +132 -0
  25. package/package.json +79 -0
  26. package/src/env.ts +4 -0
  27. package/src/handlers/analyzeCandlePatternsHandler.ts +383 -0
  28. package/src/handlers/analyzeFibonacciHandler.ts +54 -0
  29. package/src/handlers/analyzeIndicatorsHandler.ts +682 -0
  30. package/src/handlers/analyzeMarketSignalHandler.ts +272 -0
  31. package/src/handlers/analyzeMyPortfolioHandler.ts +800 -0
  32. package/src/handlers/detectPatternsHandler.ts +77 -0
  33. package/src/handlers/detectPatternsViewsHandler.ts +518 -0
  34. package/src/handlers/getTickersJpyHandler.ts +145 -0
  35. package/src/handlers/getVolatilityMetricsHandler.ts +234 -0
  36. package/src/handlers/portfolio/calc.ts +549 -0
  37. package/src/handlers/portfolio/fetch.ts +318 -0
  38. package/src/handlers/portfolio/types.ts +170 -0
  39. package/src/handlers/renderChartSvgHandler.ts +69 -0
  40. package/src/handlers/runBacktestHandler.ts +70 -0
  41. package/src/http.ts +107 -0
  42. package/src/private/auth.ts +104 -0
  43. package/src/private/client.ts +298 -0
  44. package/src/private/config.ts +25 -0
  45. package/src/private/confirmation.ts +185 -0
  46. package/src/private/schemas.ts +866 -0
  47. package/src/prompts.ts +2296 -0
  48. package/src/resources/app-resources.ts +79 -0
  49. package/src/schema/analysis.ts +942 -0
  50. package/src/schema/backtest.ts +100 -0
  51. package/src/schema/base.ts +88 -0
  52. package/src/schema/candle-validate.ts +135 -0
  53. package/src/schema/chart.ts +399 -0
  54. package/src/schema/index.ts +11 -0
  55. package/src/schema/indicators.ts +125 -0
  56. package/src/schema/market-data.ts +298 -0
  57. package/src/schema/patterns.ts +382 -0
  58. package/src/schema/types.ts +97 -0
  59. package/src/schemas.d.ts +37 -0
  60. package/src/schemas.ts +7 -0
  61. package/src/server.ts +405 -0
  62. package/src/tool-definition.ts +44 -0
  63. package/src/tool-registry.ts +174 -0
  64. package/src/types/express-shim.d.ts +9 -0
  65. package/src/types/schemas.generated.d.ts +23 -0
  66. package/tools/analyze_bb_snapshot.ts +385 -0
  67. package/tools/analyze_candle_patterns.ts +810 -0
  68. package/tools/analyze_currency_strength.ts +273 -0
  69. package/tools/analyze_ema_snapshot.ts +183 -0
  70. package/tools/analyze_fibonacci.ts +530 -0
  71. package/tools/analyze_ichimoku_snapshot.ts +606 -0
  72. package/tools/analyze_indicators.ts +691 -0
  73. package/tools/analyze_market_signal.ts +665 -0
  74. package/tools/analyze_mtf_fibonacci.ts +273 -0
  75. package/tools/analyze_mtf_sma.ts +175 -0
  76. package/tools/analyze_sma_snapshot.ts +146 -0
  77. package/tools/analyze_stoch_snapshot.ts +276 -0
  78. package/tools/analyze_support_resistance.ts +817 -0
  79. package/tools/analyze_volume_profile.ts +546 -0
  80. package/tools/chart/ichimoku-cloud.ts +113 -0
  81. package/tools/chart/render-depth.ts +139 -0
  82. package/tools/chart/render-sub-panels.ts +208 -0
  83. package/tools/chart/svg-utils.ts +102 -0
  84. package/tools/detect_macd_cross.ts +691 -0
  85. package/tools/detect_patterns.ts +424 -0
  86. package/tools/detect_whale_events.ts +181 -0
  87. package/tools/get_candles.ts +487 -0
  88. package/tools/get_flow_metrics.ts +596 -0
  89. package/tools/get_orderbook.ts +540 -0
  90. package/tools/get_ticker.ts +132 -0
  91. package/tools/get_tickers_jpy.ts +240 -0
  92. package/tools/get_transactions.ts +209 -0
  93. package/tools/get_volatility_metrics.ts +302 -0
  94. package/tools/patterns/aftermath.ts +212 -0
  95. package/tools/patterns/config.ts +151 -0
  96. package/tools/patterns/detect_doubles.ts +650 -0
  97. package/tools/patterns/detect_hs.ts +635 -0
  98. package/tools/patterns/detect_pennants.ts +373 -0
  99. package/tools/patterns/detect_triangles.ts +820 -0
  100. package/tools/patterns/detect_triples.ts +633 -0
  101. package/tools/patterns/detect_wedges.ts +1072 -0
  102. package/tools/patterns/helpers.ts +517 -0
  103. package/tools/patterns/index.ts +40 -0
  104. package/tools/patterns/regression.ts +153 -0
  105. package/tools/patterns/smoothing.ts +168 -0
  106. package/tools/patterns/swing.ts +91 -0
  107. package/tools/patterns/types.ts +193 -0
  108. package/tools/prepare_chart_data.ts +294 -0
  109. package/tools/prepare_depth_data.ts +189 -0
  110. package/tools/private/analyze_my_portfolio.ts +21 -0
  111. package/tools/private/cancel_order.ts +127 -0
  112. package/tools/private/cancel_orders.ts +121 -0
  113. package/tools/private/create_order.ts +236 -0
  114. package/tools/private/get_margin_positions.ts +134 -0
  115. package/tools/private/get_margin_status.ts +155 -0
  116. package/tools/private/get_margin_trade_history.ts +156 -0
  117. package/tools/private/get_my_assets.ts +207 -0
  118. package/tools/private/get_my_deposit_withdrawal.ts +500 -0
  119. package/tools/private/get_my_orders.ts +157 -0
  120. package/tools/private/get_my_trade_history.ts +229 -0
  121. package/tools/private/get_order.ts +95 -0
  122. package/tools/private/get_orders_info.ts +90 -0
  123. package/tools/private/preview_cancel_order.ts +172 -0
  124. package/tools/private/preview_cancel_orders.ts +137 -0
  125. package/tools/private/preview_order.ts +292 -0
  126. package/tools/render_candle_pattern_diagram.ts +389 -0
  127. package/tools/render_chart_svg.ts +799 -0
  128. package/tools/render_depth_svg.ts +274 -0
  129. package/tools/trading_process/index.ts +7 -0
  130. package/tools/trading_process/lib/backtest_engine.ts +252 -0
  131. package/tools/trading_process/lib/equity.ts +131 -0
  132. package/tools/trading_process/lib/fetch_candles.ts +181 -0
  133. package/tools/trading_process/lib/sma.ts +62 -0
  134. package/tools/trading_process/lib/strategies/bb_breakout.ts +141 -0
  135. package/tools/trading_process/lib/strategies/index.ts +52 -0
  136. package/tools/trading_process/lib/strategies/macd_cross.ts +256 -0
  137. package/tools/trading_process/lib/strategies/rsi.ts +133 -0
  138. package/tools/trading_process/lib/strategies/sma_cross.ts +214 -0
  139. package/tools/trading_process/lib/strategies/types.ts +118 -0
  140. package/tools/trading_process/lib/svg_to_png.ts +64 -0
  141. package/tools/trading_process/render_backtest_chart_generic.ts +729 -0
  142. package/tools/trading_process/run_backtest.ts +243 -0
  143. package/tools/trading_process/types.ts +85 -0
  144. package/tools/validate_candle_data.ts +260 -0
  145. package/tsconfig.json +17 -0
  146. package/ui/cancel-confirm/dist/cancel-confirm.html +99 -0
  147. package/ui/order-confirm/dist/order-confirm.html +99 -0
@@ -0,0 +1,318 @@
1
+ /**
2
+ * portfolio/fetch — API データ取得レイヤー。
3
+ *
4
+ * Private API(入出金・約定履歴)のページネーション、
5
+ * Public API(ticker・キャンドル)の取得、テクニカル分析の取得を担当。
6
+ */
7
+
8
+ import { dayjs } from '../../../lib/datetime.js';
9
+ import analyzeIndicators from '../../../tools/analyze_indicators.js';
10
+ import type { BitbankPrivateClient } from '../../private/client.js';
11
+ import {
12
+ type CandlePriceData,
13
+ type DepositWithdrawalData,
14
+ type RawDeposit,
15
+ type RawTrade,
16
+ type RawWithdrawal,
17
+ type TechnicalSummary,
18
+ tryGet,
19
+ } from './types.js';
20
+
21
+ // ── Configuration ──
22
+ const MAX_PAGES = 10;
23
+ const PAGE_SIZE = 1000;
24
+
25
+ async function paginateDeposits(
26
+ client: BitbankPrivateClient,
27
+ baseParams: Record<string, string>,
28
+ sinceMs?: number,
29
+ ): Promise<{ deposits: RawDeposit[]; complete: boolean; error?: string }> {
30
+ const all: RawDeposit[] = [];
31
+ let since: string | undefined = sinceMs != null ? String(sinceMs) : undefined;
32
+ for (let page = 0; page < MAX_PAGES; page++) {
33
+ const params = { ...baseParams, count: String(PAGE_SIZE), ...(since ? { since } : {}) };
34
+ const result = await tryGet<{ deposits: RawDeposit[] }>(client, '/v1/user/deposit_history', params);
35
+ if (!result.ok) {
36
+ return { deposits: all, complete: false, error: result.error };
37
+ }
38
+ const batch = result.data.deposits || [];
39
+ all.push(...batch);
40
+ if (batch.length < PAGE_SIZE) {
41
+ return { deposits: all, complete: true };
42
+ }
43
+ // 次ページ: 最後のレコードの confirmed_at + 1ms を since に
44
+ const lastTs = batch[batch.length - 1]?.confirmed_at;
45
+ if (!lastTs) break;
46
+ since = String(lastTs + 1);
47
+ }
48
+ return { deposits: all, complete: false };
49
+ }
50
+
51
+ async function paginateWithdrawals(
52
+ client: BitbankPrivateClient,
53
+ baseParams: Record<string, string>,
54
+ sinceMs?: number,
55
+ ): Promise<{ withdrawals: RawWithdrawal[]; complete: boolean; error?: string }> {
56
+ const all: RawWithdrawal[] = [];
57
+ let since: string | undefined = sinceMs != null ? String(sinceMs) : undefined;
58
+ for (let page = 0; page < MAX_PAGES; page++) {
59
+ const params = { ...baseParams, count: String(PAGE_SIZE), ...(since ? { since } : {}) };
60
+ const result = await tryGet<{ withdrawals: RawWithdrawal[] }>(client, '/v1/user/withdrawal_history', params);
61
+ if (!result.ok) {
62
+ return { withdrawals: all, complete: false, error: result.error };
63
+ }
64
+ const batch = result.data.withdrawals || [];
65
+ all.push(...batch);
66
+ if (batch.length < PAGE_SIZE) {
67
+ return { withdrawals: all, complete: true };
68
+ }
69
+ const lastTs = batch[batch.length - 1]?.requested_at;
70
+ if (!lastTs) break;
71
+ since = String(lastTs + 1);
72
+ }
73
+ return { withdrawals: all, complete: false };
74
+ }
75
+
76
+ /** ページネーション付きで約定履歴を取得(最大 MAX_PAGES ページ、古い順) */
77
+ export async function paginateTrades(
78
+ client: BitbankPrivateClient,
79
+ sinceMs?: number,
80
+ ): Promise<{ trades: RawTrade[]; truncated: boolean }> {
81
+ const all: RawTrade[] = [];
82
+ let since: string | undefined = sinceMs != null ? String(sinceMs) : undefined;
83
+ for (let page = 0; page < MAX_PAGES; page++) {
84
+ const params: Record<string, string> = { count: String(PAGE_SIZE), order: 'asc' };
85
+ if (since) params.since = since;
86
+ const result = await tryGet<{ trades: RawTrade[] }>(client, '/v1/user/spot/trade_history', params);
87
+ if (!result.ok) break;
88
+ const batch = result.data.trades || [];
89
+ all.push(...batch);
90
+ if (batch.length < PAGE_SIZE) return { trades: all, truncated: false };
91
+ // 次ページ: 最後の約定の executed_at + 1ms を since に
92
+ const lastTs = batch[batch.length - 1]?.executed_at;
93
+ if (!lastTs) break;
94
+ since = String(lastTs + 1);
95
+ }
96
+ // MAX_PAGES 到達 or エラーで抜けた場合、最終バッチが満杯なら打ち切り
97
+ const truncated = all.length > 0 && all.length % PAGE_SIZE === 0;
98
+ return { trades: all, truncated };
99
+ }
100
+
101
+ /**
102
+ * 入出金履歴を取得する(JPY + 暗号資産の両方、ページネーション対応)。
103
+ * sinceMs を指定すると、その日時以降のデータのみ取得する。
104
+ * 全リクエスト失敗時は null を返す。一部失敗時は warnings 付きで返す。
105
+ */
106
+ export async function fetchDepositWithdrawal(
107
+ client: BitbankPrivateClient,
108
+ sinceMs?: number,
109
+ ): Promise<DepositWithdrawalData | null> {
110
+ try {
111
+ const [cryptoDepResult, jpyDepResult, cryptoWdResult, jpyWdResult] = await Promise.all([
112
+ paginateDeposits(client, {}, sinceMs),
113
+ paginateDeposits(client, { asset: 'jpy' }, sinceMs),
114
+ paginateWithdrawals(client, {}, sinceMs),
115
+ paginateWithdrawals(client, { asset: 'jpy' }, sinceMs),
116
+ ]);
117
+
118
+ const warnings: string[] = [];
119
+ const apiResults = [
120
+ { error: cryptoDepResult.error, label: '暗号資産入庫履歴' },
121
+ { error: jpyDepResult.error, label: 'JPY入金履歴' },
122
+ { error: cryptoWdResult.error, label: '暗号資産出庫履歴' },
123
+ { error: jpyWdResult.error, label: 'JPY出金履歴' },
124
+ ];
125
+ for (const { error, label } of apiResults) {
126
+ if (error) {
127
+ warnings.push(`${label}の取得に失敗: ${error}`);
128
+ }
129
+ }
130
+
131
+ // 全チャネルでデータゼロかつエラーあり = 全失敗
132
+ const totalItems =
133
+ cryptoDepResult.deposits.length +
134
+ jpyDepResult.deposits.length +
135
+ cryptoWdResult.withdrawals.length +
136
+ jpyWdResult.withdrawals.length;
137
+ if (totalItems === 0 && warnings.length === 4) {
138
+ return { deposits: [], withdrawals: [], warnings, allFailed: true, isComplete: false };
139
+ }
140
+
141
+ // 成功分からデータを収集
142
+ const rawDeposits = [...cryptoDepResult.deposits, ...jpyDepResult.deposits];
143
+ const rawWithdrawals = [...cryptoWdResult.withdrawals, ...jpyWdResult.withdrawals];
144
+
145
+ // UUID で重複排除
146
+ const seenDeposit = new Set<string>();
147
+ const allDeposits = rawDeposits.filter((d) => {
148
+ if (seenDeposit.has(d.uuid)) return false;
149
+ seenDeposit.add(d.uuid);
150
+ return true;
151
+ });
152
+
153
+ const seenWithdrawal = new Set<string>();
154
+ const allWithdrawals = rawWithdrawals.filter((w) => {
155
+ if (seenWithdrawal.has(w.uuid)) return false;
156
+ seenWithdrawal.add(w.uuid);
157
+ return true;
158
+ });
159
+
160
+ const isComplete =
161
+ cryptoDepResult.complete && jpyDepResult.complete && cryptoWdResult.complete && jpyWdResult.complete;
162
+
163
+ return { deposits: allDeposits, withdrawals: allWithdrawals, warnings, allFailed: false, isComplete };
164
+ } catch {
165
+ return null;
166
+ }
167
+ }
168
+
169
+ // ── Ticker 取得 ──
170
+
171
+ export async function fetchTickerPrices(): Promise<Map<string, number>> {
172
+ const prices = new Map<string, number>();
173
+ try {
174
+ const res = await fetch('https://public.bitbank.cc/tickers_jpy', {
175
+ signal: AbortSignal.timeout(3000),
176
+ });
177
+ if (!res.ok) return prices;
178
+ const json = (await res.json()) as { success?: number; data?: Array<{ pair: string; last: string }> };
179
+ if (json.success !== 1 || !Array.isArray(json.data)) return prices;
180
+ for (const item of json.data) {
181
+ const asset = item.pair.replace('_jpy', '');
182
+ const last = Number(item.last);
183
+ if (Number.isFinite(last) && last > 0) prices.set(asset, last);
184
+ }
185
+ } catch {
186
+ /* ticker 失敗は非致命的 */
187
+ }
188
+ return prices;
189
+ }
190
+
191
+ /**
192
+ * 1dayキャンドルから期初始値 + 全日次始値マップを一括取得する。
193
+ * boundaryPrices: 既存の年初/月初/日初パフォーマンス計算用。
194
+ * dailyPrices: 資産推移時系列(equity series)構築用。asset → (candleTimestampMs → openPrice)。
195
+ */
196
+ export async function fetchCandlePriceData(
197
+ pairs: string[],
198
+ yearStartMs: number,
199
+ monthStartMs: number,
200
+ dayStartMs: number,
201
+ ): Promise<CandlePriceData> {
202
+ const boundaryPrices = new Map<string, { yearStart?: number; monthStart?: number; dayStart?: number }>();
203
+ const dailyPrices = new Map<string, Map<number, number>>();
204
+ const nowJst = dayjs().tz('Asia/Tokyo');
205
+ const year = nowJst.year();
206
+
207
+ const promises = pairs.map(async (pair) => {
208
+ try {
209
+ const url = `https://public.bitbank.cc/${pair}/candlestick/1day/${year}`;
210
+ const res = await fetch(url, { signal: AbortSignal.timeout(5000) });
211
+ if (!res.ok) return;
212
+ const json = (await res.json()) as {
213
+ success?: number;
214
+ data?: { candlestick?: Array<{ ohlcv?: Array<Array<string | number>> }> };
215
+ };
216
+ if (json.success !== 1) return;
217
+
218
+ const ohlcv = json.data?.candlestick?.[0]?.ohlcv;
219
+ if (!Array.isArray(ohlcv) || ohlcv.length === 0) return;
220
+
221
+ const asset = pair.replace('_jpy', '');
222
+ let yearStartPrice: number | undefined;
223
+ let monthStartPrice: number | undefined;
224
+ let dayStartPrice: number | undefined;
225
+ const priceByDate = new Map<number, number>();
226
+
227
+ for (const candle of ohlcv) {
228
+ const ts = Number(candle[5]);
229
+ const open = Number(candle[0]);
230
+ if (!Number.isFinite(open) || open <= 0) continue;
231
+
232
+ // Normalize to JST midnight so keys match buildEquitySeries date lookups
233
+ const jstMidnight = dayjs(ts).tz('Asia/Tokyo').startOf('day').valueOf();
234
+ priceByDate.set(jstMidnight, open);
235
+
236
+ if (yearStartPrice == null && ts >= yearStartMs) {
237
+ yearStartPrice = open;
238
+ }
239
+ if (monthStartPrice == null && ts >= monthStartMs) {
240
+ monthStartPrice = open;
241
+ }
242
+ if (dayStartPrice == null && ts >= dayStartMs) {
243
+ dayStartPrice = open;
244
+ }
245
+ }
246
+
247
+ boundaryPrices.set(asset, { yearStart: yearStartPrice, monthStart: monthStartPrice, dayStart: dayStartPrice });
248
+ dailyPrices.set(asset, priceByDate);
249
+ } catch {
250
+ // Non-fatal: price unavailable for this pair
251
+ }
252
+ });
253
+
254
+ await Promise.all(promises);
255
+ return { boundaryPrices, dailyPrices };
256
+ }
257
+
258
+ // ── テクニカル分析 ──
259
+
260
+ export async function fetchTechnical(pairs: string[]): Promise<TechnicalSummary[]> {
261
+ const results: TechnicalSummary[] = [];
262
+ // 並列で取得(最大5通貨に制限)
263
+ const targets = pairs.slice(0, 5);
264
+ const promises = targets.map(async (pair) => {
265
+ try {
266
+ const res = await analyzeIndicators(pair, '1day', 60);
267
+ if (!res?.ok) return null;
268
+ const data = res.data;
269
+ const indicators = data.indicators;
270
+ const rsi14 = indicators.RSI_14 != null ? Number(indicators.RSI_14) : undefined;
271
+ const sma25 = indicators.SMA_25 != null ? Number(indicators.SMA_25) : undefined;
272
+ const lastClose = data.normalized?.at?.(-1)?.close;
273
+
274
+ let smaDeviation: number | undefined;
275
+ if (sma25 && lastClose && Number.isFinite(sma25) && Number.isFinite(lastClose)) {
276
+ smaDeviation = Math.round(((lastClose - sma25) / sma25) * 10000) / 100;
277
+ }
278
+
279
+ // trend は analyzeIndicators の data に含まれる
280
+ const trend = data.trend;
281
+
282
+ // 総合判定: RSI とトレンドを組み合わせて判定
283
+ // analyzeIndicators の trend は uptrend/strong_uptrend/downtrend/strong_downtrend/sideways
284
+ const isBullish = trend === 'uptrend' || trend === 'strong_uptrend';
285
+ const isBearish = trend === 'downtrend' || trend === 'strong_downtrend';
286
+ let signal = 'neutral';
287
+ if (rsi14 != null) {
288
+ if (rsi14 >= 70) {
289
+ // RSI 買われすぎ: 上昇トレンド中なら強気維持、それ以外は過熱警告
290
+ signal = isBullish ? 'bullish' : 'overbought';
291
+ } else if (rsi14 <= 30) {
292
+ // RSI 売られすぎ: 下落トレンド中は弱気継続(落ちるナイフ)、それ以外は反発期待
293
+ signal = isBearish ? 'bearish' : 'oversold';
294
+ }
295
+ }
296
+ if (signal === 'neutral') {
297
+ if (isBullish) signal = 'bullish';
298
+ else if (isBearish) signal = 'bearish';
299
+ }
300
+
301
+ return {
302
+ pair,
303
+ trend,
304
+ rsi_14: rsi14 != null ? Math.round(rsi14 * 100) / 100 : undefined,
305
+ sma_deviation_pct: smaDeviation,
306
+ signal,
307
+ };
308
+ } catch {
309
+ return null;
310
+ }
311
+ });
312
+
313
+ const settled = await Promise.all(promises);
314
+ for (const r of settled) {
315
+ if (r) results.push(r);
316
+ }
317
+ return results;
318
+ }
@@ -0,0 +1,170 @@
1
+ /**
2
+ * portfolio/types — analyzeMyPortfolioHandler で使用する型定義。
3
+ */
4
+
5
+ import { getErrorMessage } from '../../../lib/error.js';
6
+ import type { BitbankPrivateClient } from '../../private/client.js';
7
+
8
+ // ── Private API レスポンス型 ──
9
+
10
+ export interface RawAsset {
11
+ asset: string;
12
+ free_amount: string;
13
+ onhand_amount: string;
14
+ locked_amount: string;
15
+ amount_precision: number;
16
+ withdrawal_fee: { min: string; max: string } | string;
17
+ stop_deposit: boolean;
18
+ stop_withdrawal: boolean;
19
+ }
20
+
21
+ export interface RawTrade {
22
+ trade_id: number;
23
+ pair: string;
24
+ order_id: number;
25
+ 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
+ executed_at: number;
33
+ }
34
+
35
+ export interface RawDeposit {
36
+ uuid: string;
37
+ asset: string;
38
+ amount: string;
39
+ status: string;
40
+ found_at: number;
41
+ confirmed_at: number;
42
+ }
43
+
44
+ export interface RawWithdrawal {
45
+ uuid: string;
46
+ asset: string;
47
+ amount: string;
48
+ fee?: string;
49
+ status: string;
50
+ requested_at: number;
51
+ }
52
+
53
+ export interface DepositWithdrawalData {
54
+ deposits: RawDeposit[];
55
+ withdrawals: RawWithdrawal[];
56
+ /** 一部の API リクエストが失敗した場合の警告メッセージ */
57
+ warnings: string[];
58
+ /** 全リクエストが失敗した場合 true */
59
+ allFailed: boolean;
60
+ /** 全履歴を取得できたか(false = API 件数上限に達した) */
61
+ isComplete: boolean;
62
+ }
63
+
64
+ /** 個別 API リクエストの結果をラップ */
65
+ export type FetchResult<T> = { ok: true; data: T } | { ok: false; error: string };
66
+
67
+ // ── 損益計算 ──
68
+
69
+ export interface PnlResult {
70
+ avg_buy_price: number | undefined;
71
+ cost_basis: number | undefined;
72
+ realized_pnl: number;
73
+ trade_count: number;
74
+ }
75
+
76
+ export interface PeriodRealizedPnl {
77
+ /** 期間内の合計実現損益(JPY) */
78
+ realized_pnl: number;
79
+ /** 期間内の売却約定件数 */
80
+ sell_count: number;
81
+ /** 期間の開始日時(ISO8601 JST) */
82
+ period_start: string;
83
+ /** 期間の終了日時(ISO8601 JST) = 取得時点 */
84
+ period_end: string;
85
+ }
86
+
87
+ // ── 入出金サマリー ──
88
+
89
+ export interface DepositWithdrawalSummary {
90
+ total_jpy_deposited: number;
91
+ total_jpy_withdrawn: number;
92
+ net_jpy_invested: number;
93
+ crypto_deposit_count: number;
94
+ crypto_deposit_estimated_jpy: number | undefined;
95
+ crypto_withdrawal_count: number;
96
+ account_return_pct: number | undefined;
97
+ account_return_jpy: number | undefined;
98
+ is_complete: boolean;
99
+ analysis_basis: 'deposit_withdrawal' | 'trade_only';
100
+ }
101
+
102
+ export interface PeriodDWSummary {
103
+ jpy_deposited: number;
104
+ jpy_withdrawn: number;
105
+ net_jpy: number;
106
+ crypto_deposit_count: number;
107
+ crypto_deposit_estimated_jpy: number | undefined;
108
+ crypto_withdrawal_count: number;
109
+ crypto_withdrawal_estimated_jpy: number | undefined;
110
+ period_start: string;
111
+ period_end: string;
112
+ }
113
+
114
+ // ── パフォーマンス ──
115
+
116
+ export interface PeriodPerformance {
117
+ start_value_jpy: number;
118
+ current_value_jpy: number;
119
+ change_jpy: number;
120
+ change_pct: number | undefined;
121
+ net_flow_jpy: number;
122
+ withdrawal_fee_jpy: number;
123
+ adjusted_change_jpy: number;
124
+ adjusted_change_pct: number | undefined;
125
+ period_start: string;
126
+ period_end: string;
127
+ note: string;
128
+ }
129
+
130
+ export interface CandlePriceData {
131
+ boundaryPrices: Map<string, { yearStart?: number; monthStart?: number; dayStart?: number }>;
132
+ dailyPrices: Map<string, Map<number, number>>;
133
+ }
134
+
135
+ export interface EquityPoint {
136
+ timestamp: string;
137
+ value_jpy: number;
138
+ }
139
+
140
+ export interface PeriodNetFlowResult {
141
+ /** 純入出金額(元本移動のみ。出金手数料は含まない) */
142
+ net_flow_jpy: number;
143
+ /** 期間中の出金手数料合計(JPY)。コストとして performance に残る */
144
+ withdrawal_fee_jpy: number;
145
+ }
146
+
147
+ // ── テクニカル ──
148
+
149
+ export interface TechnicalSummary {
150
+ pair: string;
151
+ trend?: string;
152
+ rsi_14?: number;
153
+ sma_deviation_pct?: number;
154
+ signal?: string;
155
+ }
156
+
157
+ // ── API ヘルパー ──
158
+
159
+ export async function tryGet<T>(
160
+ client: BitbankPrivateClient,
161
+ path: string,
162
+ params?: Record<string, string>,
163
+ ): Promise<FetchResult<T>> {
164
+ try {
165
+ const data = await client.get<T>(path, params);
166
+ return { ok: true, data };
167
+ } catch (err) {
168
+ return { ok: false, error: getErrorMessage(err) };
169
+ }
170
+ }
@@ -0,0 +1,69 @@
1
+ import type { z } from 'zod';
2
+ import { failFromValidation } from '../../lib/result.js';
3
+ import { ensurePair } from '../../lib/validate.js';
4
+ import renderChartSvg from '../../tools/render_chart_svg.js';
5
+ import { RenderChartSvgInputSchema, RenderChartSvgOutputSchema } from '../schemas.js';
6
+ import type { ToolDefinition } from '../tool-definition.js';
7
+
8
+ type ChartOutput = z.infer<typeof RenderChartSvgOutputSchema>;
9
+
10
+ export const toolDef: ToolDefinition = {
11
+ name: 'render_chart_svg',
12
+ description:
13
+ '[SVG file / PNG save] ローソク足・ラインチャートをサーバー側で SVG/PNG に生成。\nクライアント側で描画可能な場合は prepare_chart_data を優先。\nユーザーが SVG/PNG 保存を明示した場合のみ使用。自発的呼び出し禁止。\ndetect_patterns の overlays を渡してパターン描画可能。\nオプションのインジケーター(SMA/EMA/BB/一目均衡表)はユーザーが明示的に要求した場合のみ指定すること。デフォルトではすべてオフ。',
14
+ inputSchema: RenderChartSvgInputSchema,
15
+ handler: async (args: Record<string, unknown>) => {
16
+ const chk = ensurePair(args.pair || 'btc_jpy');
17
+ if (!chk.ok) return failFromValidation(chk);
18
+ const raw = await renderChartSvg({ ...args, pair: chk.pair } as z.infer<typeof RenderChartSvgInputSchema>);
19
+ const parsed: ChartOutput = RenderChartSvgOutputSchema.parse(raw);
20
+
21
+ const data = ((parsed as Record<string, unknown>).data as Record<string, unknown>) ?? {};
22
+ const meta = ((parsed as Record<string, unknown>).meta as Record<string, unknown>) ?? {};
23
+ const pair = String(meta.pair || (args as Record<string, unknown>).pair || 'pair').toUpperCase();
24
+ const type = String(meta.type || (args as Record<string, unknown>).type || '1day');
25
+
26
+ if (!('svg' in data) || !data.svg) {
27
+ const txt = String(parsed.summary || 'chart rendered (no svg)');
28
+ return { content: [{ type: 'text', text: txt }], structuredContent: parsed as Record<string, unknown> };
29
+ }
30
+
31
+ // run_backtest と同じ方式: 生 SVG をテキストとしてそのまま返す
32
+ const id = String(meta.identifier || `${pair}-${type}-${Date.now()}`);
33
+ const ttl = String(meta.title || `${pair} ${type} chart`);
34
+ const range = meta.range as { start?: string; end?: string } | undefined;
35
+ const rangeLine = range ? `Period: ${range.start} \u2013 ${range.end}` : '';
36
+ const indicators = meta.indicators as string[] | undefined;
37
+ const indLine = Array.isArray(indicators) && indicators.length ? `Indicators: ${indicators.join(', ')}` : '';
38
+ const legendLines =
39
+ 'legend' in data && data.legend
40
+ ? Object.entries(data.legend)
41
+ .map(([k, v]) => `${k}: ${String(v)}`)
42
+ .join(' / ')
43
+ : '';
44
+
45
+ const summary = [`${pair} ${type} chart`, rangeLine, indLine, legendLines].filter(Boolean).join(' | ');
46
+
47
+ const svgBlock = [
48
+ '',
49
+ '--- Chart SVG ---',
50
+ `identifier: ${id}`,
51
+ `title: ${ttl}`,
52
+ 'type: image/svg+xml',
53
+ '',
54
+ String(data.svg),
55
+ ].join('\n');
56
+
57
+ return {
58
+ content: [{ type: 'text', text: summary + svgBlock }],
59
+ structuredContent: {
60
+ ...(parsed as Record<string, unknown>),
61
+ artifactHint: {
62
+ renderHint: 'ARTIFACT_REQUIRED',
63
+ displayType: 'image/svg+xml',
64
+ source: 'inline_svg',
65
+ },
66
+ },
67
+ };
68
+ },
69
+ };
@@ -0,0 +1,70 @@
1
+ import { toStructured } from '../../lib/result.js';
2
+ import { runBacktest } from '../../tools/trading_process/index.js';
3
+ import { RunBacktestInputSchema } from '../schemas.js';
4
+ import type { ToolDefinition } from '../tool-definition.js';
5
+
6
+ export const toolDef: ToolDefinition = {
7
+ name: 'run_backtest',
8
+ description: `[Backtest / Strategy Test / SMA Cross / RSI / MACD] 汎用バックテスト(backtest / strategy test / simulation / performance)。データ取得〜計算〜チャート描画を一括実行。
9
+
10
+ 戦略: sma_cross / rsi / macd_cross / bb_breakout。期間: 1M/3M/6M。時間軸: 1D/4H/1H。
11
+ SVG チャート付きで損益・勝率・最大DD・Sharpe Ratio 等を返却。独自実装不要。`,
12
+ inputSchema: RunBacktestInputSchema,
13
+ handler: async (args: Record<string, unknown>) => {
14
+ const parsed = RunBacktestInputSchema.parse(args);
15
+ const res = await runBacktest({
16
+ pair: parsed.pair,
17
+ timeframe: parsed.timeframe,
18
+ period: parsed.period,
19
+ strategy: parsed.strategy,
20
+ fee_bp: parsed.fee_bp,
21
+ execution: parsed.execution,
22
+ outputDir: parsed.outputDir,
23
+ savePng: parsed.savePng,
24
+ includeSvg: parsed.includeSvg,
25
+ chartDetail: parsed.chartDetail,
26
+ });
27
+
28
+ if (!res.ok) {
29
+ const errorText = res.availableStrategies
30
+ ? `Error: ${res.error}\nAvailable strategies: ${res.availableStrategies.join(', ')}`
31
+ : `Error: ${res.error}`;
32
+ return { content: [{ type: 'text', text: errorText }], structuredContent: toStructured(res) };
33
+ }
34
+
35
+ // SVG がある場合はアーティファクト用のヒントを追加
36
+ let svgHint = '';
37
+ if (res.svg) {
38
+ svgHint = [
39
+ '',
40
+ '--- Backtest Chart (SVG) ---',
41
+ `identifier: backtest-${parsed.strategy?.type}-${parsed.pair}-${Date.now()}`,
42
+ `title: ${parsed.pair?.toUpperCase() || 'BTC_JPY'} ${res.data.input.strategy.type} Backtest`,
43
+ 'type: image/svg+xml',
44
+ '',
45
+ res.svg,
46
+ ].join('\n');
47
+ }
48
+
49
+ return {
50
+ content: [{ type: 'text', text: res.summary + svgHint }],
51
+ structuredContent: {
52
+ ok: true,
53
+ summary: res.summary,
54
+ svg: res.svg,
55
+ data: {
56
+ input: res.data.input,
57
+ summary: res.data.summary,
58
+ trade_count: res.data.trades.length,
59
+ },
60
+ artifactHint: res.svg
61
+ ? {
62
+ renderHint: 'ARTIFACT_REQUIRED',
63
+ displayType: 'image/svg+xml',
64
+ source: 'inline_svg',
65
+ }
66
+ : undefined,
67
+ },
68
+ };
69
+ },
70
+ };