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,274 @@
|
|
|
1
|
+
// tools/render_depth_svg.ts
|
|
2
|
+
import fs from 'node:fs/promises';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import { z } from 'zod';
|
|
5
|
+
import { nowIso, toDisplayTime } from '../lib/datetime.js';
|
|
6
|
+
import { buildCumulativeSteps } from '../lib/depth-analysis.js';
|
|
7
|
+
import { formatPair } from '../lib/formatter.js';
|
|
8
|
+
import getDepth from '../lib/get-depth.js';
|
|
9
|
+
import { fail, failFromError, failFromValidation, ok } from '../lib/result.js';
|
|
10
|
+
import { ensurePair } from '../lib/validate.js';
|
|
11
|
+
import type { FailResult, OkResult, Pair } from '../src/schemas.js';
|
|
12
|
+
import type { ToolDefinition } from '../src/tool-definition.js';
|
|
13
|
+
|
|
14
|
+
type RenderData = { svg?: string; filePath?: string; summary?: Record<string, unknown> };
|
|
15
|
+
type RenderMeta = {
|
|
16
|
+
pair: Pair;
|
|
17
|
+
type: string;
|
|
18
|
+
bbMode: 'default';
|
|
19
|
+
sizeBytes?: number;
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
export default async function renderDepthSvg(
|
|
23
|
+
args: {
|
|
24
|
+
pair?: Pair;
|
|
25
|
+
type?: string; // schema都合上の表示用。depth自体は時間軸に依存しない
|
|
26
|
+
depth?: { levels?: number };
|
|
27
|
+
preferFile?: boolean;
|
|
28
|
+
autoSave?: boolean;
|
|
29
|
+
} = {},
|
|
30
|
+
): Promise<OkResult<RenderData, RenderMeta> | FailResult> {
|
|
31
|
+
try {
|
|
32
|
+
const chk = ensurePair(args.pair || 'btc_jpy');
|
|
33
|
+
if (!chk.ok) return failFromValidation(chk);
|
|
34
|
+
const pair = chk.pair;
|
|
35
|
+
const type = String(args.type || '1day');
|
|
36
|
+
const levels = Math.max(10, Number(args?.depth?.levels ?? 200));
|
|
37
|
+
|
|
38
|
+
const depth = await getDepth(pair, { maxLevels: levels });
|
|
39
|
+
if (!depth.ok) return fail(depth.summary.replace(/^Error: /, ''), depth.meta?.errorType || 'internal');
|
|
40
|
+
const asks: Array<[string, string]> = depth.data.asks || [];
|
|
41
|
+
const bids: Array<[string, string]> = depth.data.bids || [];
|
|
42
|
+
|
|
43
|
+
// 両側の板が揃っていなければ深度チャートとして描画不可
|
|
44
|
+
if (!asks.length || !bids.length) {
|
|
45
|
+
return fail('板データが不足しています(asks/bids の両方が必要です)', 'upstream');
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// 価格レンジ
|
|
49
|
+
const minBid = Number(bids[bids.length - 1]?.[0] ?? bids[0]?.[0] ?? 0);
|
|
50
|
+
const maxAsk = Number(asks[asks.length - 1]?.[0] ?? asks[0]?.[0] ?? 0);
|
|
51
|
+
const xMinP = Math.min(minBid, Number(bids[0]?.[0] ?? minBid));
|
|
52
|
+
const xMaxP = Math.max(maxAsk, Number(asks[0]?.[0] ?? maxAsk));
|
|
53
|
+
|
|
54
|
+
// 累積量(bids: 降順、asks: 昇順)
|
|
55
|
+
const bidsSorted = [...bids]
|
|
56
|
+
.map(([p, s]) => [Number(p), Number(s)] as [number, number])
|
|
57
|
+
.sort((a, b) => b[0] - a[0]);
|
|
58
|
+
const asksSorted = [...asks]
|
|
59
|
+
.map(([p, s]) => [Number(p), Number(s)] as [number, number])
|
|
60
|
+
.sort((a, b) => a[0] - b[0]);
|
|
61
|
+
const bidSteps = buildCumulativeSteps(bidsSorted, 'bid');
|
|
62
|
+
const askSteps = buildCumulativeSteps(asksSorted, 'ask');
|
|
63
|
+
const maxQty = Math.max(bidSteps.at(-1)?.[1] || 0, askSteps.at(-1)?.[1] || 0) || 1;
|
|
64
|
+
|
|
65
|
+
// キャンバス
|
|
66
|
+
const w = 860,
|
|
67
|
+
h = 420;
|
|
68
|
+
// チャート(プロット領域)自体を押し下げ、最上段のY軸目盛りがヘッダー群の下に来るようにする
|
|
69
|
+
const padding = { top: 84, right: 12, bottom: 32, left: 64 };
|
|
70
|
+
const plotW = w - padding.left - padding.right;
|
|
71
|
+
const plotH = h - padding.top - padding.bottom;
|
|
72
|
+
const x = (price: number) =>
|
|
73
|
+
Number((padding.left + ((price - xMinP) * plotW) / Math.max(1, xMaxP - xMinP)).toFixed(1));
|
|
74
|
+
const y = (qty: number) => Number((h - padding.bottom - (qty * plotH) / maxQty).toFixed(1));
|
|
75
|
+
const fmtJPYCompact = (p: number) => `¥${Math.round(p).toLocaleString('ja-JP')}`;
|
|
76
|
+
const fmtJPYAxis = (p: number) => {
|
|
77
|
+
const v = Math.round(p / 1000) * 1000;
|
|
78
|
+
return `¥${v.toLocaleString('ja-JP')}`;
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
// ステップパス生成
|
|
82
|
+
const toStepPath = (steps: Array<[number, number]>) => {
|
|
83
|
+
if (!steps.length) return '';
|
|
84
|
+
const pts = steps.map(([p, q]) => `${x(p)},${y(q)}`);
|
|
85
|
+
return `M ${pts.join(' L ')}`;
|
|
86
|
+
};
|
|
87
|
+
const bidPath = toStepPath(bidSteps);
|
|
88
|
+
const askPath = toStepPath(askSteps);
|
|
89
|
+
|
|
90
|
+
// 塗りつぶし
|
|
91
|
+
const toFillPath = (steps: Array<[number, number]>, side: 'bid' | 'ask') => {
|
|
92
|
+
if (!steps.length) return '';
|
|
93
|
+
const head = steps[0];
|
|
94
|
+
const tail = steps[steps.length - 1];
|
|
95
|
+
const baseY = y(0);
|
|
96
|
+
const poly = ['M', `${x(head[0])},${baseY}`, 'L']
|
|
97
|
+
.concat(steps.map(([p, q]) => `${x(p)},${y(q)}`))
|
|
98
|
+
.concat(['L', `${x(tail[0])},${baseY}`, 'Z'])
|
|
99
|
+
.join(' ');
|
|
100
|
+
const fill = side === 'bid' ? 'rgba(16,185,129,0.12)' : 'rgba(249,115,22,0.12)';
|
|
101
|
+
return `<path d="${poly}" fill="${fill}" stroke="none"/>`;
|
|
102
|
+
};
|
|
103
|
+
const bidFill = toFillPath(bidSteps, 'bid');
|
|
104
|
+
const askFill = toFillPath(askSteps, 'ask');
|
|
105
|
+
|
|
106
|
+
const bestBid = Number(bidsSorted[0]?.[0] ?? 0);
|
|
107
|
+
const bestAsk = Number(asksSorted[0]?.[0] ?? 0);
|
|
108
|
+
const mid =
|
|
109
|
+
bestBid && bestAsk ? (bestBid + bestAsk) / 2 : (Number(bids[0]?.[0] ?? 0) + Number(asks[0]?.[0] ?? 0)) / 2;
|
|
110
|
+
// 軸と目盛り
|
|
111
|
+
const yAxis = `<line x1="${padding.left}" y1="${padding.top}" x2="${padding.left}" y2="${h - padding.bottom}" stroke="#4b5563" stroke-width="1"/>`;
|
|
112
|
+
const xAxis = `<line x1="${padding.left}" y1="${h - padding.bottom}" x2="${w - padding.right}" y2="${h - padding.bottom}" stroke="#4b5563" stroke-width="1"/>`;
|
|
113
|
+
// Y軸目盛り(キリの良い数値)
|
|
114
|
+
const yStep = maxQty <= 25 ? 5 : maxQty <= 50 ? 10 : 20;
|
|
115
|
+
const yMaxNice = Math.ceil(maxQty / yStep) * yStep;
|
|
116
|
+
const yTicks = [];
|
|
117
|
+
for (let v = 0; v <= yMaxNice; v += yStep) yTicks.push({ q: v, y: y(v) });
|
|
118
|
+
const yTickTexts = yTicks
|
|
119
|
+
.map(
|
|
120
|
+
(t) =>
|
|
121
|
+
`<text x="${padding.left - 8}" y="${t.y}" text-anchor="end" dominant-baseline="middle" fill="#e5e7eb" font-size="10">${Math.round(t.q)} BTC</text>`,
|
|
122
|
+
)
|
|
123
|
+
.join('');
|
|
124
|
+
// X軸目盛り(5分割)
|
|
125
|
+
const xTicks = (() => {
|
|
126
|
+
const out: Array<{ p: number; x: number }> = [];
|
|
127
|
+
const N = 4;
|
|
128
|
+
for (let i = 0; i <= N; i++) {
|
|
129
|
+
const p = xMinP + ((xMaxP - xMinP) * i) / N;
|
|
130
|
+
out.push({ p, x: x(p) });
|
|
131
|
+
}
|
|
132
|
+
return out;
|
|
133
|
+
})();
|
|
134
|
+
const xTickTexts = xTicks
|
|
135
|
+
.map(
|
|
136
|
+
(t) =>
|
|
137
|
+
`<text x="${t.x}" y="${h - padding.bottom + 14}" text-anchor="middle" fill="#e5e7eb" font-size="10">${fmtJPYAxis(t.p)}</text>`,
|
|
138
|
+
)
|
|
139
|
+
.join('');
|
|
140
|
+
const legendDepth = `
|
|
141
|
+
<g font-size="12" fill="#e5e7eb" transform="translate(${padding.left + 190}, ${Math.max(14, padding.top - 34)})">
|
|
142
|
+
<rect x="0" y="-10" width="12" height="12" fill="#10b981"></rect>
|
|
143
|
+
<text x="16" y="0">買い (Bids)</text>
|
|
144
|
+
<rect x="120" y="-10" width="12" height="12" fill="#f97316"></rect>
|
|
145
|
+
<text x="136" y="0">売り (Asks)</text>
|
|
146
|
+
</g>`;
|
|
147
|
+
|
|
148
|
+
// 注釈(タイトル、タイムスタンプ、比率など)
|
|
149
|
+
const nowJst = toDisplayTime(undefined) ?? nowIso();
|
|
150
|
+
// ±1%集計
|
|
151
|
+
const band = 0.01;
|
|
152
|
+
const bidBand = bidsSorted.filter(([p]) => p >= mid * (1 - band));
|
|
153
|
+
const askBand = asksSorted.filter(([p]) => p <= mid * (1 + band));
|
|
154
|
+
const bidDepth = bidBand.reduce((s, [, q]) => s + Number(q || 0), 0);
|
|
155
|
+
const askDepth = askBand.reduce((s, [, q]) => s + Number(q || 0), 0);
|
|
156
|
+
const ratio = askDepth > 0 ? bidDepth / askDepth : Infinity;
|
|
157
|
+
const headerTexts = `
|
|
158
|
+
<g font-size="12" fill="#e5e7eb">
|
|
159
|
+
<text x="${padding.left}" y="${padding.top - 36}">${formatPair(pair)} 板の深さ</text>
|
|
160
|
+
<text x="${padding.left}" y="${padding.top - 22}">${nowJst} JST</text>
|
|
161
|
+
<text x="${w - padding.right}" y="${padding.top - 8}" text-anchor="end">買い/売り 比率(±1%): ${Number.isFinite(ratio) ? ratio.toFixed(2) : '∞'}</text>
|
|
162
|
+
</g>`;
|
|
163
|
+
// 中央線ラベル(価格を見やすく)
|
|
164
|
+
const midLabel = `<text x="${x(mid)}" y="${padding.top + 12}" text-anchor="middle" fill="#e5e7eb" font-size="12">${fmtJPYCompact(mid)}</text>`;
|
|
165
|
+
|
|
166
|
+
const svg = `
|
|
167
|
+
<svg width="${w}" height="${h}" viewBox="0 0 ${w} ${h}" preserveAspectRatio="xMidYMid meet" xmlns="http://www.w3.org/2000/svg" style="background-color:#1f2937;color:#e5e7eb;font-family:sans-serif;max-width:100%;height:auto;">
|
|
168
|
+
${legendDepth}
|
|
169
|
+
${headerTexts}
|
|
170
|
+
<g class="axes">
|
|
171
|
+
${yAxis}${xAxis}
|
|
172
|
+
${yTickTexts}
|
|
173
|
+
${xTickTexts}
|
|
174
|
+
</g>
|
|
175
|
+
<g class="plot-area">
|
|
176
|
+
${bidFill}
|
|
177
|
+
${askFill}
|
|
178
|
+
<path d="${bidPath}" fill="none" stroke="#10b981" stroke-width="2"/>
|
|
179
|
+
<path d="${askPath}" fill="none" stroke="#f97316" stroke-width="2"/>
|
|
180
|
+
<line x1="${x(mid)}" y1="${padding.top}" x2="${x(mid)}" y2="${h - padding.bottom}" stroke="#9ca3af" stroke-width="1" stroke-dasharray="4 4"/>
|
|
181
|
+
${midLabel}
|
|
182
|
+
</g>
|
|
183
|
+
</svg>`;
|
|
184
|
+
|
|
185
|
+
const finalSvg = svg.replace(/\s{2,}/g, ' ').replace(/>\s+</g, '><');
|
|
186
|
+
const sizeBytes = Buffer.byteLength(finalSvg, 'utf8');
|
|
187
|
+
const preferFile = Boolean(args.preferFile);
|
|
188
|
+
const autoSave = Boolean(args.autoSave);
|
|
189
|
+
|
|
190
|
+
const meta: RenderMeta = { pair, type, bbMode: 'default', sizeBytes };
|
|
191
|
+
|
|
192
|
+
const assetsDir = path.join(process.cwd(), 'assets');
|
|
193
|
+
const filename = `depth-${pair}-${Date.now()}.svg`;
|
|
194
|
+
const outputPath = path.join(assetsDir, filename);
|
|
195
|
+
|
|
196
|
+
const summary = {
|
|
197
|
+
pair,
|
|
198
|
+
currentPrice: Math.round(mid),
|
|
199
|
+
bestBid: Math.round(bestBid),
|
|
200
|
+
bestAsk: Math.round(bestAsk),
|
|
201
|
+
bandPct: band,
|
|
202
|
+
bidDepth: Number(bidDepth.toFixed(4)),
|
|
203
|
+
askDepth: Number(askDepth.toFixed(4)),
|
|
204
|
+
ratio: Number.isFinite(ratio) ? Number(ratio.toFixed(2)) : null,
|
|
205
|
+
timestamp: nowIso(),
|
|
206
|
+
};
|
|
207
|
+
|
|
208
|
+
if (preferFile || autoSave) {
|
|
209
|
+
await fs.mkdir(assetsDir, { recursive: true });
|
|
210
|
+
await fs.writeFile(outputPath, finalSvg);
|
|
211
|
+
return ok<RenderData, RenderMeta>(
|
|
212
|
+
`${formatPair(pair)} depth chart saved to ${outputPath}`,
|
|
213
|
+
{ filePath: outputPath, svg: undefined, summary },
|
|
214
|
+
meta,
|
|
215
|
+
);
|
|
216
|
+
}
|
|
217
|
+
// inline
|
|
218
|
+
return ok<RenderData, RenderMeta>(
|
|
219
|
+
`${formatPair(pair)} depth chart rendered`,
|
|
220
|
+
{ svg: finalSvg, filePath: undefined, summary },
|
|
221
|
+
meta,
|
|
222
|
+
);
|
|
223
|
+
} catch (e: unknown) {
|
|
224
|
+
return failFromError(e, { defaultMessage: '板の深度チャート描画に失敗しました' });
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
// ── MCP ツール定義(tool-registry から自動収集) ──
|
|
229
|
+
export const toolDef: ToolDefinition = {
|
|
230
|
+
name: 'render_depth_svg',
|
|
231
|
+
description: `[Depth Chart / Order Book Visualization] 板の深さチャートを SVG 生成(depth chart / order book visualization / bid-ask depth)。
|
|
232
|
+
クライアント側(Claude.ai の Visualizer 等)で描画可能な場合は prepare_depth_data を優先し、本ツールは SVG/PNG ファイル保存(preferFile / autoSave)やファイル埋め込み用途にフォールバックする位置づけ。
|
|
233
|
+
data.svg を HTML に埋め込んで表示。`,
|
|
234
|
+
inputSchema: z.object({
|
|
235
|
+
pair: z.string().default('btc_jpy'),
|
|
236
|
+
type: z.string().default('1day'),
|
|
237
|
+
depth: z
|
|
238
|
+
.object({ levels: z.number().int().min(10).max(1000).optional().default(200) })
|
|
239
|
+
.optional()
|
|
240
|
+
.default({ levels: 200 }),
|
|
241
|
+
preferFile: z.boolean().optional(),
|
|
242
|
+
autoSave: z.boolean().optional(),
|
|
243
|
+
}),
|
|
244
|
+
handler: async ({ pair, type, depth, preferFile, autoSave }: Record<string, unknown>) => {
|
|
245
|
+
const res = await renderDepthSvg({
|
|
246
|
+
pair: pair as Pair | undefined,
|
|
247
|
+
type: type as string | undefined,
|
|
248
|
+
depth: depth as { levels?: number } | undefined,
|
|
249
|
+
preferFile: preferFile as boolean | undefined,
|
|
250
|
+
autoSave: autoSave as boolean | undefined,
|
|
251
|
+
});
|
|
252
|
+
if (!res?.ok) return res;
|
|
253
|
+
const data = res.data || {};
|
|
254
|
+
const header = `${String(pair).toUpperCase()} Depth chart`;
|
|
255
|
+
if (data.filePath) {
|
|
256
|
+
const text = `${header}\nSaved: computer://${data.filePath}`;
|
|
257
|
+
return { content: [{ type: 'text', text }], structuredContent: { ...res } as Record<string, unknown> };
|
|
258
|
+
}
|
|
259
|
+
if (data.svg) {
|
|
260
|
+
const text = [
|
|
261
|
+
header,
|
|
262
|
+
'',
|
|
263
|
+
'--- Depth SVG ---',
|
|
264
|
+
`identifier: depth-${String(pair)}-${Date.now()}`,
|
|
265
|
+
`title: Depth ${String(pair).toUpperCase()}`,
|
|
266
|
+
'type: image/svg+xml',
|
|
267
|
+
'',
|
|
268
|
+
String(data.svg),
|
|
269
|
+
].join('\n');
|
|
270
|
+
return { content: [{ type: 'text', text }], structuredContent: { ...res } as Record<string, unknown> };
|
|
271
|
+
}
|
|
272
|
+
return { content: [{ type: 'text', text: header }], structuredContent: { ...res } as Record<string, unknown> };
|
|
273
|
+
},
|
|
274
|
+
};
|
|
@@ -0,0 +1,252 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* lib/backtest_engine.ts - 汎用バックテストエンジン
|
|
3
|
+
*
|
|
4
|
+
* シグナルからトレードを生成し、エクイティ・ドローダウンを計算
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import type { Candle, DrawdownPoint, EquityPoint, Trade } from '../types.js';
|
|
8
|
+
import { calculateEquityAndDrawdown } from './equity.js';
|
|
9
|
+
import type { Overlay, Signal, Strategy, StrategyConfig } from './strategies/types.js';
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* バックテスト入力
|
|
13
|
+
*/
|
|
14
|
+
export interface BacktestEngineInput {
|
|
15
|
+
pair: string;
|
|
16
|
+
timeframe: string;
|
|
17
|
+
period: string;
|
|
18
|
+
strategy: StrategyConfig;
|
|
19
|
+
fee_bp: number;
|
|
20
|
+
execution: 't+1_open';
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* バックテストサマリー
|
|
25
|
+
*/
|
|
26
|
+
export interface BacktestEngineSummary {
|
|
27
|
+
/** 複利計算による総損益[%] */
|
|
28
|
+
total_pnl_pct: number;
|
|
29
|
+
trade_count: number;
|
|
30
|
+
win_rate: number;
|
|
31
|
+
/** 0以上。最大ドローダウン[%] */
|
|
32
|
+
max_drawdown_pct: number;
|
|
33
|
+
/** Buy&Hold との比較 */
|
|
34
|
+
buy_hold_pnl_pct: number;
|
|
35
|
+
/** 超過リターン(戦略 - Buy&Hold) */
|
|
36
|
+
excess_return_pct: number;
|
|
37
|
+
/** Profit Factor: 総利益 / 総損失(損失がない場合 null) */
|
|
38
|
+
profit_factor: number | null;
|
|
39
|
+
/** 年率換算 Sharpe Ratio(日次リターンベース) */
|
|
40
|
+
sharpe_ratio: number | null;
|
|
41
|
+
/** 1トレードあたり平均損益[%] */
|
|
42
|
+
avg_pnl_pct: number;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* バックテスト結果
|
|
47
|
+
*/
|
|
48
|
+
export interface BacktestEngineResult {
|
|
49
|
+
input: BacktestEngineInput;
|
|
50
|
+
summary: BacktestEngineSummary;
|
|
51
|
+
trades: Trade[];
|
|
52
|
+
equity_curve: EquityPoint[];
|
|
53
|
+
drawdown_curve: DrawdownPoint[];
|
|
54
|
+
overlays: Overlay[];
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* シグナル配列からトレードを実行
|
|
59
|
+
*
|
|
60
|
+
* @param candles ローソク足データ
|
|
61
|
+
* @param signals シグナル配列
|
|
62
|
+
* @param fee_bp 片道手数料(basis points)
|
|
63
|
+
* @returns トレード配列
|
|
64
|
+
*/
|
|
65
|
+
export function executeTradesFromSignals(candles: Candle[], signals: Signal[], fee_bp: number): Trade[] {
|
|
66
|
+
const trades: Trade[] = [];
|
|
67
|
+
let position: 'none' | 'long' = 'none';
|
|
68
|
+
let entryTime = '';
|
|
69
|
+
let entryPrice = 0;
|
|
70
|
+
|
|
71
|
+
for (let i = 0; i < signals.length - 1; i++) {
|
|
72
|
+
const signal = signals[i];
|
|
73
|
+
const nextCandle = candles[i + 1];
|
|
74
|
+
|
|
75
|
+
if (!nextCandle) continue;
|
|
76
|
+
|
|
77
|
+
// t+1 open で執行
|
|
78
|
+
const execPrice = nextCandle.open;
|
|
79
|
+
const execTime = nextCandle.time;
|
|
80
|
+
|
|
81
|
+
// エントリー
|
|
82
|
+
if (position === 'none' && signal.action === 'buy') {
|
|
83
|
+
position = 'long';
|
|
84
|
+
entryTime = execTime;
|
|
85
|
+
entryPrice = execPrice;
|
|
86
|
+
}
|
|
87
|
+
// エグジット
|
|
88
|
+
else if (position === 'long' && signal.action === 'sell') {
|
|
89
|
+
// 往復手数料率(乗数)
|
|
90
|
+
const feeMultiplier = 1 - (fee_bp / 10000) * 2;
|
|
91
|
+
|
|
92
|
+
// グロスリターン乗数
|
|
93
|
+
const grossReturn = execPrice / entryPrice;
|
|
94
|
+
|
|
95
|
+
// ネットリターン乗数(手数料控除後)
|
|
96
|
+
const netReturn = grossReturn * feeMultiplier;
|
|
97
|
+
|
|
98
|
+
// 表示用パーセント
|
|
99
|
+
const pnlPct = (netReturn - 1) * 100;
|
|
100
|
+
const feePct = (1 - feeMultiplier) * 100;
|
|
101
|
+
|
|
102
|
+
trades.push({
|
|
103
|
+
entry_time: entryTime,
|
|
104
|
+
entry_price: entryPrice,
|
|
105
|
+
exit_time: execTime,
|
|
106
|
+
exit_price: execPrice,
|
|
107
|
+
pnl_pct: Number(pnlPct.toFixed(4)),
|
|
108
|
+
fee_pct: Number(feePct.toFixed(4)),
|
|
109
|
+
net_return: Number(netReturn.toFixed(6)),
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
position = 'none';
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
return trades;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Profit Factor を計算
|
|
121
|
+
* 総利益 / 総損失(abs)。損失がゼロの場合 null。
|
|
122
|
+
*/
|
|
123
|
+
function calcProfitFactor(trades: Trade[]): number | null {
|
|
124
|
+
let grossProfit = 0;
|
|
125
|
+
let grossLoss = 0;
|
|
126
|
+
for (const t of trades) {
|
|
127
|
+
if (t.pnl_pct > 0) grossProfit += t.pnl_pct;
|
|
128
|
+
else if (t.pnl_pct < 0) grossLoss += Math.abs(t.pnl_pct);
|
|
129
|
+
}
|
|
130
|
+
if (grossLoss === 0) return grossProfit > 0 ? null : null; // 全勝 or トレードなし
|
|
131
|
+
return Number((grossProfit / grossLoss).toFixed(2));
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* 年率換算 Sharpe Ratio を日次エクイティカーブから計算
|
|
136
|
+
* Sharpe = mean(daily_return) / stdev(daily_return) * sqrt(365)
|
|
137
|
+
*/
|
|
138
|
+
function calcSharpeRatio(equityCurve: EquityPoint[]): number | null {
|
|
139
|
+
if (equityCurve.length < 2) return null;
|
|
140
|
+
|
|
141
|
+
// 日次リターン(equity_pct の差分をパーセントで)
|
|
142
|
+
const dailyReturns: number[] = [];
|
|
143
|
+
for (let i = 1; i < equityCurve.length; i++) {
|
|
144
|
+
const prevEq = 1 + equityCurve[i - 1].equity_pct / 100;
|
|
145
|
+
const currEq = 1 + equityCurve[i].equity_pct / 100;
|
|
146
|
+
if (prevEq > 0) {
|
|
147
|
+
dailyReturns.push(currEq / prevEq - 1);
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
if (dailyReturns.length < 2) return null;
|
|
151
|
+
|
|
152
|
+
const mean = dailyReturns.reduce((a, b) => a + b, 0) / dailyReturns.length;
|
|
153
|
+
const variance = dailyReturns.reduce((sum, r) => sum + (r - mean) ** 2, 0) / (dailyReturns.length - 1);
|
|
154
|
+
const stdev = Math.sqrt(variance);
|
|
155
|
+
if (stdev === 0) return null;
|
|
156
|
+
|
|
157
|
+
// 暗号資産: 年間 365 日
|
|
158
|
+
const sharpe = (mean / stdev) * Math.sqrt(365);
|
|
159
|
+
return Number(sharpe.toFixed(2));
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* サマリー統計を計算(複利)
|
|
164
|
+
*/
|
|
165
|
+
export function calculateSummary(
|
|
166
|
+
trades: Trade[],
|
|
167
|
+
maxDrawdown: number,
|
|
168
|
+
candles: Candle[],
|
|
169
|
+
equityCurve: EquityPoint[],
|
|
170
|
+
): BacktestEngineSummary {
|
|
171
|
+
// Buy&Hold の計算
|
|
172
|
+
let buyHoldPnlPct = 0;
|
|
173
|
+
if (candles.length >= 2) {
|
|
174
|
+
const firstClose = candles[0].close;
|
|
175
|
+
const lastClose = candles[candles.length - 1].close;
|
|
176
|
+
buyHoldPnlPct = ((lastClose - firstClose) / firstClose) * 100;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
if (trades.length === 0) {
|
|
180
|
+
return {
|
|
181
|
+
total_pnl_pct: 0,
|
|
182
|
+
trade_count: 0,
|
|
183
|
+
win_rate: 0,
|
|
184
|
+
max_drawdown_pct: 0,
|
|
185
|
+
buy_hold_pnl_pct: Number(buyHoldPnlPct.toFixed(2)),
|
|
186
|
+
excess_return_pct: Number((-buyHoldPnlPct).toFixed(2)),
|
|
187
|
+
profit_factor: null,
|
|
188
|
+
sharpe_ratio: calcSharpeRatio(equityCurve),
|
|
189
|
+
avg_pnl_pct: 0,
|
|
190
|
+
};
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
// 複利で総損益を計算
|
|
194
|
+
const totalReturn = trades.reduce((acc, t) => acc * t.net_return, 1.0);
|
|
195
|
+
const totalPnlPct = (totalReturn - 1) * 100;
|
|
196
|
+
|
|
197
|
+
const wins = trades.filter((t) => t.pnl_pct > 0).length;
|
|
198
|
+
const excessReturn = totalPnlPct - buyHoldPnlPct;
|
|
199
|
+
const avgPnl = trades.reduce((s, t) => s + t.pnl_pct, 0) / trades.length;
|
|
200
|
+
|
|
201
|
+
return {
|
|
202
|
+
total_pnl_pct: Number(totalPnlPct.toFixed(2)),
|
|
203
|
+
trade_count: trades.length,
|
|
204
|
+
win_rate: Number((wins / trades.length).toFixed(4)),
|
|
205
|
+
max_drawdown_pct: Number(maxDrawdown.toFixed(2)),
|
|
206
|
+
buy_hold_pnl_pct: Number(buyHoldPnlPct.toFixed(2)),
|
|
207
|
+
excess_return_pct: Number(excessReturn.toFixed(2)),
|
|
208
|
+
profit_factor: calcProfitFactor(trades),
|
|
209
|
+
sharpe_ratio: calcSharpeRatio(equityCurve),
|
|
210
|
+
avg_pnl_pct: Number(avgPnl.toFixed(2)),
|
|
211
|
+
};
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
/**
|
|
215
|
+
* バックテストを実行
|
|
216
|
+
*
|
|
217
|
+
* @param candles ローソク足データ
|
|
218
|
+
* @param strategy 戦略オブジェクト
|
|
219
|
+
* @param input バックテスト入力パラメータ
|
|
220
|
+
* @returns バックテスト結果
|
|
221
|
+
*/
|
|
222
|
+
export function runBacktestEngine(
|
|
223
|
+
candles: Candle[],
|
|
224
|
+
strategy: Strategy,
|
|
225
|
+
input: BacktestEngineInput,
|
|
226
|
+
): BacktestEngineResult {
|
|
227
|
+
const params = { ...strategy.defaultParams, ...input.strategy.params };
|
|
228
|
+
|
|
229
|
+
// 1. シグナル生成
|
|
230
|
+
const signals = strategy.generate(candles, params);
|
|
231
|
+
|
|
232
|
+
// 2. トレード実行
|
|
233
|
+
const trades = executeTradesFromSignals(candles, signals, input.fee_bp);
|
|
234
|
+
|
|
235
|
+
// 3. エクイティ・ドローダウン計算
|
|
236
|
+
const { equity_curve, drawdown_curve, max_drawdown } = calculateEquityAndDrawdown(trades, candles);
|
|
237
|
+
|
|
238
|
+
// 4. サマリー計算
|
|
239
|
+
const summary = calculateSummary(trades, max_drawdown, candles, equity_curve);
|
|
240
|
+
|
|
241
|
+
// 5. オーバーレイデータ取得
|
|
242
|
+
const overlays = strategy.getOverlays(candles, params);
|
|
243
|
+
|
|
244
|
+
return {
|
|
245
|
+
input,
|
|
246
|
+
summary,
|
|
247
|
+
trades,
|
|
248
|
+
equity_curve,
|
|
249
|
+
drawdown_curve,
|
|
250
|
+
overlays,
|
|
251
|
+
};
|
|
252
|
+
}
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* lib/equity.ts - エクイティカーブ・ドローダウン計算
|
|
3
|
+
*
|
|
4
|
+
* 【計算仕様(含み損益ベース)】
|
|
5
|
+
*
|
|
6
|
+
* ■ エクイティ
|
|
7
|
+
* - 初期値: 1.0(= 100%)
|
|
8
|
+
* - ポジション非保有時: 前回の確定エクイティを維持
|
|
9
|
+
* - ポジション保有中: entry_price から current_close までの含み損益を反映
|
|
10
|
+
* equity = confirmed_equity * (current_close / entry_price)
|
|
11
|
+
* - トレード決済時: confirmed_equity を更新
|
|
12
|
+
*
|
|
13
|
+
* ■ ドローダウン
|
|
14
|
+
* - 定義: (peak_equity - current_equity) / peak_equity * 100
|
|
15
|
+
* - 常に 0 以上の値
|
|
16
|
+
* - 新しいピークを更新すると DD = 0 に戻る
|
|
17
|
+
*
|
|
18
|
+
* ■ 表示用
|
|
19
|
+
* - equity_pct: (equity - 1) * 100 で表示
|
|
20
|
+
* - drawdown_pct: 0以上の下落幅[%]。表示時に -XX% とする
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
import type { Candle, DrawdownPoint, EquityPoint, Trade } from '../types.js';
|
|
24
|
+
|
|
25
|
+
export interface EquityResult {
|
|
26
|
+
equity_curve: EquityPoint[];
|
|
27
|
+
drawdown_curve: DrawdownPoint[];
|
|
28
|
+
/** 0以上。最大ドローダウン[%] */
|
|
29
|
+
max_drawdown: number;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
interface PositionInfo {
|
|
33
|
+
isLong: boolean;
|
|
34
|
+
entryPrice: number;
|
|
35
|
+
entryEquity: number; // エントリー時点の確定エクイティ
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* トレード結果からエクイティカーブとドローダウンを計算(含み損益ベース)
|
|
40
|
+
*
|
|
41
|
+
* @param trades トレード配列
|
|
42
|
+
* @param candles ローソク足配列
|
|
43
|
+
* @returns エクイティカーブ、ドローダウンカーブ、最大ドローダウン
|
|
44
|
+
*/
|
|
45
|
+
export function calculateEquityAndDrawdown(trades: Trade[], candles: Candle[]): EquityResult {
|
|
46
|
+
// トレードを entry_time と exit_time でマッピング
|
|
47
|
+
const tradeByEntryTime = new Map<string, Trade>();
|
|
48
|
+
const tradeByExitTime = new Map<string, Trade>();
|
|
49
|
+
for (const t of trades) {
|
|
50
|
+
tradeByEntryTime.set(t.entry_time, t);
|
|
51
|
+
tradeByExitTime.set(t.exit_time, t);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const equity_curve: EquityPoint[] = [];
|
|
55
|
+
const drawdown_curve: DrawdownPoint[] = [];
|
|
56
|
+
|
|
57
|
+
let confirmedEquity = 1.0; // 確定済みエクイティ
|
|
58
|
+
let peakEquity = 1.0;
|
|
59
|
+
let maxDrawdown = 0;
|
|
60
|
+
let position: PositionInfo | null = null;
|
|
61
|
+
|
|
62
|
+
for (const candle of candles) {
|
|
63
|
+
// エントリーチェック
|
|
64
|
+
const entryTrade = tradeByEntryTime.get(candle.time);
|
|
65
|
+
if (entryTrade && !position) {
|
|
66
|
+
position = {
|
|
67
|
+
isLong: true,
|
|
68
|
+
entryPrice: entryTrade.entry_price,
|
|
69
|
+
entryEquity: confirmedEquity,
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// 現在のエクイティを計算
|
|
74
|
+
let currentEquity: number;
|
|
75
|
+
if (position) {
|
|
76
|
+
// ポジション保有中: 含み損益を反映
|
|
77
|
+
const unrealizedReturn = candle.close / position.entryPrice;
|
|
78
|
+
currentEquity = position.entryEquity * unrealizedReturn;
|
|
79
|
+
} else {
|
|
80
|
+
// ポジション非保有: 確定エクイティを維持
|
|
81
|
+
currentEquity = confirmedEquity;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// エグジットチェック(エクイティ計算後に処理)
|
|
85
|
+
const exitTrade = tradeByExitTime.get(candle.time);
|
|
86
|
+
if (exitTrade && position) {
|
|
87
|
+
// 確定エクイティを更新(手数料込み)
|
|
88
|
+
confirmedEquity = position.entryEquity * exitTrade.net_return;
|
|
89
|
+
currentEquity = confirmedEquity; // 決済時は確定値を使用
|
|
90
|
+
position = null;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// 表示用エクイティ [%] = (equity - 1) * 100
|
|
94
|
+
const equityPct = (currentEquity - 1) * 100;
|
|
95
|
+
const confirmedPct = (confirmedEquity - 1) * 100;
|
|
96
|
+
equity_curve.push({
|
|
97
|
+
time: candle.time,
|
|
98
|
+
equity_pct: Number(equityPct.toFixed(4)),
|
|
99
|
+
confirmed_pct: Number(confirmedPct.toFixed(4)),
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
// ピーク更新
|
|
103
|
+
if (currentEquity > peakEquity) {
|
|
104
|
+
peakEquity = currentEquity;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// ドローダウン計算
|
|
108
|
+
let drawdownPct = 0;
|
|
109
|
+
if (peakEquity > 0) {
|
|
110
|
+
drawdownPct = ((peakEquity - currentEquity) / peakEquity) * 100;
|
|
111
|
+
}
|
|
112
|
+
if (drawdownPct < 0) {
|
|
113
|
+
drawdownPct = 0;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
drawdown_curve.push({
|
|
117
|
+
time: candle.time,
|
|
118
|
+
drawdown_pct: Number(drawdownPct.toFixed(4)),
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
if (drawdownPct > maxDrawdown) {
|
|
122
|
+
maxDrawdown = drawdownPct;
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
return {
|
|
127
|
+
equity_curve,
|
|
128
|
+
drawdown_curve,
|
|
129
|
+
max_drawdown: Number(maxDrawdown.toFixed(4)),
|
|
130
|
+
};
|
|
131
|
+
}
|