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,562 @@
1
+ import { dayjs } from './datetime.js';
2
+
3
+ export interface PatternDiagramData {
4
+ svg: string;
5
+ artifact: {
6
+ identifier: string;
7
+ title: string;
8
+ };
9
+ }
10
+
11
+ export interface SupportResistanceDiagramData {
12
+ svg: string;
13
+ artifact: {
14
+ identifier: string;
15
+ title: string;
16
+ };
17
+ }
18
+
19
+ interface SRLevel {
20
+ price: number;
21
+ pctFromCurrent: number;
22
+ strength: number; // 1-3 (★の数)
23
+ label: string; // "第1サポート", "第1レジスタンス"等
24
+ note?: string; // "6回目の試し中", "25日線", "空白地帯"等
25
+ }
26
+
27
+ function getPatternLabel(patternType: string): string {
28
+ switch (patternType) {
29
+ case 'double_bottom':
30
+ return 'ダブルボトム';
31
+ case 'double_top':
32
+ return 'ダブルトップ';
33
+ case 'head_and_shoulders':
34
+ return 'ヘッドアンドショルダー';
35
+ case 'inverse_head_and_shoulders':
36
+ return '逆ヘッドアンドショルダー';
37
+ case 'triple_top':
38
+ return 'トリプルトップ';
39
+ case 'triple_bottom':
40
+ return 'トリプルボトム';
41
+ case 'falling_wedge':
42
+ return 'フォーリングウェッジ';
43
+ case 'rising_wedge':
44
+ return 'ライジングウェッジ';
45
+ default:
46
+ return patternType;
47
+ }
48
+ }
49
+
50
+ function formatDateShort(iso?: string): string {
51
+ if (!iso) return '';
52
+ const d = dayjs(iso).utc();
53
+ return `${d.month() + 1}/${d.date()}`;
54
+ }
55
+
56
+ function formatDateIsoShort(iso?: string): string {
57
+ if (!iso) return '';
58
+ return String(iso).split('T')[0] || String(iso);
59
+ }
60
+
61
+ export function generatePatternDiagram(
62
+ patternType: string,
63
+ pivots: Array<{ idx: number; price: number; kind: 'H' | 'L'; date?: string }>,
64
+ neckline: { price: number },
65
+ range: { start: string; end: string },
66
+ options?: { isForming?: boolean },
67
+ ): PatternDiagramData {
68
+ const startDate = formatDateIsoShort(range.start);
69
+ const _endDate = formatDateIsoShort(range.end);
70
+ const identifier = `${patternType}-diagram-${startDate}`;
71
+ const title = `${getPatternLabel(patternType)}構造図 (${formatDateShort(range.start)}-${formatDateShort(range.end)})`;
72
+ const dashed = options?.isForming ? '5,5' : '';
73
+
74
+ if (patternType === 'double_bottom') {
75
+ // Expect order: valley1 (L), peak (H), valley2 (L)
76
+ const v1 = pivots.find((p) => p.kind === 'L');
77
+ const pk = pivots.find((p) => p.kind === 'H');
78
+ const rest = pivots.filter((p) => p.kind === 'L' && p !== v1);
79
+ const v2 = rest.length ? rest[0] : undefined;
80
+ const valley1Date = formatDateShort(v1?.date);
81
+ const peakDate = formatDateShort(pk?.date);
82
+ const valley2Date = formatDateShort(v2?.date);
83
+ const necklinePrice = Math.round(neckline.price).toLocaleString('ja-JP');
84
+ const svg = `<svg width="600" height="300" xmlns="http://www.w3.org/2000/svg">
85
+ <rect width="600" height="300" fill="#f8f9fa"/>
86
+ <line x1="50" y1="100" x2="550" y2="100" stroke="#666" stroke-width="2" stroke-dasharray="5,5"/>
87
+ <text x="350" y="95" fill="#555" font-size="12">ネックライン: ${necklinePrice}円</text>
88
+ <polyline points="150,250 250,100 350,250" fill="none" stroke="#ccc" stroke-width="2"/>
89
+ <line x1="50" y1="50" x2="150" y2="250" stroke="#ccc" stroke-width="2" stroke-dasharray="${dashed}"/>
90
+ <line x1="350" y1="250" x2="550" y2="50" stroke="#ccc" stroke-width="2" stroke-dasharray="${dashed}"/>
91
+ <circle cx="150" cy="250" r="6" fill="#3b82f6"/>
92
+ <text x="150" y="280" text-anchor="middle" fill="#333" font-size="14">谷: ${valley1Date}</text>
93
+ <circle cx="250" cy="100" r="6" fill="#3b82f6"/>
94
+ <text x="250" y="80" text-anchor="middle" fill="#333" font-size="14">山: ${peakDate}</text>
95
+ <circle cx="350" cy="250" r="6" fill="#3b82f6"/>
96
+ <text x="350" y="280" text-anchor="middle" fill="#333" font-size="14">谷: ${valley2Date}</text>
97
+ <text x="300" y="30" text-anchor="middle" fill="#111" font-size="14">${getPatternLabel(patternType)} (${formatDateShort(range.start)}-${formatDateShort(range.end)})</text>
98
+ </svg>`;
99
+ return { svg, artifact: { identifier, title } };
100
+ }
101
+
102
+ if (patternType === 'double_top') {
103
+ // Expect order: peak1 (H), valley (L), peak2 (H)
104
+ const p1 = pivots.find((p) => p.kind === 'H');
105
+ const vl = pivots.find((p) => p.kind === 'L');
106
+ const rest = pivots.filter((p) => p.kind === 'H' && p !== p1);
107
+ const p2 = rest.length ? rest[0] : undefined;
108
+ const peak1Date = formatDateShort(p1?.date);
109
+ const valleyDate = formatDateShort(vl?.date);
110
+ const peak2Date = formatDateShort(p2?.date);
111
+ const necklinePrice = Math.round(neckline.price).toLocaleString('ja-JP');
112
+ const svg = `<svg width="600" height="300" xmlns="http://www.w3.org/2000/svg">
113
+ <rect width="600" height="300" fill="#f8f9fa"/>
114
+ <line x1="50" y1="200" x2="550" y2="200" stroke="#666" stroke-width="2" stroke-dasharray="5,5"/>
115
+ <text x="350" y="215" fill="#555" font-size="12">ネックライン: ${necklinePrice}円</text>
116
+ <polyline points="150,70 250,200 350,70" fill="none" stroke="#ccc" stroke-width="2"/>
117
+ <line x1="50" y1="250" x2="150" y2="70" stroke="#ccc" stroke-width="2" stroke-dasharray="${dashed}"/>
118
+ <line x1="350" y1="70" x2="550" y2="250" stroke="#ccc" stroke-width="2" stroke-dasharray="${dashed}"/>
119
+ <circle cx="150" cy="70" r="6" fill="#3b82f6"/>
120
+ <text x="150" y="55" text-anchor="middle" fill="#333" font-size="14">山: ${peak1Date}</text>
121
+ <circle cx="250" cy="200" r="6" fill="#3b82f6"/>
122
+ <text x="250" y="235" text-anchor="middle" fill="#333" font-size="14">谷: ${valleyDate}</text>
123
+ <circle cx="350" cy="70" r="6" fill="#3b82f6"/>
124
+ <text x="350" y="55" text-anchor="middle" fill="#333" font-size="14">山: ${peak2Date}</text>
125
+ <text x="300" y="30" text-anchor="middle" fill="#111" font-size="14">${getPatternLabel(patternType)} (${formatDateShort(range.start)}-${formatDateShort(range.end)})</text>
126
+ </svg>`;
127
+ return { svg, artifact: { identifier, title } };
128
+ }
129
+
130
+ if (patternType === 'inverse_head_and_shoulders') {
131
+ // Expect order: left shoulder (L), peak1 (H), head (L), peak2 (H), right shoulder (L)
132
+ const leftShoulder = pivots[0];
133
+ const peak1 = pivots[1];
134
+ const head = pivots[2];
135
+ const peak2 = pivots[3];
136
+ const rightShoulder = pivots[4];
137
+ const lsDate = formatDateShort(leftShoulder?.date);
138
+ const p1Date = formatDateShort(peak1?.date);
139
+ const headDate = formatDateShort(head?.date);
140
+ const p2Date = formatDateShort(peak2?.date);
141
+ const rsDate = formatDateShort(rightShoulder?.date);
142
+ // ネックライン: 山1/山2の平均価格(表示用)
143
+ const nlVal = ((peak1?.price ?? 0) + (peak2?.price ?? 0)) / 2;
144
+ const necklinePrice = Math.round(nlVal).toLocaleString('ja-JP');
145
+ const svg = `<svg width="600" height="300" xmlns="http://www.w3.org/2000/svg">
146
+ <rect width="600" height="300" fill="#f8f9fa"/>
147
+ <!-- Neckline (peaks average) -->
148
+ <line x1="50" y1="80" x2="550" y2="80" stroke="#666" stroke-width="2" stroke-dasharray="5,5"/>
149
+ <text x="300" y="95" text-anchor="middle" fill="#555" font-size="12">ネックライン: ${necklinePrice}円</text>
150
+ <!-- Structural polyline: L1 -> H1 -> L(head) -> H2 -> L3 -->
151
+ <polyline points="100,180 200,80 300,240 400,80 500,180" fill="none" stroke="#ccc" stroke-width="2"/>
152
+ <!-- Trend guide lines (left/right) -->
153
+ <line x1="50" y1="40" x2="100" y2="180" stroke="#ccc" stroke-width="2" stroke-dasharray="${dashed}"/>
154
+ <line x1="500" y1="180" x2="550" y2="40" stroke="#ccc" stroke-width="2" stroke-dasharray="${dashed}"/>
155
+ <!-- Pivot markers -->
156
+ <circle cx="100" cy="180" r="6" fill="#3b82f6"/>
157
+ <text x="100" y="205" text-anchor="middle" fill="#333" font-size="14">左肩: ${lsDate}</text>
158
+ <circle cx="200" cy="80" r="6" fill="#3b82f6"/>
159
+ <text x="200" y="65" text-anchor="middle" fill="#333" font-size="14">山1: ${p1Date}</text>
160
+ <circle cx="300" cy="240" r="6" fill="#3b82f6"/>
161
+ <text x="300" y="265" text-anchor="middle" fill="#333" font-size="14">谷: ${headDate}</text>
162
+ <circle cx="400" cy="80" r="6" fill="#3b82f6"/>
163
+ <text x="400" y="65" text-anchor="middle" fill="#333" font-size="14">山2: ${p2Date}</text>
164
+ <circle cx="500" cy="180" r="6" fill="#3b82f6"/>
165
+ <text x="500" y="205" text-anchor="middle" fill="#333" font-size="14">右肩: ${rsDate}</text>
166
+ <text x="300" y="20" text-anchor="middle" fill="#111" font-size="14">${getPatternLabel(patternType)} (${formatDateShort(range.start)}-${formatDateShort(range.end)})</text>
167
+ </svg>`;
168
+ return { svg, artifact: { identifier, title } };
169
+ }
170
+
171
+ if (patternType === 'head_and_shoulders') {
172
+ // Expect order: left shoulder (H), valley1 (L), head (H), valley2 (L), right shoulder (H)
173
+ const leftShoulder = pivots[0];
174
+ const valley1 = pivots[1];
175
+ const head = pivots[2];
176
+ const valley2 = pivots[3];
177
+ const rightShoulder = pivots[4];
178
+ const lsDate = formatDateShort(leftShoulder?.date);
179
+ const v1Date = formatDateShort(valley1?.date);
180
+ const headDate = formatDateShort(head?.date);
181
+ const v2Date = formatDateShort(valley2?.date);
182
+ const rsDate = formatDateShort(rightShoulder?.date);
183
+ // ネックライン: 谷1/谷2の平均価格(表示用)
184
+ const nlVal = ((valley1?.price ?? 0) + (valley2?.price ?? 0)) / 2;
185
+ const necklinePrice = Math.round(nlVal).toLocaleString('ja-JP');
186
+ const svg = `<svg width="600" height="300" xmlns="http://www.w3.org/2000/svg">
187
+ <rect width="600" height="300" fill="#f8f9fa"/>
188
+ <!-- Neckline (valleys average) -->
189
+ <line x1="50" y1="220" x2="550" y2="220" stroke="#666" stroke-width="2" stroke-dasharray="5,5"/>
190
+ <text x="300" y="210" text-anchor="middle" fill="#555" font-size="12">ネックライン: ${necklinePrice}円</text>
191
+ <!-- Structural polyline: H1 -> L1 -> H(head) -> L2 -> H3 -->
192
+ <polyline points="100,120 200,220 300,60 400,220 500,120" fill="none" stroke="#ccc" stroke-width="2"/>
193
+ <!-- Trend guide lines (left/right) -->
194
+ <line x1="50" y1="260" x2="100" y2="120" stroke="#ccc" stroke-width="2" stroke-dasharray="${dashed}"/>
195
+ <line x1="500" y1="120" x2="550" y2="260" stroke="#ccc" stroke-width="2" stroke-dasharray="${dashed}"/>
196
+ <!-- Pivot markers -->
197
+ <circle cx="100" cy="120" r="6" fill="#3b82f6"/>
198
+ <text x="100" y="105" text-anchor="middle" fill="#333" font-size="14">左肩: ${lsDate}</text>
199
+ <circle cx="200" cy="220" r="6" fill="#3b82f6"/>
200
+ <text x="200" y="245" text-anchor="middle" fill="#333" font-size="14">谷1: ${v1Date}</text>
201
+ <circle cx="300" cy="60" r="6" fill="#3b82f6"/>
202
+ <text x="300" y="45" text-anchor="middle" fill="#333" font-size="14">山: ${headDate}</text>
203
+ <circle cx="400" cy="220" r="6" fill="#3b82f6"/>
204
+ <text x="400" y="245" text-anchor="middle" fill="#333" font-size="14">谷2: ${v2Date}</text>
205
+ <circle cx="500" cy="120" r="6" fill="#3b82f6"/>
206
+ <text x="500" y="105" text-anchor="middle" fill="#333" font-size="14">右肩: ${rsDate}</text>
207
+ <text x="300" y="20" text-anchor="middle" fill="#111" font-size="14">${getPatternLabel(patternType)} (${formatDateShort(range.start)}-${formatDateShort(range.end)})</text>
208
+ </svg>`;
209
+ return { svg, artifact: { identifier, title } };
210
+ }
211
+
212
+ if (patternType === 'triple_bottom') {
213
+ // Expect order: L1, H1, L2, H2, L3
214
+ const valley1 = pivots[0];
215
+ const peak1 = pivots[1];
216
+ const valley2 = pivots[2];
217
+ const peak2 = pivots[3];
218
+ const valley3 = pivots[4];
219
+ const v1Date = formatDateShort(valley1?.date);
220
+ const p1Date = formatDateShort(peak1?.date);
221
+ const v2Date = formatDateShort(valley2?.date);
222
+ const p2Date = formatDateShort(peak2?.date);
223
+ const v3Date = formatDateShort(valley3?.date);
224
+ const nlVal = ((peak1?.price ?? 0) + (peak2?.price ?? 0)) / 2;
225
+ const necklinePrice = Math.round(nlVal).toLocaleString('ja-JP');
226
+ const svg = `<svg width="600" height="300" xmlns="http://www.w3.org/2000/svg">
227
+ <rect width="600" height="300" fill="#f8f9fa"/>
228
+ <!-- Neckline (peaks average) -->
229
+ <line x1="50" y1="120" x2="550" y2="120" stroke="#666" stroke-width="2" stroke-dasharray="5,5"/>
230
+ <text x="450" y="135" text-anchor="start" fill="#555" font-size="12">ネックライン: ${necklinePrice}円</text>
231
+ <!-- Structural polyline: L1 -> H1 -> L2 -> H2 -> L3 -->
232
+ <polyline points="80,250 165,120 250,250 335,120 420,250" fill="none" stroke="#ccc" stroke-width="2"/>
233
+ <!-- Trend guide lines -->
234
+ <line x1="30" y1="80" x2="80" y2="250" stroke="#ccc" stroke-width="2" stroke-dasharray="${dashed}"/>
235
+ <line x1="420" y1="250" x2="570" y2="80" stroke="#ccc" stroke-width="2" stroke-dasharray="${dashed}"/>
236
+ <!-- Pivots -->
237
+ <circle cx="80" cy="250" r="6" fill="#3b82f6"/><text x="80" y="275" text-anchor="middle" fill="#333" font-size="14">谷1: ${v1Date}</text>
238
+ <circle cx="165" cy="120" r="6" fill="#3b82f6"/><text x="165" y="105" text-anchor="middle" fill="#333" font-size="14">山1: ${p1Date}</text>
239
+ <circle cx="250" cy="250" r="6" fill="#3b82f6"/><text x="250" y="275" text-anchor="middle" fill="#333" font-size="14">谷2: ${v2Date}</text>
240
+ <circle cx="335" cy="120" r="6" fill="#3b82f6"/><text x="335" y="105" text-anchor="middle" fill="#333" font-size="14">山2: ${p2Date}</text>
241
+ <circle cx="420" cy="250" r="6" fill="#3b82f6"/><text x="420" y="275" text-anchor="middle" fill="#333" font-size="14">谷3: ${v3Date}</text>
242
+ <text x="300" y="20" text-anchor="middle" fill="#111" font-size="14">${getPatternLabel(patternType)} (${formatDateShort(range.start)}-${formatDateShort(range.end)})</text>
243
+ </svg>`;
244
+ return { svg, artifact: { identifier, title } };
245
+ }
246
+
247
+ if (patternType === 'triple_top') {
248
+ // Expect order: H1, L1, H2, L2, H3
249
+ const peak1 = pivots[0];
250
+ const valley1 = pivots[1];
251
+ const peak2 = pivots[2];
252
+ const valley2 = pivots[3];
253
+ const peak3 = pivots[4];
254
+ const p1Date = formatDateShort(peak1?.date);
255
+ const v1Date = formatDateShort(valley1?.date);
256
+ const p2Date = formatDateShort(peak2?.date);
257
+ const v2Date = formatDateShort(valley2?.date);
258
+ const p3Date = formatDateShort(peak3?.date);
259
+ const nlVal = ((valley1?.price ?? 0) + (valley2?.price ?? 0)) / 2;
260
+ const necklinePrice = Math.round(nlVal).toLocaleString('ja-JP');
261
+ const svg = `<svg width="600" height="300" xmlns="http://www.w3.org/2000/svg">
262
+ <rect width="600" height="300" fill="#f8f9fa"/>
263
+ <!-- Neckline (valleys average) -->
264
+ <line x1="50" y1="210" x2="550" y2="210" stroke="#666" stroke-width="2" stroke-dasharray="5,5"/>
265
+ <text x="480" y="200" text-anchor="start" fill="#555" font-size="12">ネックライン: ${necklinePrice}円</text>
266
+ <!-- Structural polyline: H1 -> L1 -> H2 -> L2 -> H3 -->
267
+ <polyline points="80,80 165,210 250,80 335,210 420,80" fill="none" stroke="#ccc" stroke-width="2"/>
268
+ <!-- Trend guide lines -->
269
+ <line x1="30" y1="250" x2="80" y2="80" stroke="#ccc" stroke-width="2" stroke-dasharray="${dashed}"/>
270
+ <line x1="420" y1="80" x2="570" y2="250" stroke="#ccc" stroke-width="2" stroke-dasharray="${dashed}"/>
271
+ <!-- Pivots -->
272
+ <circle cx="80" cy="80" r="6" fill="#3b82f6"/><text x="80" y="65" text-anchor="middle" fill="#333" font-size="14">山1: ${p1Date}</text>
273
+ <circle cx="165" cy="210" r="6" fill="#3b82f6"/><text x="165" y="235" text-anchor="middle" fill="#333" font-size="14">谷1: ${v1Date}</text>
274
+ <circle cx="250" cy="80" r="6" fill="#3b82f6"/><text x="250" y="65" text-anchor="middle" fill="#333" font-size="14">山2: ${p2Date}</text>
275
+ <circle cx="335" cy="210" r="6" fill="#3b82f6"/><text x="335" y="235" text-anchor="middle" fill="#333" font-size="14">谷2: ${v2Date}</text>
276
+ <circle cx="420" cy="80" r="6" fill="#3b82f6"/><text x="420" y="65" text-anchor="middle" fill="#333" font-size="14">山3: ${p3Date}</text>
277
+ <text x="300" y="20" text-anchor="middle" fill="#111" font-size="14">${getPatternLabel(patternType)} (${formatDateShort(range.start)}-${formatDateShort(range.end)})</text>
278
+ </svg>`;
279
+ return { svg, artifact: { identifier, title } };
280
+ }
281
+
282
+ if (patternType === 'falling_wedge') {
283
+ // テンプレート図(600x300)。上側/下側の収束ライン、主要タッチ、アペックス、上抜け矢印を描画
284
+ // 傾きや位置はサンプル配置(視覚的分かりやすさ優先)
285
+ const startShort = formatDateShort(range.start);
286
+ const endShort = formatDateShort(range.end);
287
+ // 主要タッチポイント(間引き想定、固定配置)
288
+ const touchPoints = [
289
+ { x: 100, y: 80 },
290
+ { x: 200, y: 100 },
291
+ { x: 300, y: 120 },
292
+ { x: 400, y: 140 },
293
+ { x: 500, y: 160 },
294
+ { x: 150, y: 180 },
295
+ { x: 250, y: 200 },
296
+ { x: 350, y: 220 },
297
+ { x: 450, y: 240 },
298
+ ];
299
+ const zigzag = '100,80 150,180 200,100 250,200 300,120 350,220 400,140 450,240 500,160';
300
+ const svg = `<svg width="600" height="300" xmlns="http://www.w3.org/2000/svg">
301
+ <rect width="600" height="300" fill="#f8f9fa"/>
302
+ <!-- 収束ライン(上側・下側) -->
303
+ <line x1="100" y1="80" x2="500" y2="180" stroke="#ccc" stroke-width="2" stroke-dasharray="${dashed}"/>
304
+ <line x1="100" y1="180" x2="500" y2="240" stroke="#ccc" stroke-width="2" stroke-dasharray="${dashed}"/>
305
+ <!-- 構造ジグザグ -->
306
+ <polyline points="${zigzag}" fill="none" stroke="#bbb" stroke-width="2"/>
307
+ <!-- アペックス(右端やや先) -->
308
+ <circle cx="550" cy="210" r="8" fill="#f97316"/>
309
+ <text x="550" y="230" text-anchor="middle" fill="#333" font-size="12">収束点</text>
310
+ <!-- ブレイクアウト矢印(上方向) -->
311
+ <path d="M 520 160 L 520 100 L 510 110 M 520 100 L 530 110" stroke="#16a34a" stroke-width="3" fill="none"/>
312
+ <text x="540" y="100" fill="#16a34a" font-size="14">上抜け期待</text>
313
+ <!-- 主要タッチポイント(間引き) -->
314
+ ${touchPoints
315
+ .slice(0, 5)
316
+ .map((p) => `<circle cx="${p.x}" cy="${p.y}" r="6" fill="#3b82f6"/>`)
317
+ .join('')}
318
+ <text x="300" y="30" text-anchor="middle" fill="#111" font-size="14">${getPatternLabel(patternType)} (${startShort}-${endShort})</text>
319
+ </svg>`;
320
+ return { svg, artifact: { identifier, title } };
321
+ }
322
+
323
+ if (patternType === 'rising_wedge') {
324
+ // テンプレート図(600x300)。上側/下側の収束ライン、主要タッチ、アペックス、下抜け矢印を描画
325
+ const startShort = formatDateShort(range.start);
326
+ const endShort = formatDateShort(range.end);
327
+ const touchPoints = [
328
+ { x: 100, y: 240 },
329
+ { x: 200, y: 220 },
330
+ { x: 300, y: 200 },
331
+ { x: 400, y: 180 },
332
+ { x: 500, y: 160 },
333
+ { x: 150, y: 200 },
334
+ { x: 250, y: 180 },
335
+ { x: 350, y: 160 },
336
+ { x: 450, y: 140 },
337
+ ];
338
+ const zigzag = '100,240 150,200 200,220 250,180 300,200 350,160 400,180 450,140 500,160';
339
+ const svg = `<svg width="600" height="300" xmlns="http://www.w3.org/2000/svg">
340
+ <rect width="600" height="300" fill="#f8f9fa"/>
341
+ <!-- 収束ライン(上側・下側) -->
342
+ <line x1="100" y1="200" x2="500" y2="100" stroke="#ccc" stroke-width="2" stroke-dasharray="${dashed}"/>
343
+ <line x1="100" y1="240" x2="500" y2="140" stroke="#ccc" stroke-width="2" stroke-dasharray="${dashed}"/>
344
+ <!-- 構造ジグザグ -->
345
+ <polyline points="${zigzag}" fill="none" stroke="#bbb" stroke-width="2"/>
346
+ <!-- アペックス(右端やや先) -->
347
+ <circle cx="550" cy="110" r="8" fill="#f97316"/>
348
+ <text x="550" y="95" text-anchor="middle" fill="#333" font-size="12">収束点</text>
349
+ <!-- ブレイクアウト矢印(下方向) -->
350
+ <path d="M 520 160 L 520 220 L 510 210 M 520 220 L 530 210" stroke="#ef4444" stroke-width="3" fill="none"/>
351
+ <text x="540" y="230" fill="#ef4444" font-size="14">下抜け期待</text>
352
+ <!-- 主要タッチポイント(間引き) -->
353
+ ${touchPoints
354
+ .slice(0, 5)
355
+ .map((p) => `<circle cx="${p.x}" cy="${p.y}" r="6" fill="#3b82f6"/>`)
356
+ .join('')}
357
+ <text x="300" y="30" text-anchor="middle" fill="#111" font-size="14">${getPatternLabel(patternType)} (${startShort}-${endShort})</text>
358
+ </svg>`;
359
+ return { svg, artifact: { identifier, title } };
360
+ }
361
+
362
+ // Fallback (other patterns not yet implemented)
363
+ const svg = `<svg width="600" height="300" xmlns="http://www.w3.org/2000/svg">
364
+ <rect width="600" height="300" fill="#f8f9fa"/>
365
+ <text x="300" y="150" text-anchor="middle" fill="#111" font-size="16">${getPatternLabel(patternType)} 構造図は準備中です</text>
366
+ </svg>`;
367
+ return { svg, artifact: { identifier, title } };
368
+ }
369
+
370
+ export function generateSupportResistanceDiagram(
371
+ currentPrice: number,
372
+ supports: SRLevel[],
373
+ resistances: SRLevel[],
374
+ options?: {
375
+ highlightNearestSupport?: boolean;
376
+ highlightNearestResistance?: boolean;
377
+ },
378
+ ): SupportResistanceDiagramData {
379
+ const identifier = `support-resistance-diagram-${Date.now()}`;
380
+ const title = `サポート・レジスタンス構造図`;
381
+
382
+ // 価格帯を正規化(現在価格を中心に配置)
383
+ const allLevels = [
384
+ ...resistances.map((r) => ({ ...r, type: 'resistance' as const })),
385
+ {
386
+ price: currentPrice,
387
+ pctFromCurrent: 0,
388
+ strength: 0,
389
+ label: '現在価格',
390
+ note: undefined,
391
+ type: 'current' as const,
392
+ },
393
+ ...supports.map((s) => ({ ...s, type: 'support' as const })),
394
+ ].sort((a, b) => b.price - a.price); // 価格降順
395
+
396
+ // SVG設定
397
+ const width = 700;
398
+ const height = 600;
399
+ const margin = { top: 60, right: 150, bottom: 100, left: 100 };
400
+ const plotHeight = height - margin.top - margin.bottom;
401
+
402
+ // 価格レンジ計算
403
+ const maxPrice = allLevels[0].price;
404
+ const minPrice = allLevels[allLevels.length - 1].price;
405
+ const priceRange = maxPrice - minPrice;
406
+ const priceScale = plotHeight / priceRange;
407
+
408
+ // Y座標計算関数
409
+ const getY = (price: number) => {
410
+ return margin.top + (maxPrice - price) * priceScale;
411
+ };
412
+
413
+ // ライン描画データ生成
414
+ const lines = allLevels.map((level, _idx) => {
415
+ const y = getY(level.price);
416
+ const priceStr = Math.round(level.price).toLocaleString('ja-JP');
417
+ const pctStr =
418
+ level.pctFromCurrent !== 0 ? `(${level.pctFromCurrent > 0 ? '+' : ''}${level.pctFromCurrent.toFixed(1)}%)` : '';
419
+
420
+ let color = '#666';
421
+ let strokeWidth = 2;
422
+ let dashArray = '5,5';
423
+ let emoji = '';
424
+
425
+ if (level.type === 'resistance') {
426
+ color = '#ef4444';
427
+ strokeWidth = level.strength + 1;
428
+ emoji = '🔴';
429
+ const resistanceIdx = resistances.findIndex((r) => r.price === level.price);
430
+ if (options?.highlightNearestResistance && resistanceIdx === 0) {
431
+ strokeWidth = 5;
432
+ }
433
+ } else if (level.type === 'support') {
434
+ color = '#22c55e';
435
+ strokeWidth = level.strength + 1;
436
+ emoji = '🟢';
437
+ const supportIdx = supports.findIndex((s) => s.price === level.price);
438
+ if (options?.highlightNearestSupport && supportIdx === 0) {
439
+ strokeWidth = 5;
440
+ }
441
+ } else if (level.type === 'current') {
442
+ color = '#3b82f6';
443
+ strokeWidth = 3;
444
+ dashArray = '';
445
+ emoji = '📍';
446
+ }
447
+
448
+ const stars = level.strength > 0 ? ' ' + '★'.repeat(level.strength) + '☆'.repeat(3 - level.strength) : '';
449
+ const labelText = `${emoji} ${level.label}: ${priceStr}円 ${pctStr}${stars}`;
450
+
451
+ return {
452
+ y,
453
+ color,
454
+ strokeWidth,
455
+ dashArray,
456
+ labelText,
457
+ note: level.note,
458
+ type: level.type,
459
+ price: level.price,
460
+ };
461
+ });
462
+
463
+ // 矢印と距離ラベル描画(隣接レベル間)
464
+ const arrows = [];
465
+ for (let i = 0; i < allLevels.length - 1; i++) {
466
+ const current = allLevels[i];
467
+ const next = allLevels[i + 1];
468
+ const y1 = getY(current.price);
469
+ const y2 = getY(next.price);
470
+ const midY = (y1 + y2) / 2;
471
+ const pctDiff = Math.abs(((next.price - current.price) / current.price) * 100);
472
+
473
+ // 特殊な距離(空白地帯など)を強調
474
+ let distanceColor = '#666';
475
+ let distanceLabel = `${pctDiff.toFixed(1)}%`;
476
+
477
+ if (current.type === 'current' && next.type === 'support') {
478
+ if (pctDiff > 2) {
479
+ distanceColor = '#ef4444';
480
+ distanceLabel = `${pctDiff.toFixed(1)}% (空白)`;
481
+ }
482
+ }
483
+
484
+ arrows.push({
485
+ x: margin.left - 40,
486
+ y1: y1 + 5,
487
+ y2: y2 - 5,
488
+ midY,
489
+ label: distanceLabel,
490
+ color: distanceColor,
491
+ });
492
+ }
493
+
494
+ // SVG生成
495
+ const lineElements = lines
496
+ .map(
497
+ (line, _idx) => `
498
+ <line x1="${margin.left}" y1="${line.y}" x2="${width - margin.right + 30}" y2="${line.y}"
499
+ stroke="${line.color}" stroke-width="${line.strokeWidth}" ${line.dashArray ? `stroke-dasharray="${line.dashArray}"` : ''} />
500
+ <text x="${width - margin.right + 40}" y="${line.y + 5}" fill="${line.color}" font-size="13" font-weight="bold">
501
+ ${line.labelText}
502
+ </text>
503
+ ${
504
+ line.note
505
+ ? `<text x="${width - margin.right + 40}" y="${line.y + 20}" fill="#666" font-size="11">
506
+ (${line.note})
507
+ </text>`
508
+ : ''
509
+ }`,
510
+ )
511
+ .join('');
512
+
513
+ const arrowElements = arrows
514
+ .map(
515
+ (arrow) => `
516
+ <line x1="${arrow.x}" y1="${arrow.y1}" x2="${arrow.x}" y2="${arrow.y2}"
517
+ stroke="${arrow.color}" stroke-width="1.5" marker-end="url(#arrowhead-${arrow.color.replace('#', '')})" />
518
+ <text x="${arrow.x - 35}" y="${arrow.midY + 4}" fill="${arrow.color}" font-size="11" text-anchor="end">
519
+ ${arrow.label}
520
+ </text>`,
521
+ )
522
+ .join('');
523
+
524
+ // リスク距離の警告レベル判定
525
+ const nearestSupportPct = supports[0]?.pctFromCurrent || 0;
526
+ const nearestResistancePct = resistances[0]?.pctFromCurrent || 0;
527
+ const maxDownsidePct = supports[supports.length - 1]?.pctFromCurrent || 0;
528
+ const maxUpsidePct = resistances[resistances.length - 1]?.pctFromCurrent || 0;
529
+
530
+ const supportWarning =
531
+ Math.abs(nearestSupportPct) < 1 ? ' ⚠️ 非常に近い' : Math.abs(nearestSupportPct) < 2 ? ' (注意)' : '';
532
+
533
+ const svg = `<svg xmlns="http://www.w3.org/2000/svg" width="${width}" height="${height}" viewBox="0 0 ${width} ${height}">
534
+ <defs>
535
+ <marker id="arrowhead-ef4444" markerWidth="10" markerHeight="10" refX="5" refY="5" orient="auto">
536
+ <polygon points="0 0, 10 5, 0 10" fill="#ef4444" />
537
+ </marker>
538
+ <marker id="arrowhead-666" markerWidth="10" markerHeight="10" refX="5" refY="5" orient="auto">
539
+ <polygon points="0 0, 10 5, 0 10" fill="#666" />
540
+ </marker>
541
+ </defs>
542
+ <rect width="${width}" height="${height}" fill="#fafafa"/>
543
+
544
+ <!-- タイトル -->
545
+ <text x="${width / 2}" y="30" text-anchor="middle" fill="#111" font-size="18" font-weight="bold">${title}</text>
546
+
547
+ <!-- ライン -->
548
+ ${lineElements}
549
+
550
+ <!-- 距離矢印 -->
551
+ ${arrowElements}
552
+
553
+ <!-- リスク距離ボックス -->
554
+ <rect x="30" y="${height - 90}" width="280" height="80" fill="white" stroke="#ccc" stroke-width="1" rx="5"/>
555
+ <text x="40" y="${height - 70}" fill="#111" font-size="13" font-weight="bold">リスク距離</text>
556
+ <text x="40" y="${height - 50}" fill="#22c55e" font-size="11">最も近いサポート: ${nearestSupportPct.toFixed(1)}%${supportWarning}</text>
557
+ <text x="40" y="${height - 35}" fill="#ef4444" font-size="11">最も近いレジスタンス: +${nearestResistancePct.toFixed(1)}%</text>
558
+ <text x="40" y="${height - 20}" fill="#666" font-size="11">最大下落リスク: ${maxDownsidePct.toFixed(1)}% / 上昇余地: +${maxUpsidePct.toFixed(1)}%</text>
559
+ </svg>`;
560
+
561
+ return { svg, artifact: { identifier, title } };
562
+ }
package/lib/result.ts ADDED
@@ -0,0 +1,104 @@
1
+ import type { FailResult, OkResult } from '../src/schemas.js';
2
+ import { getErrorMessage, isAbortError } from './error.js';
3
+
4
+ export function ok<T = Record<string, unknown>, M = Record<string, unknown>>(
5
+ summary: string,
6
+ data: T = {} as T,
7
+ meta: M = {} as M,
8
+ ): OkResult<T, M> {
9
+ return {
10
+ ok: true,
11
+ summary,
12
+ data,
13
+ meta,
14
+ };
15
+ }
16
+
17
+ export function fail<M = Record<string, unknown>>(
18
+ message: string,
19
+ type: string = 'user',
20
+ meta: M = {} as M,
21
+ ): FailResult<M> {
22
+ return {
23
+ ok: false,
24
+ summary: `Error: ${message}`,
25
+ data: {},
26
+ meta: { errorType: type, ...(meta as object) } as FailResult<M>['meta'],
27
+ };
28
+ }
29
+
30
+ export interface FailFromErrorOptions {
31
+ /** Zod スキーマ。指定時は fail() 結果を schema.parse() でラップ */
32
+ schema?: { parse: (v: unknown) => unknown };
33
+ /** タイムアウト検出用の timeoutMs 値。指定時は AbortError を 'timeout' として扱う */
34
+ timeoutMs?: number;
35
+ /** タイムアウト以外のデフォルトエラータイプ (default: 'internal') */
36
+ defaultType?: string;
37
+ /** getErrorMessage が空を返した場合のフォールバックメッセージ (default: 'internal error') */
38
+ defaultMessage?: string;
39
+ }
40
+
41
+ /**
42
+ * catch ブロックで捕捉したエラーから fail() 結果を生成する共通ヘルパー。
43
+ *
44
+ * - AbortError → 'timeout' タイプ + タイムアウトメッセージ
45
+ * - その他 → defaultType + エラーメッセージ
46
+ * - schema 指定時は schema.parse() でラップ
47
+ */
48
+ export function failFromError(err: unknown, opts: FailFromErrorOptions = {}): ReturnType<typeof fail> {
49
+ const { schema, timeoutMs, defaultType = 'internal', defaultMessage = 'internal error' } = opts;
50
+
51
+ let message: string;
52
+ let errorType: string;
53
+
54
+ if (timeoutMs != null && isAbortError(err)) {
55
+ message = `タイムアウト (${timeoutMs}ms)`;
56
+ errorType = 'timeout';
57
+ } else {
58
+ message = getErrorMessage(err) || defaultMessage;
59
+ errorType = defaultType;
60
+ }
61
+
62
+ const result = fail(message, errorType);
63
+ return (schema ? schema.parse(result) : result) as ReturnType<typeof fail>;
64
+ }
65
+
66
+ /**
67
+ * Zod スキーマの .parse() 結果を OkResult<T, M> | FailResult として返す型安全ヘルパー。
68
+ *
69
+ * 背景: toolResultSchema() が生成する Zod union の z.infer 型と
70
+ * OkResult<T, M> | FailResult は構造的に一致するが、FailResult の meta 型の
71
+ * 推論差異により TypeScript が直接代入を許可しない。このヘルパーでキャストを
72
+ * 1箇所に集約し、各ツールファイルからキャストを排除する。
73
+ */
74
+ export function parseAsResult<T, M>(
75
+ schema: { parse: (v: unknown) => unknown },
76
+ value: unknown,
77
+ ): OkResult<T, M> | FailResult {
78
+ return schema.parse(value) as OkResult<T, M> | FailResult;
79
+ }
80
+
81
+ /**
82
+ * ensurePair / validateLimit / validateDate の失敗結果から fail() を生成する共通ヘルパー。
83
+ *
84
+ * @param result - バリデーション関数の失敗結果 ({ error: { message, type } })
85
+ * @param schema - Zod スキーマ(指定時は schema.parse() でラップ)
86
+ */
87
+ export function failFromValidation(
88
+ result: { error: { message: string; type: string } },
89
+ schema?: { parse: (v: unknown) => unknown },
90
+ ): FailResult {
91
+ const f = fail(result.error.message, result.error.type);
92
+ return (schema ? schema.parse(f) : f) as FailResult;
93
+ }
94
+
95
+ /**
96
+ * Result オブジェクトを structuredContent 用の Record<string, unknown> に変換する。
97
+ *
98
+ * ツールハンドラが返す McpResponse の structuredContent は Record<string, unknown> を期待するが、
99
+ * Result 型は直接代入できない。このヘルパーでキャストを1箇所に集約し、各ツールファイルから
100
+ * `as unknown as Record<string, unknown>` を排除する。
101
+ */
102
+ export function toStructured(result: object): Record<string, unknown> {
103
+ return result as Record<string, unknown>;
104
+ }