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,682 @@
1
+ import type { z } from 'zod';
2
+ import { nowIso, toDisplayTime } from '../../lib/datetime.js';
3
+ import { formatDeviation, formatPercent, formatPriceJPY, formatTrendSymbol } from '../../lib/formatter.js';
4
+ import { ICHIMOKU_SHIFT, RSI_OVERBOUGHT, RSI_OVERSOLD } from '../../lib/indicator-config.js';
5
+ import { EPSILON } from '../../lib/math.js';
6
+ import { toStructured } from '../../lib/result.js';
7
+ import analyzeIndicators from '../../tools/analyze_indicators.js';
8
+ import { GetIndicatorsInputSchema } from '../schemas.js';
9
+ import type { ToolDefinition } from '../tool-definition.js';
10
+
11
+ // ── テキスト組み立て: 純粋エクスポート関数 ──
12
+
13
+ export interface BuildIndicatorsTextInput {
14
+ pair: string;
15
+ type: string;
16
+ nowJst: string;
17
+ close: number | null;
18
+ prev: number | null;
19
+ deltaPrev: { amt: number; pct: number } | null;
20
+ deltaLabel: string;
21
+ trend: string;
22
+ // RSI
23
+ rsi: number | null;
24
+ recentRsiFormatted: string[];
25
+ rsiUnitLabel: string;
26
+ // MACD
27
+ macdHist: number | null;
28
+ lastMacdCross: { type: 'golden' | 'dead'; barsAgo: number } | null;
29
+ divergence: string | null;
30
+ // SMA
31
+ sma25: number | null;
32
+ sma75: number | null;
33
+ sma200: number | null;
34
+ s25Slope: number | null;
35
+ s75Slope: number | null;
36
+ s200Slope: number | null;
37
+ arrangement: string;
38
+ crossInfo: string | null;
39
+ // BB
40
+ bbMid: number | null;
41
+ bbUp: number | null;
42
+ bbLo: number | null;
43
+ sigmaZ: number | null;
44
+ bandWidthPct: number | null;
45
+ bwTrend: string | null;
46
+ sigmaHistory: Array<{ off: number; z: number } | null> | null;
47
+ // Ichimoku
48
+ tenkan: number | null;
49
+ kijun: number | null;
50
+ spanA: number | null;
51
+ spanB: number | null;
52
+ cloudTop: number | null;
53
+ cloudBot: number | null;
54
+ cloudPos: string;
55
+ cloudThickness: number | null;
56
+ cloudThicknessPct: number | null;
57
+ chikouBull: boolean | null;
58
+ threeSignals: { judge: string };
59
+ toCloudDistance: number | null;
60
+ ichimokuConvSlope: number | null;
61
+ ichimokuBaseSlope: number | null;
62
+ // Stoch
63
+ stochK: number | null;
64
+ stochD: number | null;
65
+ stochPrevK: number | null;
66
+ stochPrevD: number | null;
67
+ // OBV
68
+ obvVal: number | null;
69
+ obvSma20: number | null;
70
+ obvTrend: string | null;
71
+ obvPrev: number | null;
72
+ obvUnit: string;
73
+ }
74
+
75
+ export function buildIndicatorsText(input: BuildIndicatorsTextInput): string {
76
+ const {
77
+ pair,
78
+ type,
79
+ nowJst,
80
+ close,
81
+ prev,
82
+ deltaPrev,
83
+ deltaLabel,
84
+ trend,
85
+ rsi,
86
+ recentRsiFormatted,
87
+ rsiUnitLabel,
88
+ macdHist,
89
+ lastMacdCross,
90
+ divergence,
91
+ sma25,
92
+ sma75,
93
+ sma200,
94
+ s25Slope,
95
+ s75Slope,
96
+ s200Slope,
97
+ arrangement,
98
+ crossInfo,
99
+ bbMid,
100
+ bbUp,
101
+ bbLo,
102
+ sigmaZ,
103
+ bandWidthPct,
104
+ bwTrend,
105
+ sigmaHistory,
106
+ tenkan,
107
+ kijun,
108
+ spanA,
109
+ spanB,
110
+ cloudTop: _cloudTop,
111
+ cloudBot: _cloudBot,
112
+ cloudPos,
113
+ cloudThickness,
114
+ cloudThicknessPct,
115
+ chikouBull,
116
+ threeSignals,
117
+ toCloudDistance,
118
+ ichimokuConvSlope,
119
+ ichimokuBaseSlope,
120
+ stochK,
121
+ stochD,
122
+ stochPrevK,
123
+ stochPrevD,
124
+ obvVal,
125
+ obvSma20,
126
+ obvTrend,
127
+ obvPrev,
128
+ obvUnit,
129
+ } = input;
130
+
131
+ const vsCurPct = (ref?: number | null) => formatDeviation(close, ref);
132
+ const rsiInterp = (val: number | null) => {
133
+ if (val == null) return '—';
134
+ if (val <= RSI_OVERSOLD) return '売られすぎ圏(反発の可能性)';
135
+ if (val < 50) return '弱め(反発余地)';
136
+ if (val < RSI_OVERBOUGHT) return '中立〜強め';
137
+ return '買われすぎ圏(反落の可能性)';
138
+ };
139
+
140
+ const lines: string[] = [];
141
+ // Header with time and 24h change
142
+ lines.push(`=== ${String(pair).toUpperCase()} ${String(type)} 分析 ===`);
143
+ lines.push(`${nowJst} 現在`);
144
+ const chgLine = deltaPrev ? `(${deltaLabel}: ${formatPercent(deltaPrev.pct, { sign: true, digits: 1 })})` : '';
145
+ lines.push(deltaPrev ? `${formatPriceJPY(close)} ${chgLine}` : formatPriceJPY(close));
146
+ lines.push('');
147
+ // 総合判定(簡潔)
148
+ lines.push('【総合判定】');
149
+ const trendText =
150
+ trend === 'strong_downtrend' ? '強い下降トレンド ⚠️' : trend === 'uptrend' ? '上昇トレンド' : '中立/レンジ';
151
+ const rsiHint =
152
+ rsi == null
153
+ ? '—'
154
+ : Number(rsi) <= RSI_OVERSOLD
155
+ ? '売られすぎ'
156
+ : Number(rsi) >= RSI_OVERBOUGHT
157
+ ? '買われすぎ'
158
+ : '中立圏';
159
+ const bwState =
160
+ bandWidthPct == null ? '—' : bandWidthPct < 8 ? 'スクイーズ' : bandWidthPct > 20 ? 'エクスパンション' : '標準';
161
+ lines.push(` トレンド: ${trendText}`);
162
+ lines.push(` 勢い: RSI=${rsi ?? 'n/a'} → ${rsiHint}`);
163
+ lines.push(
164
+ ` リスク: BB幅=${bandWidthPct != null ? `${bandWidthPct}%` : 'n/a'} → ${bwState}${bwTrend ? `(${bwTrend})` : ''}`,
165
+ );
166
+ lines.push('');
167
+ // Momentum
168
+ lines.push('【モメンタム】');
169
+ lines.push(` RSI(14): ${rsi ?? 'n/a'} → ${rsiInterp(rsi)}`);
170
+ if (recentRsiFormatted.length >= 2) {
171
+ lines.push(` 【RSI推移(直近${recentRsiFormatted.length}${rsiUnitLabel})】`);
172
+ lines.push('');
173
+ lines.push(` ${recentRsiFormatted.join(' → ')}`);
174
+ }
175
+ const macdHistFmt = macdHist == null ? 'n/a' : `${Math.round(Number(macdHist)).toLocaleString('ja-JP')}`;
176
+ const macdHint =
177
+ macdHist == null ? '—' : Number(macdHist) >= 0 ? '強気継続(プラス=上昇圧力)' : '弱気継続(マイナス=下落圧力)';
178
+ lines.push(` MACD: hist=${macdHistFmt} → ${macdHint}`);
179
+ const crossStr = lastMacdCross
180
+ ? `${lastMacdCross.type === 'golden' ? 'ゴールデン' : 'デッド'}クロス: ${lastMacdCross.barsAgo}本前`
181
+ : '直近クロス: なし';
182
+ lines.push(` ・${crossStr}`);
183
+ lines.push(` ・ダイバージェンス: ${divergence ?? 'なし'}`);
184
+ lines.push('');
185
+ // Trend (SMA)
186
+ lines.push('【トレンド(移動平均線)】');
187
+ lines.push(` 配置: ${arrangement}`);
188
+ lines.push(` SMA(25): ${formatPriceJPY(sma25)} (${vsCurPct(sma25)}) ${formatTrendSymbol(s25Slope)}`);
189
+ lines.push(` SMA(75): ${formatPriceJPY(sma75)} (${vsCurPct(sma75)}) ${formatTrendSymbol(s75Slope)}`);
190
+ lines.push(` SMA(200): ${formatPriceJPY(sma200)} (${vsCurPct(sma200)}) ${formatTrendSymbol(s200Slope)}`);
191
+ if (crossInfo) lines.push(` ${crossInfo}`);
192
+ lines.push('');
193
+ // Volatility (BB)
194
+ lines.push('【ボラティリティ(ボリンジャーバンド±2σ)】');
195
+ lines.push(
196
+ ` 現在位置: ${sigmaZ != null ? `${sigmaZ}σ` : 'n/a'} → ${sigmaZ != null ? (sigmaZ <= -1 ? '売られすぎ' : sigmaZ >= 1 ? '買われすぎ' : '中立') : '—'}`,
197
+ );
198
+ lines.push(` middle: ${formatPriceJPY(bbMid)} (${vsCurPct(bbMid)})`);
199
+ lines.push(` upper: ${formatPriceJPY(bbUp)} (${vsCurPct(bbUp)})`);
200
+ lines.push(
201
+ ` lower: ${formatPriceJPY(bbLo)} (${vsCurPct(bbLo)})${bbLo != null && close != null && Number(bbLo) < Number(close) ? '' : ' ← 現在価格に近い'}`,
202
+ );
203
+ if (bandWidthPct != null) lines.push(` バンド幅: ${bandWidthPct}% → ${bwTrend ?? '—'}`);
204
+ if (sigmaHistory?.[0] && sigmaHistory[1]) {
205
+ const ago5 = sigmaHistory[0]?.z;
206
+ const curZ = sigmaHistory[1]?.z;
207
+ lines.push(' 過去推移:');
208
+ if (ago5 != null) lines.push(` ・5日前: ${ago5}σ`);
209
+ if (curZ != null) lines.push(` ・現在: ${curZ}σ`);
210
+ }
211
+ lines.push('');
212
+ // Ichimoku
213
+ lines.push('【一目均衡表】');
214
+ lines.push(
215
+ ` 現在位置: ${cloudPos === 'below_cloud' ? '雲の下 → 弱気' : cloudPos === 'above_cloud' ? '雲の上 → 強気' : '雲の中 → 中立'}`,
216
+ );
217
+ lines.push(` 転換線: ${formatPriceJPY(tenkan)} (${vsCurPct(tenkan)}) ${formatTrendSymbol(ichimokuConvSlope)}`);
218
+ lines.push(` 基準線: ${formatPriceJPY(kijun)} (${vsCurPct(kijun)}) ${formatTrendSymbol(ichimokuBaseSlope)}`);
219
+ lines.push(` 先行スパンA: ${formatPriceJPY(spanA)} (${vsCurPct(spanA)})`);
220
+ lines.push(` 先行スパンB: ${formatPriceJPY(spanB)} (${vsCurPct(spanB)})`);
221
+ if (cloudThickness != null)
222
+ lines.push(
223
+ ` 雲の厚さ: ${Math.round(cloudThickness).toLocaleString('ja-JP')}円(${cloudThicknessPct != null ? `${cloudThicknessPct.toFixed(1)}%` : 'n/a'})`,
224
+ );
225
+ if (chikouBull != null) lines.push(` 遅行スパン: ${chikouBull ? '価格より上 → 強気' : '価格より下 → 弱気'}`);
226
+ if (threeSignals) lines.push(` 三役判定: ${threeSignals.judge}`);
227
+ if (toCloudDistance != null && cloudPos === 'below_cloud') lines.push(` 雲突入まで: ${toCloudDistance.toFixed(1)}%`);
228
+ lines.push('');
229
+ // Stochastic RSI
230
+ lines.push('【ストキャスティクスRSI】');
231
+ if (stochK != null && stochD != null) {
232
+ lines.push(` %K: ${Number(stochK).toFixed(1)} %D: ${Number(stochD).toFixed(1)}`);
233
+ const stochZone = Number(stochK) <= 20 ? '売られすぎゾーン' : Number(stochK) >= 80 ? '買われすぎゾーン' : '中立圏';
234
+ const stochStrength =
235
+ Number(stochK) <= 10 ? '(強い売られすぎ)' : Number(stochK) >= 90 ? '(強い買われすぎ)' : '';
236
+ lines.push(` 判定: ${stochZone}${stochStrength}`);
237
+ if (stochPrevK != null && stochPrevD != null) {
238
+ const prevBelow = Number(stochPrevK) < Number(stochPrevD);
239
+ const curAbove = Number(stochK) > Number(stochD);
240
+ const prevAbove = Number(stochPrevK) > Number(stochPrevD);
241
+ const curBelow = Number(stochK) < Number(stochD);
242
+ if (prevBelow && curAbove) {
243
+ lines.push(' クロス: %Kが%Dを上抜け(買いシグナル候補)');
244
+ } else if (prevAbove && curBelow) {
245
+ lines.push(' クロス: %Kが%Dを下抜け(売りシグナル候補)');
246
+ } else {
247
+ lines.push(' クロス: なし');
248
+ }
249
+ }
250
+ } else {
251
+ lines.push(' データ不足');
252
+ }
253
+ lines.push('');
254
+ // OBV
255
+ lines.push('【OBV (On-Balance Volume)】');
256
+ if (obvVal != null) {
257
+ lines.push(` 現在値: ${Number(obvVal).toLocaleString(undefined, { maximumFractionDigits: 2 })} ${obvUnit}`.trim());
258
+ if (obvSma20 != null)
259
+ lines.push(
260
+ ` SMA(20): ${Number(obvSma20).toLocaleString(undefined, { maximumFractionDigits: 2 })} ${obvUnit}`.trim(),
261
+ );
262
+ if (obvTrend != null) {
263
+ const obvTrendLabel =
264
+ obvTrend === 'rising'
265
+ ? 'OBV > SMA → 出来高が上昇を支持'
266
+ : obvTrend === 'falling'
267
+ ? 'OBV < SMA → 出来高が下落を支持'
268
+ : 'OBV ≈ SMA → 出来高中立';
269
+ lines.push(` トレンド: ${obvTrendLabel}`);
270
+ }
271
+ // Divergence check: price direction vs OBV direction over recent bars
272
+ if (obvPrev != null && prev != null && close != null) {
273
+ const priceUp = Number(close) > Number(prev);
274
+ const priceDn = Number(close) < Number(prev);
275
+ const obvUp = Number(obvVal) > Number(obvPrev);
276
+ const obvDn = Number(obvVal) < Number(obvPrev);
277
+ if (priceUp && obvDn) {
278
+ lines.push(' ダイバージェンス: ベアリッシュ(価格↑・OBV↓)→ 上昇の持続力に疑問');
279
+ } else if (priceDn && obvUp) {
280
+ lines.push(' ダイバージェンス: ブルリッシュ(価格↓・OBV↑)→ 反発の可能性');
281
+ } else {
282
+ lines.push(' ダイバージェンス: なし(価格とOBVが同方向)');
283
+ }
284
+ }
285
+ } else {
286
+ lines.push(' データ不足');
287
+ }
288
+ lines.push('');
289
+ lines.push('【次に確認すべきこと】');
290
+ lines.push(' ・より詳しく: analyze_bb_snapshot / analyze_ichimoku_snapshot / analyze_sma_snapshot');
291
+ lines.push(' ・転換サイン例: RSI>40, MACDヒストグラムのプラ転, 25日線の明確な上抜け');
292
+ lines.push('');
293
+ lines.push('詳細は structuredContent.data.indicators / chart を参照。');
294
+ return lines.join('\n');
295
+ }
296
+
297
+ // ── IIFE → 名前付き純粋関数 ──
298
+
299
+ function calcDeltaPrev(close: number | null, prev: number | null): { amt: number; pct: number } | null {
300
+ if (close == null || prev == null || !Number.isFinite(prev) || prev === 0) return null;
301
+ const amt = Number(close) - Number(prev);
302
+ const pct = (amt / Math.abs(Number(prev))) * 100;
303
+ return { amt, pct };
304
+ }
305
+
306
+ function calcDeltaLabel(type: string): string {
307
+ const t = String(type ?? '').toLowerCase();
308
+ if (t.includes('day')) return '前日比';
309
+ if (t.includes('week')) return '前週比';
310
+ if (t.includes('month')) return '前月比';
311
+ if (t.includes('hour')) return '前時間比';
312
+ if (t.includes('min')) return '前足比';
313
+ return '前回比';
314
+ }
315
+
316
+ function extractRecentRsi(rsiSeries: unknown[] | null, count: number): (number | null)[] {
317
+ if (!Array.isArray(rsiSeries) || rsiSeries.length === 0) return [];
318
+ return rsiSeries.slice(-count).map((v: unknown) => {
319
+ const num = Number(v);
320
+ return Number.isFinite(num) ? num : null;
321
+ });
322
+ }
323
+
324
+ function calcRsiUnitLabel(type: string): string {
325
+ const t = String(type ?? '').toLowerCase();
326
+ if (t.includes('day')) return '日';
327
+ if (t.includes('week')) return '週';
328
+ if (t.includes('month')) return '月';
329
+ if (t.includes('hour')) return '時間';
330
+ if (t.includes('min')) return '本';
331
+ return '本';
332
+ }
333
+
334
+ function findLastMacdCross(
335
+ macdArr: number[] | null,
336
+ sigArr: number[] | null,
337
+ ): { type: 'golden' | 'dead'; barsAgo: number } | null {
338
+ if (!macdArr || !sigArr) return null;
339
+ const L = Math.min(macdArr.length, sigArr.length);
340
+ let lastIdx: number | null = null;
341
+ let lastType: 'golden' | 'dead' | null = null;
342
+ for (let i = L - 2; i >= 0; i--) {
343
+ const a0 = Number(macdArr[i]),
344
+ b0 = Number(sigArr[i]);
345
+ const a1 = Number(macdArr[i + 1]),
346
+ b1 = Number(sigArr[i + 1]);
347
+ if ([a0, b0, a1, b1].some((v) => !Number.isFinite(v))) continue;
348
+ const prevDiff = a0 - b0;
349
+ const nextDiff = a1 - b1;
350
+ if (prevDiff === 0) continue;
351
+ if ((prevDiff < 0 && nextDiff > 0) || (prevDiff > 0 && nextDiff < 0)) {
352
+ lastIdx = i + 1;
353
+ lastType = nextDiff > 0 ? 'golden' : 'dead';
354
+ break;
355
+ }
356
+ }
357
+ if (lastIdx == null) return null;
358
+ const barsAgo = L - 1 - lastIdx;
359
+ return { type: lastType as 'golden' | 'dead', barsAgo };
360
+ }
361
+
362
+ function detectDivergence(
363
+ candles: Array<{ close?: number }>,
364
+ histSeries: number[] | null,
365
+ lookback: number,
366
+ ): string | null {
367
+ // simple divergence check over last N bars using linear slope
368
+ const N = Math.min(lookback, candles.length);
369
+ if (N < 5) return null;
370
+ const pxA = Number(candles.at(-N)?.close ?? NaN),
371
+ pxB = Number(candles.at(-1)?.close ?? NaN);
372
+ if (!Number.isFinite(pxA) || !Number.isFinite(pxB) || !histSeries || histSeries.length < N) return null;
373
+ const hA = Number(histSeries.at(-N) ?? NaN),
374
+ hB = Number(histSeries.at(-1) ?? NaN);
375
+ if (!Number.isFinite(hA) || !Number.isFinite(hB)) return null;
376
+ const pxSlopeUp = pxB > pxA,
377
+ pxSlopeDn = pxB < pxA;
378
+ const histSlopeUp = hB > hA,
379
+ histSlopeDn = hB < hA;
380
+ if (pxSlopeUp && histSlopeDn) return 'ベアリッシュ(価格↑・モメンタム↓)';
381
+ if (pxSlopeDn && histSlopeUp) return 'ブルリッシュ(価格↓・モメンタム↑)';
382
+ return 'なし';
383
+ }
384
+
385
+ function calcSmaArrangement(curNum: number, s25n: number, s75n: number, s200n: number): string {
386
+ const pts: Array<{ label: string; v: number }> = [];
387
+ if (Number.isFinite(curNum)) pts.push({ label: '価格', v: curNum });
388
+ if (Number.isFinite(s25n)) pts.push({ label: '25日', v: s25n });
389
+ if (Number.isFinite(s75n)) pts.push({ label: '75日', v: s75n });
390
+ if (Number.isFinite(s200n)) pts.push({ label: '200日', v: s200n });
391
+ if (pts.length < 3) return 'n/a';
392
+ pts.sort((a, b) => a.v - b.v);
393
+ return pts.map((p) => p.label).join(' < ');
394
+ }
395
+
396
+ function calcBandWidthTrend(bbSeries: {
397
+ upper: number[] | null;
398
+ lower: number[] | null;
399
+ middle: number[] | null;
400
+ }): string | null {
401
+ try {
402
+ if (!bbSeries.upper || !bbSeries.lower || !bbSeries.middle) return null;
403
+ const L = Math.min(bbSeries.upper.length, bbSeries.lower.length, bbSeries.middle.length);
404
+ if (L < 6) return null;
405
+ const cur =
406
+ ((bbSeries.upper.at(-1) ?? 0) - (bbSeries.lower.at(-1) ?? 0)) / Math.max(EPSILON, bbSeries.middle.at(-1) ?? 0);
407
+ const prev5 =
408
+ ((bbSeries.upper.at(-6) ?? 0) - (bbSeries.lower.at(-6) ?? 0)) / Math.max(EPSILON, bbSeries.middle.at(-6) ?? 0);
409
+ if (!Number.isFinite(cur) || !Number.isFinite(prev5)) return null;
410
+ return cur > prev5 ? '拡大中' : cur < prev5 ? '収縮中' : '不変';
411
+ } catch {
412
+ return null;
413
+ }
414
+ }
415
+
416
+ function calcSigmaHistory(
417
+ candles: Array<{ close?: number }>,
418
+ bbSeries: { upper: number[] | null; middle: number[] | null },
419
+ ): Array<{ off: number; z: number } | null> | null {
420
+ try {
421
+ if (!bbSeries.upper || !bbSeries.middle) return null;
422
+ const L = Math.min(candles.length, bbSeries.upper.length, bbSeries.middle.length);
423
+ if (L < 6) return null;
424
+ const { upper, middle } = bbSeries;
425
+ const idxs = [-6, -1];
426
+ const vals = idxs.map((off) => {
427
+ const c = Number(candles.at(off)?.close ?? NaN);
428
+ const m = Number(middle.at(off) ?? NaN);
429
+ const u = Number(upper.at(off) ?? NaN);
430
+ if (![c, m, u].every(Number.isFinite)) return null;
431
+ const z = Number(((2 * (c - m)) / Math.max(EPSILON, u - m)).toFixed(2));
432
+ return { off, z };
433
+ });
434
+ return vals;
435
+ } catch {
436
+ return null;
437
+ }
438
+ }
439
+
440
+ function calcChikouBull(candles: Array<{ close?: number }>, close: number | null): boolean | null {
441
+ const CHIKOU_LOOKBACK = ICHIMOKU_SHIFT + 1;
442
+ if (candles.length < CHIKOU_LOOKBACK || close == null) return null;
443
+ const past = Number(candles.at(-CHIKOU_LOOKBACK)?.close ?? NaN);
444
+ if (!Number.isFinite(past)) return null;
445
+ return Number(close) > past;
446
+ }
447
+
448
+ function calcThreeSignals(
449
+ cloudPos: string,
450
+ tenkan: number | null,
451
+ kijun: number | null,
452
+ chikouBull: boolean | null,
453
+ ): { judge: string; aboveCloud: boolean; convAboveBase: boolean | null; chikouAbove: boolean | null } {
454
+ const aboveCloud = cloudPos === 'above_cloud';
455
+ const convAboveBase = tenkan != null && kijun != null ? Number(tenkan) >= Number(kijun) : null;
456
+ const chikouAbove = chikouBull;
457
+ let judge: '三役好転' | '三役逆転' | '混在' = '混在';
458
+ if (aboveCloud && convAboveBase === true && chikouAbove === true) judge = '三役好転';
459
+ if (cloudPos === 'below_cloud' && convAboveBase === false && chikouAbove === false) judge = '三役逆転';
460
+ return { judge, aboveCloud, convAboveBase, chikouAbove };
461
+ }
462
+
463
+ function calcCloudDistance(
464
+ close: number | null,
465
+ cloudTop: number | null,
466
+ cloudBot: number | null,
467
+ cloudPos: string,
468
+ ): number | null {
469
+ if (close == null || cloudTop == null || cloudBot == null) return null;
470
+ if (cloudPos === 'below_cloud') {
471
+ const need = cloudBot - Number(close);
472
+ return need > 0 ? (need / Math.max(EPSILON, Number(close))) * 100 : 0;
473
+ }
474
+ if (cloudPos === 'above_cloud') {
475
+ const need = Number(close) - cloudTop;
476
+ return need > 0 ? (need / Math.max(EPSILON, Number(close))) * 100 : 0;
477
+ }
478
+ return 0;
479
+ }
480
+
481
+ function findSmaCross(s25: number[] | null, s75: number[] | null): string | null {
482
+ if (!s25 || !s75) return null;
483
+ const L = Math.min(s25.length, s75.length);
484
+ let lastIdx: number | null = null;
485
+ let t: 'golden' | 'dead' | null = null;
486
+ for (let i = L - 2; i >= 0; i--) {
487
+ const d0 = Number(s25[i]) - Number(s75[i]);
488
+ const d1 = Number(s25[i + 1]) - Number(s75[i + 1]);
489
+ if (![d0, d1].every(Number.isFinite)) continue;
490
+ if ((d0 < 0 && d1 > 0) || (d0 > 0 && d1 < 0)) {
491
+ lastIdx = i + 1;
492
+ t = d1 > 0 ? 'golden' : 'dead';
493
+ break;
494
+ }
495
+ }
496
+ if (lastIdx == null) return '直近クロス: なし';
497
+ return `直近クロス: ${t === 'golden' ? 'ゴールデン' : 'デッド'}(${L - 1 - lastIdx}本前)`;
498
+ }
499
+
500
+ export const toolDef: ToolDefinition = {
501
+ name: 'analyze_indicators',
502
+ description:
503
+ '[Technical Indicators / RSI / MACD / SMA] テクニカル指標の総合分析。最新値・トレンド判定・シグナルをテキストで返す。十分な limit を指定(例: 日足200本)。\n\n描画 → prepare_chart_data / render_chart_svg。バックテスト → run_backtest。',
504
+ inputSchema: GetIndicatorsInputSchema,
505
+ handler: async ({ pair, type, limit }: z.infer<typeof GetIndicatorsInputSchema>) => {
506
+ const res = await analyzeIndicators(pair, type, limit);
507
+ if (!res.ok) return res;
508
+ const ind = (res?.data?.indicators ?? {}) as Record<string, number | null | undefined> & {
509
+ series?: Record<string, number[]>;
510
+ OBV_trend?: string | null;
511
+ };
512
+ const candles = (Array.isArray(res?.data?.normalized) ? res.data.normalized : []) as Array<{
513
+ close?: number;
514
+ [k: string]: unknown;
515
+ }>;
516
+ const close = candles.at(-1)?.close ?? null;
517
+ const prev = candles.at(-2)?.close ?? null;
518
+ const nowJst = toDisplayTime(undefined) ?? nowIso();
519
+ const deltaPrev = calcDeltaPrev(close, prev);
520
+ const deltaLabel = calcDeltaLabel(type);
521
+ const rsi = ind.RSI_14 ?? null;
522
+ const rsiSeries = Array.isArray(res?.data?.indicators?.RSI_14_series) ? res.data.indicators.RSI_14_series : null;
523
+ const recentRsiRaw = extractRecentRsi(rsiSeries, 7);
524
+ const recentRsiFormatted = recentRsiRaw.map((v) => (v == null ? 'n/a' : Number(v).toFixed(1)));
525
+ const rsiUnitLabel = calcRsiUnitLabel(type);
526
+ const sma25 = ind.SMA_25 ?? null;
527
+ const sma75 = ind.SMA_75 ?? null;
528
+ const sma200 = ind.SMA_200 ?? null;
529
+ const bbMid = ind.BB_middle ?? ind.BB2_middle ?? null;
530
+ const bbUp = ind.BB_upper ?? ind.BB2_upper ?? null;
531
+ const bbLo = ind.BB_lower ?? ind.BB2_lower ?? null;
532
+ const sigmaZ =
533
+ close != null && bbMid != null && bbUp != null && bbUp - bbMid !== 0
534
+ ? Number(((2 * (close - bbMid)) / (bbUp - bbMid)).toFixed(2))
535
+ : null;
536
+ const bandWidthPct =
537
+ bbUp != null && bbLo != null && bbMid ? Number((((bbUp - bbLo) / bbMid) * 100).toFixed(2)) : null;
538
+ const macdHist = ind.MACD_hist ?? null;
539
+ const spanA = ind.ICHIMOKU_spanA ?? null;
540
+ const spanB = ind.ICHIMOKU_spanB ?? null;
541
+ const tenkan = ind.ICHIMOKU_conversion ?? null;
542
+ const kijun = ind.ICHIMOKU_base ?? null;
543
+ const cloudTop = spanA != null && spanB != null ? Math.max(spanA, spanB) : null;
544
+ const cloudBot = spanA != null && spanB != null ? Math.min(spanA, spanB) : null;
545
+ const cloudPos =
546
+ close != null && cloudTop != null && cloudBot != null
547
+ ? close > cloudTop
548
+ ? 'above_cloud'
549
+ : close < cloudBot
550
+ ? 'below_cloud'
551
+ : 'in_cloud'
552
+ : 'unknown';
553
+ const trend = res?.data?.trend ?? 'unknown';
554
+ // Helpers: slope and last cross
555
+ const slopeOf = (seriesKey: string, n = 5): number | null => {
556
+ const arr = Array.isArray(ind?.series?.[seriesKey]) ? ind.series[seriesKey] : null;
557
+ if (!arr || arr.length < 2) return null;
558
+ const len = Math.min(n, arr.length);
559
+ const a = Number(arr.at(-len) ?? NaN);
560
+ const b = Number(arr.at(-1) ?? NaN);
561
+ if (!Number.isFinite(a) || !Number.isFinite(b) || len <= 1) return null;
562
+ return (b - a) / (len - 1);
563
+ };
564
+ const lastMacdCross = findLastMacdCross(
565
+ Array.isArray(ind?.series?.MACD_line) ? ind.series.MACD_line : null,
566
+ Array.isArray(ind?.series?.MACD_signal) ? ind.series.MACD_signal : null,
567
+ );
568
+ const divergence = detectDivergence(
569
+ candles,
570
+ Array.isArray(ind?.series?.MACD_hist) ? ind.series.MACD_hist : null,
571
+ 14,
572
+ );
573
+ // SMA arrangement and deviations
574
+ const curNum = Number(close ?? NaN);
575
+ const s25n = Number(sma25 ?? NaN),
576
+ s75n = Number(sma75 ?? NaN),
577
+ s200n = Number(sma200 ?? NaN);
578
+ const arrangement = calcSmaArrangement(curNum, s25n, s75n, s200n);
579
+ const s25Slope = slopeOf('SMA_25', 5),
580
+ s75Slope = slopeOf('SMA_75', 5),
581
+ s200Slope = slopeOf('SMA_200', 7);
582
+ // BB width trend and sigma history (last 5-7 bars)
583
+ const bbSeries = {
584
+ upper: Array.isArray(ind?.series?.BB_upper)
585
+ ? ind.series.BB_upper
586
+ : Array.isArray(ind?.series?.BB2_upper)
587
+ ? ind.series.BB2_upper
588
+ : null,
589
+ lower: Array.isArray(ind?.series?.BB_lower)
590
+ ? ind.series.BB_lower
591
+ : Array.isArray(ind?.series?.BB2_lower)
592
+ ? ind.series.BB2_lower
593
+ : null,
594
+ middle: Array.isArray(ind?.series?.BB_middle)
595
+ ? ind.series.BB_middle
596
+ : Array.isArray(ind?.series?.BB2_middle)
597
+ ? ind.series.BB2_middle
598
+ : null,
599
+ };
600
+ const bwTrend = calcBandWidthTrend(bbSeries);
601
+ const sigmaHistory = calcSigmaHistory(candles, bbSeries);
602
+ // Ichimoku extras: cloud thickness, chikou proxy, three signals, distance to cloud
603
+ const cloudThickness = cloudTop != null && cloudBot != null ? cloudTop - cloudBot : null;
604
+ const cloudThicknessPct =
605
+ cloudThickness != null && close != null && Number.isFinite(close)
606
+ ? (cloudThickness / Math.max(EPSILON, Number(close))) * 100
607
+ : null;
608
+ const chikouBull = calcChikouBull(candles, close);
609
+ const threeSignals = calcThreeSignals(cloudPos, tenkan, kijun, chikouBull);
610
+ const toCloudDistance = calcCloudDistance(close, cloudTop, cloudBot, cloudPos);
611
+ // Simple cross info (SMA 25/75)
612
+ const crossInfo = findSmaCross(
613
+ Array.isArray(ind?.series?.SMA_25) ? ind.series.SMA_25 : null,
614
+ Array.isArray(ind?.series?.SMA_75) ? ind.series.SMA_75 : null,
615
+ );
616
+ // Stochastic RSI and OBV values
617
+ const stochK = ind.STOCH_RSI_K ?? null;
618
+ const stochD = ind.STOCH_RSI_D ?? null;
619
+ const stochPrevK = ind.STOCH_RSI_prevK ?? null;
620
+ const stochPrevD = ind.STOCH_RSI_prevD ?? null;
621
+ const obvVal = ind.OBV ?? null;
622
+ const obvSma20 = ind.OBV_SMA20 ?? null;
623
+ const obvTrend = ind.OBV_trend ?? null;
624
+ const obvPrev = ind.OBV_prevObv ?? null;
625
+
626
+ const text = buildIndicatorsText({
627
+ pair,
628
+ type,
629
+ nowJst,
630
+ close,
631
+ prev,
632
+ deltaPrev,
633
+ deltaLabel,
634
+ trend,
635
+ rsi,
636
+ recentRsiFormatted,
637
+ rsiUnitLabel,
638
+ macdHist,
639
+ lastMacdCross,
640
+ divergence,
641
+ sma25,
642
+ sma75,
643
+ sma200,
644
+ s25Slope,
645
+ s75Slope,
646
+ s200Slope,
647
+ arrangement,
648
+ crossInfo,
649
+ bbMid,
650
+ bbUp,
651
+ bbLo,
652
+ sigmaZ,
653
+ bandWidthPct,
654
+ bwTrend,
655
+ sigmaHistory,
656
+ tenkan,
657
+ kijun,
658
+ spanA,
659
+ spanB,
660
+ cloudTop,
661
+ cloudBot,
662
+ cloudPos,
663
+ cloudThickness,
664
+ cloudThicknessPct,
665
+ chikouBull,
666
+ threeSignals,
667
+ toCloudDistance,
668
+ ichimokuConvSlope: slopeOf('ICHIMOKU_conversion', 5),
669
+ ichimokuBaseSlope: slopeOf('ICHIMOKU_base', 5),
670
+ stochK,
671
+ stochD,
672
+ stochPrevK,
673
+ stochPrevD,
674
+ obvVal,
675
+ obvSma20,
676
+ obvTrend,
677
+ obvPrev,
678
+ obvUnit: String(pair).toLowerCase().includes('btc') ? 'BTC' : '',
679
+ });
680
+ return { content: [{ type: 'text', text }], structuredContent: toStructured(res) };
681
+ },
682
+ };