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,139 @@
1
+ /**
2
+ * chart/render-depth — Depth チャート(板の深度)の SVG 描画。
3
+ *
4
+ * メインのローソク足チャートとは完全に独立したコードパス。
5
+ */
6
+
7
+ import fs from 'node:fs/promises';
8
+ import path from 'node:path';
9
+ import { formatPair } from '../../lib/formatter.js';
10
+ import getDepth from '../../lib/get-depth.js';
11
+ import { fail, ok } from '../../lib/result.js';
12
+ import type { Pair, Result } from '../../src/schemas.js';
13
+
14
+ type RenderData = { svg?: string; filePath?: string; legend?: Record<string, string> };
15
+ type RenderMeta = {
16
+ pair: Pair;
17
+ type: string;
18
+ bbMode: 'default' | 'extended';
19
+ [key: string]: unknown;
20
+ };
21
+
22
+ export async function renderDepthChart(
23
+ pair: string,
24
+ args: { depth?: { levels?: number }; type?: string },
25
+ effectivePrecision: number,
26
+ ): Promise<Result<RenderData, RenderMeta>> {
27
+ try {
28
+ const depth = await getDepth(pair, { maxLevels: args.depth?.levels ?? 200 });
29
+ if (!depth.ok) return fail(depth.summary.replace(/^Error: /, ''), depth.meta?.errorType || 'internal');
30
+ const asks: Array<[string, string]> = depth.data.asks || [];
31
+ const bids: Array<[string, string]> = depth.data.bids || [];
32
+ // 価格レンジ
33
+ const minBid = Number(bids[bids.length - 1]?.[0] ?? bids[0]?.[0] ?? 0);
34
+ const maxAsk = Number(asks[asks.length - 1]?.[0] ?? asks[0]?.[0] ?? 0);
35
+ const xMinP = Math.min(minBid, Number(bids[0]?.[0] ?? minBid));
36
+ const xMaxP = Math.max(maxAsk, Number(asks[0]?.[0] ?? maxAsk));
37
+ // 累積量(左:bids 降順→小へ、右:asks 昇順→大へ)
38
+ const bidsSorted = [...bids]
39
+ .map(([p, s]) => [Number(p), Number(s)] as [number, number])
40
+ .sort((a, b) => b[0] - a[0]);
41
+ const asksSorted = [...asks]
42
+ .map(([p, s]) => [Number(p), Number(s)] as [number, number])
43
+ .sort((a, b) => a[0] - b[0]);
44
+ let cum = 0;
45
+ const bidSteps: Array<[number, number]> = [];
46
+ for (const [p, s] of bidsSorted) {
47
+ cum += s;
48
+ bidSteps.push([p, cum]);
49
+ }
50
+ cum = 0;
51
+ const askSteps: Array<[number, number]> = [];
52
+ for (const [p, s] of asksSorted) {
53
+ cum += s;
54
+ askSteps.push([p, cum]);
55
+ }
56
+ const maxQty = Math.max(bidSteps.at(-1)?.[1] || 0, askSteps.at(-1)?.[1] || 0) || 1;
57
+
58
+ // キャンバス
59
+ const w = 860,
60
+ h = 420;
61
+ const padding = { top: 36, right: 12, bottom: 32, left: 64 };
62
+ const plotW = w - padding.left - padding.right;
63
+ const plotH = h - padding.top - padding.bottom;
64
+ const x = (price: number) =>
65
+ Number((padding.left + ((price - xMinP) * plotW) / Math.max(1, xMaxP - xMinP)).toFixed(effectivePrecision));
66
+ const y = (qty: number) => Number((h - padding.bottom - (qty * plotH) / maxQty).toFixed(effectivePrecision));
67
+
68
+ // ステップパス生成
69
+ const toStepPath = (steps: Array<[number, number]>) => {
70
+ if (!steps.length) return '';
71
+ const pts = steps.map(([p, q]) => `${x(p)},${y(q)}`);
72
+ return `M ${pts.join(' L ')}`;
73
+ };
74
+ const bidPath = toStepPath(bidSteps);
75
+ const askPath = toStepPath(askSteps);
76
+
77
+ // 塗りつぶし(ステップ下を半透明で)
78
+ const toFillPath = (steps: Array<[number, number]>, side: 'bid' | 'ask') => {
79
+ if (!steps.length) return '';
80
+ const head = steps[0];
81
+ const tail = steps[steps.length - 1];
82
+ const baseY = y(0);
83
+ const poly = ['M', `${x(head[0])},${baseY}`, 'L']
84
+ .concat(steps.map(([p, q]) => `${x(p)},${y(q)}`))
85
+ .concat(['L', `${x(tail[0])},${baseY}`, 'Z'])
86
+ .join(' ');
87
+ const fill = side === 'bid' ? 'rgba(16,185,129,0.12)' : 'rgba(249,115,22,0.12)';
88
+ return `<path d="${poly}" fill="${fill}" stroke="none"/>`;
89
+ };
90
+ const bidFill = toFillPath(bidSteps, 'bid');
91
+ const askFill = toFillPath(askSteps, 'ask');
92
+
93
+ const mid = (Number(bids[0]?.[0] ?? 0) + Number(asks[0]?.[0] ?? 0)) / 2;
94
+ const yAxis = `
95
+ <line x1="${padding.left}" y1="${padding.top}" x2="${padding.left}" y2="${h - padding.bottom}" stroke="#4b5563" stroke-width="1"/>
96
+ `;
97
+ const xAxis = `
98
+ <line x1="${padding.left}" y1="${h - padding.bottom}" x2="${w - padding.right}" y2="${h - padding.bottom}" stroke="#4b5563" stroke-width="1"/>
99
+ `;
100
+ const legendDepth = `
101
+ <g font-size="12" fill="#e5e7eb" transform="translate(${padding.left}, ${Math.max(14, padding.top - 18)})">
102
+ <rect x="0" y="-10" width="12" height="12" fill="#10b981"></rect>
103
+ <text x="16" y="0">買い (Bids)</text>
104
+ <rect x="120" y="-10" width="12" height="12" fill="#f97316"></rect>
105
+ <text x="136" y="0">売り (Asks)</text>
106
+ </g>`;
107
+
108
+ const svg = `
109
+ <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;">
110
+ <title>${formatPair(pair)} depth chart</title>
111
+ ${legendDepth}
112
+ <g class="axes">${yAxis}${xAxis}</g>
113
+ <g class="plot-area">
114
+ ${bidFill}
115
+ ${askFill}
116
+ <path d="${bidPath}" fill="none" stroke="#10b981" stroke-width="2"/>
117
+ <path d="${askPath}" fill="none" stroke="#f97316" stroke-width="2"/>
118
+ <line x1="${x(mid)}" y1="${padding.top}" x2="${x(mid)}" y2="${h - padding.bottom}" stroke="#9ca3af" stroke-width="1" stroke-dasharray="4 4"/>
119
+ </g>
120
+ </svg>`;
121
+ const assetsDir = path.join(process.cwd(), 'assets');
122
+ await fs.mkdir(assetsDir, { recursive: true });
123
+ const outputPath = path.join(assetsDir, `depth-${pair}-${Date.now()}.svg`);
124
+ await fs.writeFile(outputPath, svg);
125
+ const metaOut: RenderMeta = {
126
+ pair: pair as Pair,
127
+ type: String(args.type || '1day'),
128
+ bbMode: 'default',
129
+ };
130
+ return ok<RenderData, RenderMeta>(
131
+ `${formatPair(pair)} depth chart saved to ${outputPath}`,
132
+ { filePath: outputPath, svg },
133
+ metaOut,
134
+ );
135
+ } catch (e: unknown) {
136
+ const msg = e instanceof Error ? e.message : String(e);
137
+ return fail(msg || 'failed to render depth', 'internal');
138
+ }
139
+ }
@@ -0,0 +1,208 @@
1
+ /**
2
+ * chart/render-sub-panels — MACD / RSI / Volume のサブパネル SVG 描画。
3
+ */
4
+
5
+ import { RSI_OVERBOUGHT, RSI_OVERSOLD } from '../../lib/indicator-config.js';
6
+ import { niceTicks } from './svg-utils.js';
7
+
8
+ // ── Configuration ──
9
+ const RSI_MIDLINE = 50;
10
+ const PRICE_PANEL_HEIGHT = 420;
11
+
12
+ /** サブパネル描画に必要なコンテキスト */
13
+ export interface SubPanelContext {
14
+ x: (i: number) => number;
15
+ padding: { top: number; right: number; bottom: number; left: number };
16
+ plotW: number;
17
+ w: number;
18
+ barW: number;
19
+ effectivePrecision: number;
20
+ pastBuffer: number;
21
+ displayItems: Array<{ open: number; close: number; volume?: number; [k: string]: unknown }>;
22
+ indicators: Record<string, unknown> | undefined;
23
+ }
24
+
25
+ const SUB_PANEL_HEIGHT = 120;
26
+ const SUB_PANEL_GAP = 24;
27
+ const LEGEND_H = 18;
28
+
29
+ function subPanelY(v: number, min: number, max: number, top: number, precision: number): number {
30
+ const dataH = SUB_PANEL_HEIGHT - LEGEND_H;
31
+ const range = Math.max(1e-10, max - min);
32
+ return Number((top + SUB_PANEL_HEIGHT - ((v - min) * dataH) / range).toFixed(precision));
33
+ }
34
+
35
+ export function renderSubPanels(
36
+ panelTypes: Array<'macd' | 'rsi' | 'volume'>,
37
+ ctx: SubPanelContext,
38
+ ): { svg: string; totalHeight: number } {
39
+ if (panelTypes.length === 0) return { svg: '', totalHeight: 0 };
40
+
41
+ const { padding, plotW, w } = ctx;
42
+ const h = PRICE_PANEL_HEIGHT;
43
+ const pricePanelBottom = h - padding.bottom;
44
+ let currentTop = pricePanelBottom + SUB_PANEL_GAP;
45
+ let svgOut = '';
46
+
47
+ for (const panelType of panelTypes) {
48
+ const panelBottom = currentTop + SUB_PANEL_HEIGHT;
49
+ let pc = '';
50
+ pc += `<rect x="${padding.left}" y="${currentTop}" width="${plotW}" height="${SUB_PANEL_HEIGHT}" fill="rgba(255,255,255,0.02)"/>`;
51
+ pc += `<line x1="${padding.left}" y1="${currentTop}" x2="${w - padding.right}" y2="${currentTop}" stroke="#374151" stroke-width="0.5"/>`;
52
+
53
+ if (panelType === 'macd') {
54
+ pc += renderMacdPanel(currentTop, ctx);
55
+ } else if (panelType === 'rsi') {
56
+ pc += renderRsiPanel(currentTop, ctx);
57
+ } else if (panelType === 'volume') {
58
+ pc += renderVolumePanel(currentTop, ctx);
59
+ }
60
+
61
+ pc += `<line x1="${padding.left}" y1="${currentTop}" x2="${padding.left}" y2="${panelBottom}" stroke="#4b5563" stroke-width="1"/>`;
62
+ svgOut += pc;
63
+ currentTop = panelBottom + SUB_PANEL_GAP;
64
+ }
65
+
66
+ const totalHeight = panelTypes.length * SUB_PANEL_HEIGHT + panelTypes.length * SUB_PANEL_GAP;
67
+ return { svg: svgOut, totalHeight };
68
+ }
69
+
70
+ function renderMacdPanel(currentTop: number, ctx: SubPanelContext): string {
71
+ const { x, padding, w, barW, effectivePrecision, pastBuffer, displayItems, indicators } = ctx;
72
+ const py = (v: number, yMin: number, yMax: number) => subPanelY(v, yMin, yMax, currentTop, effectivePrecision);
73
+ let pc = '';
74
+
75
+ const ms = indicators?.macd_series as { line?: number[]; signal?: number[]; hist?: number[] } | undefined;
76
+ const mLine = (ms?.line || []) as Array<number | null>;
77
+ const mSig = (ms?.signal || []) as Array<number | null>;
78
+ const mHist = (ms?.hist || []) as Array<number | null>;
79
+ const vals: number[] = [];
80
+ for (const s of [mLine, mSig, mHist]) {
81
+ const sliced = s.slice(pastBuffer);
82
+ for (let i = 0; i < sliced.length; i++) {
83
+ const v = sliced[i];
84
+ if (v != null && i < displayItems.length) vals.push(v as number);
85
+ }
86
+ }
87
+ if (vals.length > 0) {
88
+ const mMin = Math.min(...vals);
89
+ const mMax = Math.max(...vals);
90
+ const pad = (mMax - mMin) * 0.1 || 1;
91
+ const yMin = mMin - pad;
92
+ const yMax = mMax + pad;
93
+
94
+ if (yMin < 0 && yMax > 0) {
95
+ pc += `<line x1="${padding.left}" y1="${py(0, yMin, yMax)}" x2="${w - padding.right}" y2="${py(0, yMin, yMax)}" stroke="#4b5563" stroke-width="0.5" stroke-dasharray="4 4"/>`;
96
+ }
97
+ const hBarW = Math.max(1, barW * 0.7);
98
+ mHist.forEach((val, i) => {
99
+ if (val == null) return;
100
+ const idx = i - pastBuffer;
101
+ if (idx < 0 || idx >= displayItems.length) return;
102
+ const cx = x(idx);
103
+ const topY = py(val as number, yMin, yMax);
104
+ const zeroY = py(0, yMin, yMax);
105
+ const color = (val as number) >= 0 ? 'rgba(34,197,94,0.6)' : 'rgba(239,68,68,0.6)';
106
+ pc += `<rect x="${Number((cx - hBarW / 2).toFixed(1))}" y="${Math.min(topY, zeroY)}" width="${Number(hBarW.toFixed(1))}" height="${Math.max(1, Math.abs(topY - zeroY))}" fill="${color}"/>`;
107
+ });
108
+ const lPts: string[] = [];
109
+ mLine.forEach((v, i) => {
110
+ if (v != null) {
111
+ const idx = i - pastBuffer;
112
+ if (idx >= 0 && idx < displayItems.length) lPts.push(`${x(idx)},${py(v as number, yMin, yMax)}`);
113
+ }
114
+ });
115
+ if (lPts.length > 1) pc += `<path d="M ${lPts.join(' L ')}" fill="none" stroke="#3b82f6" stroke-width="1.5"/>`;
116
+ const sPts: string[] = [];
117
+ mSig.forEach((v, i) => {
118
+ if (v != null) {
119
+ const idx = i - pastBuffer;
120
+ if (idx >= 0 && idx < displayItems.length) sPts.push(`${x(idx)},${py(v as number, yMin, yMax)}`);
121
+ }
122
+ });
123
+ if (sPts.length > 1) pc += `<path d="M ${sPts.join(' L ')}" fill="none" stroke="#f97316" stroke-width="1.5"/>`;
124
+ const mt = niceTicks(yMin, yMax, 3);
125
+ mt.forEach((v) => {
126
+ pc += `<text x="${padding.left - 8}" y="${py(v, yMin, yMax)}" text-anchor="end" dominant-baseline="middle" fill="#9ca3af" font-size="10">${v.toFixed(0)}</text>`;
127
+ });
128
+ }
129
+ pc += `<text x="${padding.left + 4}" y="${currentTop + 12}" fill="#9ca3af" font-size="10" font-weight="bold">MACD</text>`;
130
+ pc += `<line x1="${padding.left + 50}" y1="${currentTop + 8}" x2="${padding.left + 62}" y2="${currentTop + 8}" stroke="#3b82f6" stroke-width="1.5"/>`;
131
+ pc += `<text x="${padding.left + 65}" y="${currentTop + 12}" fill="#9ca3af" font-size="9">MACD</text>`;
132
+ pc += `<line x1="${padding.left + 100}" y1="${currentTop + 8}" x2="${padding.left + 112}" y2="${currentTop + 8}" stroke="#f97316" stroke-width="1.5"/>`;
133
+ pc += `<text x="${padding.left + 115}" y="${currentTop + 12}" fill="#9ca3af" font-size="9">Signal</text>`;
134
+ return pc;
135
+ }
136
+
137
+ function renderRsiPanel(currentTop: number, ctx: SubPanelContext): string {
138
+ const { x, padding, plotW, w, effectivePrecision, pastBuffer, displayItems, indicators } = ctx;
139
+ const rMin = 0,
140
+ rMax = 100;
141
+ const py = (v: number) => subPanelY(v, rMin, rMax, currentTop, effectivePrecision);
142
+ let pc = '';
143
+
144
+ const rsiSeries = (indicators?.RSI_14_series || []) as Array<number | null>;
145
+ pc += `<rect x="${padding.left}" y="${py(100)}" width="${plotW}" height="${Math.abs(py(RSI_OVERBOUGHT) - py(100))}" fill="rgba(239,68,68,0.06)"/>`;
146
+ pc += `<rect x="${padding.left}" y="${py(RSI_OVERSOLD)}" width="${plotW}" height="${Math.abs(py(0) - py(RSI_OVERSOLD))}" fill="rgba(34,197,94,0.06)"/>`;
147
+ (
148
+ [
149
+ { v: RSI_OVERBOUGHT, c: '#ef4444', d: '2 2' },
150
+ { v: RSI_MIDLINE, c: '#4b5563', d: '4 4' },
151
+ { v: RSI_OVERSOLD, c: '#22c55e', d: '2 2' },
152
+ ] as const
153
+ ).forEach(({ v, c, d }) => {
154
+ pc += `<line x1="${padding.left}" y1="${py(v)}" x2="${w - padding.right}" y2="${py(v)}" stroke="${c}" stroke-width="0.5" stroke-dasharray="${d}"/>`;
155
+ });
156
+ const rPts: string[] = [];
157
+ rsiSeries.forEach((v, i) => {
158
+ if (v != null) {
159
+ const idx = i - pastBuffer;
160
+ if (idx >= 0 && idx < displayItems.length) rPts.push(`${x(idx)},${py(v as number)}`);
161
+ }
162
+ });
163
+ if (rPts.length > 1) pc += `<path d="M ${rPts.join(' L ')}" fill="none" stroke="#a78bfa" stroke-width="1.5"/>`;
164
+ [0, RSI_OVERSOLD, RSI_MIDLINE, RSI_OVERBOUGHT, 100].forEach((v) => {
165
+ pc += `<text x="${padding.left - 8}" y="${py(v)}" text-anchor="end" dominant-baseline="middle" fill="#9ca3af" font-size="10">${v}</text>`;
166
+ });
167
+ pc += `<text x="${padding.left + 4}" y="${currentTop + 12}" fill="#9ca3af" font-size="10" font-weight="bold">RSI (14)</text>`;
168
+ pc += `<line x1="${padding.left + 65}" y1="${currentTop + 8}" x2="${padding.left + 77}" y2="${currentTop + 8}" stroke="#a78bfa" stroke-width="1.5"/>`;
169
+ pc += `<text x="${padding.left + 80}" y="${currentTop + 12}" fill="#9ca3af" font-size="9">RSI</text>`;
170
+ return pc;
171
+ }
172
+
173
+ function renderVolumePanel(currentTop: number, ctx: SubPanelContext): string {
174
+ const { x, padding, effectivePrecision, displayItems, barW } = ctx;
175
+ const volumes = displayItems.map((d) => (d.volume as number) || 0);
176
+ const vMax = Math.max(...volumes) || 1;
177
+ const py = (v: number) => subPanelY(v, 0, vMax, currentTop, effectivePrecision);
178
+ let pc = '';
179
+
180
+ volumes.forEach((vol, i) => {
181
+ if (vol <= 0) return;
182
+ const cx = x(i);
183
+ const topY = py(vol);
184
+ const bottomY = py(0);
185
+ const up = displayItems[i].close >= displayItems[i].open;
186
+ const color = up ? 'rgba(34,197,94,0.5)' : 'rgba(239,68,68,0.5)';
187
+ const vBarW = barW;
188
+ pc += `<rect x="${Number((cx - vBarW / 2).toFixed(1))}" y="${topY}" width="${Number(vBarW.toFixed(1))}" height="${Math.max(1, bottomY - topY)}" fill="${color}"/>`;
189
+ });
190
+ const vt = niceTicks(0, vMax, 3);
191
+ vt.forEach((v) => {
192
+ const label =
193
+ v >= 1e9
194
+ ? `${(v / 1e9).toFixed(1)}B`
195
+ : v >= 1e6
196
+ ? `${(v / 1e6).toFixed(1)}M`
197
+ : v >= 1e3
198
+ ? `${(v / 1e3).toFixed(0)}K`
199
+ : v.toFixed(0);
200
+ pc += `<text x="${padding.left - 8}" y="${py(v)}" text-anchor="end" dominant-baseline="middle" fill="#9ca3af" font-size="10">${label}</text>`;
201
+ });
202
+ pc += `<text x="${padding.left + 4}" y="${currentTop + 12}" fill="#9ca3af" font-size="10" font-weight="bold">Volume</text>`;
203
+ pc += `<rect x="${padding.left + 55}" y="${currentTop + 4}" width="8" height="8" fill="rgba(34,197,94,0.5)"/>`;
204
+ pc += `<text x="${padding.left + 66}" y="${currentTop + 12}" fill="#9ca3af" font-size="9">Up</text>`;
205
+ pc += `<rect x="${padding.left + 85}" y="${currentTop + 4}" width="8" height="8" fill="rgba(239,68,68,0.5)"/>`;
206
+ pc += `<text x="${padding.left + 96}" y="${currentTop + 12}" fill="#9ca3af" font-size="9">Down</text>`;
207
+ return pc;
208
+ }
@@ -0,0 +1,102 @@
1
+ /**
2
+ * chart/svg-utils — SVG チャート描画で共通利用するユーティリティ。
3
+ *
4
+ * 全関数は純粋関数(副作用なし)。
5
+ */
6
+
7
+ // ── Y 軸目盛り ──
8
+
9
+ /** Y 軸スケール用の "きれいな" 目盛りを生成する */
10
+ export function niceTicks(min: number, max: number, count = 5): number[] {
11
+ if (max < min) [min, max] = [max, min];
12
+ const range = max - min;
13
+ if (range === 0) return [min];
14
+
15
+ const step = Math.max(1e-9, 10 ** Math.floor(Math.log10(range / count)));
16
+ const err = (count * step) / range;
17
+
18
+ let niceStep: number;
19
+ if (err <= 0.15) niceStep = step * 10;
20
+ else if (err <= 0.35) niceStep = step * 5;
21
+ else if (err <= 0.75) niceStep = step * 2;
22
+ else niceStep = step;
23
+
24
+ const precision = Math.max(0, -Math.floor(Math.log10(niceStep)));
25
+ const niceMin = Math.round(min / niceStep) * niceStep;
26
+ const ticks: number[] = [];
27
+ for (let v = niceMin; ticks.length < 20 && v <= max * 1.01; v += niceStep) {
28
+ ticks.push(Number(v.toFixed(precision)));
29
+ }
30
+
31
+ return ticks;
32
+ }
33
+
34
+ /** Y 軸ラベルの省略表示フォーマッタ */
35
+ export function formatYLabel(val: number, isJpyPair: boolean): string {
36
+ const abs = Math.abs(val);
37
+ const prefix = isJpyPair ? '¥' : '';
38
+ if (abs >= 1_000_000_000) return `${prefix}${(val / 1_000_000_000).toFixed(1)}B`;
39
+ if (abs >= 1_000_000) return `${prefix}${(val / 1_000_000).toFixed(abs >= 10_000_000 ? 1 : 2)}M`;
40
+ if (abs >= 10_000) return `${prefix}${(val / 1_000).toFixed(abs >= 100_000 ? 0 : 1)}K`;
41
+ if (abs >= 1_000) return `${prefix}${val.toLocaleString('ja-JP')}`;
42
+ return `${prefix}${val}`;
43
+ }
44
+
45
+ // ── ポイント簡略化 ──
46
+
47
+ export type Pt = { x: number; y: number };
48
+
49
+ /** RDP 風のポイント簡略化 */
50
+ export function simplifyPts(raw: Pt[], tolerance: number): Pt[] {
51
+ if (tolerance <= 0 || raw.length <= 2) return raw;
52
+ const sqTol = tolerance * tolerance;
53
+ const simplified: Pt[] = [raw[0]];
54
+ for (let i = 1; i < raw.length - 1; i++) {
55
+ const a = raw[i - 1],
56
+ b = raw[i],
57
+ c = raw[i + 1];
58
+ const area = Math.abs((b.x - a.x) * (c.y - a.y) - (b.y - a.y) * (c.x - a.x));
59
+ const dx = c.x - a.x;
60
+ const dy = c.y - a.y;
61
+ const len2 = dx * dx + dy * dy || 1;
62
+ if ((area * area) / len2 >= sqTol) simplified.push(b);
63
+ }
64
+ simplified.push(raw[raw.length - 1]);
65
+ return simplified;
66
+ }
67
+
68
+ // ── サニタイゼーション ──
69
+
70
+ /** SVG 文字列から script タグと on* イベントハンドラを除去 */
71
+ export function sanitizeSvg(s: string): string {
72
+ return s
73
+ .replace(/<script[\s\S]*?>[\s\S]*?<\/script>/gi, '')
74
+ .replace(/\son[a-z]+="[^"]*"/gi, '')
75
+ .replace(/\son[a-z]+='[^']*'/gi, '');
76
+ }
77
+
78
+ // ── カラーマップ ──
79
+
80
+ export const smaColors: Record<number, string> = {
81
+ 5: '#f472b6',
82
+ 20: '#a78bfa',
83
+ 25: '#3b82f6',
84
+ 50: '#22d3ee',
85
+ 75: '#f59e0b',
86
+ 200: '#10b981',
87
+ };
88
+
89
+ export const emaColors: Record<number, string> = {
90
+ 12: '#ff6b35',
91
+ 26: '#ffd166',
92
+ 50: '#ef476f',
93
+ 200: '#06d6a0',
94
+ };
95
+
96
+ export const bbColors = {
97
+ bandFill2: 'rgba(59, 130, 246, 0.10)',
98
+ line1: '#9ca3af',
99
+ line2: '#3b82f6',
100
+ line3: '#f59e0b',
101
+ middle: '#9ca3af',
102
+ } as const;