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,691 @@
1
+ import { TtlCache } from '../lib/cache.js';
2
+ import { formatSummary } from '../lib/formatter.js';
3
+ import { getFetchCount } from '../lib/indicator_buffer.js';
4
+ import {
5
+ BB_PERIOD,
6
+ BB_STDDEV,
7
+ ICHIMOKU_SHIFT,
8
+ INDICATOR_CACHE_MAX_ENTRIES,
9
+ INDICATOR_CACHE_TTL_MS,
10
+ MACD_FAST,
11
+ MACD_SIGNAL,
12
+ MACD_SLOW,
13
+ OBV_SMA_PERIOD,
14
+ OBV_TREND_THRESHOLD,
15
+ RSI_OVERBOUGHT,
16
+ RSI_OVERSOLD,
17
+ RSI_PERIOD,
18
+ SMA_DEFAULT_PERIOD,
19
+ STOCH_PERIOD,
20
+ STOCH_SMOOTH_D,
21
+ STOCH_SMOOTH_K,
22
+ } from '../lib/indicator-config.js';
23
+ import {
24
+ ichimokuSnapshot,
25
+ bollingerBands as rawBollingerBands,
26
+ ema as rawEma,
27
+ ichimokuSeries as rawIchimokuSeries,
28
+ macd as rawMacd,
29
+ obv as rawObv,
30
+ rsi as rawRsi,
31
+ shiftChikou as rawShiftChikou,
32
+ sma as rawSma,
33
+ stochastic as rawStochastic,
34
+ stochRSI as rawStochRSI,
35
+ toNumericSeries,
36
+ } from '../lib/indicators.js';
37
+ import { fail, failFromValidation, ok, parseAsResult } from '../lib/result.js';
38
+ import { createMeta, ensurePair } from '../lib/validate.js';
39
+ import type {
40
+ Candle,
41
+ CandleType,
42
+ FailResult,
43
+ GetIndicatorsData,
44
+ GetIndicatorsMeta,
45
+ NumericSeries,
46
+ OkResult,
47
+ TrendLabel,
48
+ } from '../src/schemas.js';
49
+ import { GetIndicatorsDataSchema, GetIndicatorsMetaSchema, GetIndicatorsOutputSchema } from '../src/schemas.js';
50
+ import getCandles from './get_candles.js';
51
+
52
+ // --- Result cache for analyzeIndicators ---
53
+ // Same pair/type within TTL → skip redundant API call & computation.
54
+ // Especially effective when snapshot tools (BB/SMA/Ichimoku) are called
55
+ // sequentially for the same pair.
56
+
57
+ interface IndicatorCacheComputed {
58
+ normalized: Candle[];
59
+ raw: unknown;
60
+ indicators: GetIndicatorsData['indicators'];
61
+ allCloses: number[];
62
+ rsi14_series: NumericSeries;
63
+ sma_25_series: NumericSeries;
64
+ sma_75_series: NumericSeries;
65
+ bb2: { upper: NumericSeries; middle: NumericSeries; lower: NumericSeries };
66
+ warnings: string[];
67
+ trend: TrendLabel;
68
+ fetchCount: number;
69
+ }
70
+
71
+ const indicatorCache = new TtlCache<IndicatorCacheComputed>({
72
+ ttlMs: INDICATOR_CACHE_TTL_MS,
73
+ maxEntries: INDICATOR_CACHE_MAX_ENTRIES,
74
+ });
75
+
76
+ /** Clear the indicator cache (useful for testing). */
77
+ export function clearIndicatorCache(): void {
78
+ indicatorCache.clear();
79
+ }
80
+
81
+ // --- Indicators (delegates to lib/indicators.ts) ---
82
+
83
+ export function sma(values: number[], period: number = SMA_DEFAULT_PERIOD): NumericSeries {
84
+ return toNumericSeries(rawSma(values, period), 2);
85
+ }
86
+
87
+ export function rsi(values: number[], period: number = RSI_PERIOD): NumericSeries {
88
+ return toNumericSeries(rawRsi(values, period), 2);
89
+ }
90
+
91
+ export function bollingerBands(
92
+ values: number[],
93
+ period: number = BB_PERIOD,
94
+ stdDev: number = BB_STDDEV,
95
+ ): { upper: NumericSeries; middle: NumericSeries; lower: NumericSeries } {
96
+ const raw = rawBollingerBands(values, period, stdDev);
97
+ return {
98
+ upper: toNumericSeries(raw.upper, 2),
99
+ middle: toNumericSeries(raw.middle, 2),
100
+ lower: toNumericSeries(raw.lower, 2),
101
+ };
102
+ }
103
+
104
+ // Exponential Moving Average
105
+ export function ema(values: number[], period: number): NumericSeries {
106
+ if (period <= 1) return values.map((v) => (v != null ? Number(v.toFixed(2)) : null));
107
+ return toNumericSeries(rawEma(values, period), 2);
108
+ }
109
+
110
+ export function macd(
111
+ values: number[],
112
+ fast = MACD_FAST,
113
+ slow = MACD_SLOW,
114
+ signal = MACD_SIGNAL,
115
+ ): { line: NumericSeries; signal: NumericSeries; hist: NumericSeries } {
116
+ const raw = rawMacd(values, fast, slow, signal);
117
+ return {
118
+ line: toNumericSeries(raw.line, 2),
119
+ signal: toNumericSeries(raw.signal, 2),
120
+ hist: toNumericSeries(raw.hist, 2),
121
+ };
122
+ }
123
+
124
+ export function ichimokuSeries(
125
+ highs: number[],
126
+ lows: number[],
127
+ closes: number[],
128
+ ): { tenkan: NumericSeries; kijun: NumericSeries; spanA: NumericSeries; spanB: NumericSeries; chikou: NumericSeries } {
129
+ const raw = rawIchimokuSeries(highs, lows, closes);
130
+ return {
131
+ tenkan: toNumericSeries(raw.tenkan, 2),
132
+ kijun: toNumericSeries(raw.kijun, 2),
133
+ spanA: toNumericSeries(raw.spanA, 2),
134
+ spanB: toNumericSeries(raw.spanB, 2),
135
+ chikou: toNumericSeries(raw.chikou, 2),
136
+ };
137
+ }
138
+
139
+ /**
140
+ * Stochastic RSI: RSI値にストキャスティクス計算を適用。
141
+ */
142
+ export function computeStochRSI(
143
+ closes: number[],
144
+ rsiPeriod = RSI_PERIOD,
145
+ stochPeriod = STOCH_PERIOD,
146
+ smoothK = STOCH_SMOOTH_K,
147
+ smoothD = STOCH_SMOOTH_D,
148
+ ): { k: number | null; d: number | null; prevK: number | null; prevD: number | null } {
149
+ const raw = rawStochRSI(closes, rsiPeriod, stochPeriod, smoothK, smoothD);
150
+ const kNs = toNumericSeries(raw.kSeries, 2);
151
+ const dNs = toNumericSeries(raw.dSeries, 2);
152
+ return {
153
+ k: kNs.at(-1) ?? null,
154
+ d: dNs.at(-1) ?? null,
155
+ prevK: kNs.at(-2) ?? null,
156
+ prevD: dNs.at(-2) ?? null,
157
+ };
158
+ }
159
+
160
+ /**
161
+ * Classic Stochastic Oscillator: 価格のレンジ内位置を測定。
162
+ */
163
+ export function computeClassicStochastic(
164
+ highs: number[],
165
+ lows: number[],
166
+ closes: number[],
167
+ kPeriod = STOCH_PERIOD,
168
+ smoothK = STOCH_SMOOTH_K,
169
+ smoothD = STOCH_SMOOTH_D,
170
+ ): {
171
+ kSeries: (number | null)[];
172
+ dSeries: (number | null)[];
173
+ k: number | null;
174
+ d: number | null;
175
+ prevK: number | null;
176
+ prevD: number | null;
177
+ } {
178
+ const raw = rawStochastic(highs, lows, closes, kPeriod, smoothK, smoothD);
179
+ const kSeries = toNumericSeries(raw.kSeries, 2);
180
+ const dSeries = toNumericSeries(raw.dSeries, 2);
181
+ return {
182
+ kSeries,
183
+ dSeries,
184
+ k: kSeries.at(-1) ?? null,
185
+ d: dSeries.at(-1) ?? null,
186
+ prevK: kSeries.at(-2) ?? null,
187
+ prevD: dSeries.at(-2) ?? null,
188
+ };
189
+ }
190
+
191
+ /**
192
+ * OBV (On-Balance Volume): 出来高を価格方向に応じて累積加算/減算。
193
+ */
194
+ export function computeOBV(
195
+ candles: Candle[],
196
+ smaPeriod = OBV_SMA_PERIOD,
197
+ ): { obv: number | null; obvSma: number | null; prevObv: number | null; trend: 'rising' | 'falling' | 'flat' | null } {
198
+ if (candles.length < 2) return { obv: null, obvSma: null, prevObv: null, trend: null };
199
+
200
+ const closes = candles.map((c) => c.close);
201
+ const volumes = candles.map((c) => c.volume ?? 0);
202
+ const obvSeries = rawObv(closes, volumes);
203
+
204
+ const obvVal = obvSeries.at(-1) ?? null;
205
+ const prevObv = obvSeries.at(-2) ?? null;
206
+
207
+ // SMA of OBV
208
+ let obvSma: number | null = null;
209
+ if (obvSeries.length >= smaPeriod) {
210
+ const slice = obvSeries.slice(-smaPeriod);
211
+ obvSma = Number((slice.reduce((a, b) => a + b, 0) / smaPeriod).toFixed(2));
212
+ }
213
+
214
+ // Trend: compare OBV to its SMA
215
+ let trend: 'rising' | 'falling' | 'flat' | null = null;
216
+ if (obvVal != null && obvSma != null) {
217
+ const diff = obvVal - obvSma;
218
+ const threshold = Math.abs(obvSma) * OBV_TREND_THRESHOLD;
219
+ if (diff > threshold) trend = 'rising';
220
+ else if (diff < -threshold) trend = 'falling';
221
+ else trend = 'flat';
222
+ }
223
+
224
+ return { obv: obvVal, obvSma, prevObv, trend };
225
+ }
226
+
227
+ function ichimoku(
228
+ highs: number[],
229
+ lows: number[],
230
+ closes: number[],
231
+ ): { conversion: number; base: number; spanA: number; spanB: number } | null {
232
+ const snap = ichimokuSnapshot(highs, lows, closes);
233
+ if (!snap) return null;
234
+ return {
235
+ conversion: Number(snap.conversion.toFixed(2)),
236
+ base: Number(snap.base.toFixed(2)),
237
+ spanA: Number(snap.spanA.toFixed(2)),
238
+ spanB: Number(snap.spanB.toFixed(2)),
239
+ };
240
+ }
241
+
242
+ function createChartData(
243
+ normalized: Candle[],
244
+ indicators: GetIndicatorsData['indicators'],
245
+ limit: number = 50,
246
+ ): GetIndicatorsData['chart'] {
247
+ const fullLength = normalized.length;
248
+ const recent = normalized.slice(-limit);
249
+ const pastBuffer = fullLength - recent.length;
250
+ const shift = ICHIMOKU_SHIFT;
251
+
252
+ return {
253
+ candles: normalized,
254
+ indicators: {
255
+ SMA_5: indicators.sma_5_series ?? [],
256
+ SMA_20: indicators.sma_20_series ?? [],
257
+ SMA_25: indicators.sma_25_series ?? [],
258
+ SMA_50: indicators.sma_50_series ?? [],
259
+ SMA_75: indicators.sma_75_series ?? [],
260
+ SMA_200: indicators.sma_200_series ?? [],
261
+ EMA_12: indicators.ema_12_series ?? [],
262
+ EMA_26: indicators.ema_26_series ?? [],
263
+ EMA_50: indicators.ema_50_series ?? [],
264
+ EMA_200: indicators.ema_200_series ?? [],
265
+ RSI_14: indicators.RSI_14,
266
+ BB1_upper: indicators.bb1_series?.upper ?? [],
267
+ BB1_middle: indicators.bb1_series?.middle ?? [],
268
+ BB1_lower: indicators.bb1_series?.lower ?? [],
269
+ BB2_upper: indicators.bb2_series?.upper ?? [],
270
+ BB2_middle: indicators.bb2_series?.middle ?? [],
271
+ BB2_lower: indicators.bb2_series?.lower ?? [],
272
+ BB3_upper: indicators.bb3_series?.upper ?? [],
273
+ BB3_middle: indicators.bb3_series?.middle ?? [],
274
+ BB3_lower: indicators.bb3_series?.lower ?? [],
275
+ BB_upper: indicators.bb2_series?.upper ?? [],
276
+ BB_middle: indicators.bb2_series?.middle ?? [],
277
+ BB_lower: indicators.bb2_series?.lower ?? [],
278
+ ICHI_tenkan: indicators.ichi_series?.tenkan ?? [],
279
+ ICHI_kijun: indicators.ichi_series?.kijun ?? [],
280
+ ICHI_spanA: indicators.ichi_series?.spanA ?? [],
281
+ ICHI_spanB: indicators.ichi_series?.spanB ?? [],
282
+ ICHI_chikou: indicators.ichi_series?.chikou
283
+ ? toNumericSeries(
284
+ rawShiftChikou(
285
+ indicators.ichi_series.chikou.map((v) => v ?? NaN),
286
+ shift,
287
+ ),
288
+ 2,
289
+ )
290
+ : [],
291
+ macd_series: indicators.macd_series,
292
+ RSI_14_series: indicators.RSI_14_series ?? [],
293
+ stoch_k_series: indicators.stoch_k_series ?? [],
294
+ stoch_d_series: indicators.stoch_d_series ?? [],
295
+ },
296
+ meta: { pastBuffer, shift },
297
+ stats: {
298
+ min: Math.min(...recent.map((c) => c.low)),
299
+ max: Math.max(...recent.map((c) => c.high)),
300
+ avg: recent.reduce((sum, c) => sum + c.close, 0) / Math.max(1, recent.length),
301
+ volume_avg: recent.reduce((sum, c) => sum + (c.volume ?? 0), 0) / Math.max(1, recent.length),
302
+ },
303
+ };
304
+ }
305
+
306
+ function computeAllIndicators(normalized: Candle[]): GetIndicatorsData['indicators'] {
307
+ const allHighs = normalized.map((c) => c.high);
308
+ const allLows = normalized.map((c) => c.low);
309
+ const allCloses = normalized.map((c) => c.close);
310
+
311
+ const rsi14_series = rsi(allCloses, RSI_PERIOD);
312
+ const macdSeries = macd(allCloses, MACD_FAST, MACD_SLOW, MACD_SIGNAL);
313
+ const bb1 = bollingerBands(allCloses, BB_PERIOD, 1);
314
+ const bb2Val = bollingerBands(allCloses, BB_PERIOD, BB_STDDEV);
315
+ const bb3 = bollingerBands(allCloses, BB_PERIOD, 3);
316
+ const ichi = ichimokuSeries(allHighs, allLows, allCloses);
317
+ const sma_5_series = sma(allCloses, 5);
318
+ const sma_20_series = sma(allCloses, 20);
319
+ const sma_25_series = sma(allCloses, 25);
320
+ const sma_50_series = sma(allCloses, 50);
321
+ const sma_75_series = sma(allCloses, 75);
322
+ const sma_200_series = sma(allCloses, 200);
323
+ const ema_12_series = ema(allCloses, 12);
324
+ const ema_26_series = ema(allCloses, 26);
325
+ const ema_50_series = ema(allCloses, 50);
326
+ const ema_200_series = ema(allCloses, 200);
327
+
328
+ const ichiSimple = ichimoku(allHighs, allLows, allCloses);
329
+ const stoch = computeClassicStochastic(allHighs, allLows, allCloses, STOCH_PERIOD, STOCH_SMOOTH_K, STOCH_SMOOTH_D);
330
+ const stochRsi = computeStochRSI(allCloses, RSI_PERIOD, STOCH_PERIOD, STOCH_SMOOTH_K, STOCH_SMOOTH_D);
331
+ const obvResult = computeOBV(normalized, OBV_SMA_PERIOD);
332
+
333
+ return {
334
+ SMA_5: sma_5_series.at(-1),
335
+ SMA_20: sma_20_series.at(-1),
336
+ SMA_25: sma_25_series.at(-1),
337
+ SMA_50: sma_50_series.at(-1),
338
+ SMA_75: sma_75_series.at(-1),
339
+ SMA_200: sma_200_series.at(-1),
340
+ RSI_14: rsi14_series.at(-1),
341
+ RSI_14_series: rsi14_series,
342
+ BB_upper: bb2Val.upper.at(-1),
343
+ BB_middle: bb2Val.middle.at(-1),
344
+ BB_lower: bb2Val.lower.at(-1),
345
+ BB1_upper: bb1.upper.at(-1),
346
+ BB1_middle: bb1.middle.at(-1),
347
+ BB1_lower: bb1.lower.at(-1),
348
+ BB2_upper: bb2Val.upper.at(-1),
349
+ BB2_middle: bb2Val.middle.at(-1),
350
+ BB2_lower: bb2Val.lower.at(-1),
351
+ BB3_upper: bb3.upper.at(-1),
352
+ BB3_middle: bb3.middle.at(-1),
353
+ BB3_lower: bb3.lower.at(-1),
354
+ bb1_series: bb1,
355
+ bb2_series: bb2Val,
356
+ bb3_series: bb3,
357
+ ichi_series: ichi,
358
+ macd_series: macdSeries,
359
+ sma_5_series,
360
+ sma_20_series,
361
+ sma_25_series,
362
+ sma_50_series,
363
+ sma_75_series,
364
+ sma_200_series,
365
+ EMA_12: ema_12_series.at(-1),
366
+ EMA_26: ema_26_series.at(-1),
367
+ EMA_50: ema_50_series.at(-1),
368
+ EMA_200: ema_200_series.at(-1),
369
+ ema_12_series,
370
+ ema_26_series,
371
+ ema_50_series,
372
+ ema_200_series,
373
+ MACD_line: macdSeries.line.at(-1),
374
+ MACD_signal: macdSeries.signal.at(-1),
375
+ MACD_hist: macdSeries.hist.at(-1),
376
+ ...(ichiSimple
377
+ ? {
378
+ ICHIMOKU_conversion: ichiSimple.conversion,
379
+ ICHIMOKU_base: ichiSimple.base,
380
+ ICHIMOKU_spanA: ichiSimple.spanA,
381
+ ICHIMOKU_spanB: ichiSimple.spanB,
382
+ }
383
+ : {}),
384
+ STOCH_K: stoch.k,
385
+ STOCH_D: stoch.d,
386
+ STOCH_prevK: stoch.prevK,
387
+ STOCH_prevD: stoch.prevD,
388
+ stoch_k_series: stoch.kSeries,
389
+ stoch_d_series: stoch.dSeries,
390
+ STOCH_RSI_K: stochRsi.k,
391
+ STOCH_RSI_D: stochRsi.d,
392
+ STOCH_RSI_prevK: stochRsi.prevK,
393
+ STOCH_RSI_prevD: stochRsi.prevD,
394
+ OBV: obvResult.obv,
395
+ OBV_SMA20: obvResult.obvSma,
396
+ OBV_prevObv: obvResult.prevObv,
397
+ OBV_trend: obvResult.trend,
398
+ };
399
+ }
400
+
401
+ function buildWarnings(dataLength: number, candleCount: number): string[] {
402
+ const warnings: string[] = [];
403
+ if (dataLength < 5) warnings.push('SMA_5: データ不足');
404
+ if (dataLength < 20) warnings.push('SMA_20: データ不足');
405
+ if (dataLength < 25) warnings.push('SMA_25: データ不足');
406
+ if (dataLength < 50) warnings.push('SMA_50: データ不足');
407
+ if (dataLength < 75) warnings.push('SMA_75: データ不足');
408
+ if (dataLength < 200) warnings.push('SMA_200: データ不足');
409
+ if (dataLength < 12) warnings.push('EMA_12: データ不足');
410
+ if (dataLength < 26) warnings.push('EMA_26: データ不足');
411
+ if (dataLength < 50) warnings.push('EMA_50: データ不足');
412
+ if (dataLength < 200) warnings.push('EMA_200: データ不足');
413
+ if (dataLength < 15) warnings.push('RSI_14: データ不足');
414
+ if (dataLength < 20) warnings.push('Bollinger_Bands: データ不足');
415
+ if (dataLength < 52) warnings.push('Ichimoku: データ不足');
416
+ if (dataLength < 20) warnings.push('Stochastic: データ不足');
417
+ if (dataLength < 34) warnings.push('StochRSI: データ不足');
418
+ if (candleCount < 2) warnings.push('OBV: データ不足');
419
+ return warnings;
420
+ }
421
+
422
+ function padSeriesLengths(chartIndicators: Record<string, unknown>, targetLength: number): void {
423
+ const keys = [
424
+ 'SMA_5',
425
+ 'SMA_20',
426
+ 'SMA_25',
427
+ 'SMA_50',
428
+ 'SMA_75',
429
+ 'SMA_200',
430
+ 'EMA_12',
431
+ 'EMA_26',
432
+ 'EMA_50',
433
+ 'EMA_200',
434
+ 'BB_upper',
435
+ 'BB_middle',
436
+ 'BB_lower',
437
+ 'BB1_upper',
438
+ 'BB1_middle',
439
+ 'BB1_lower',
440
+ 'BB2_upper',
441
+ 'BB2_middle',
442
+ 'BB2_lower',
443
+ 'BB3_upper',
444
+ 'BB3_middle',
445
+ 'BB3_lower',
446
+ 'ICHI_tenkan',
447
+ 'ICHI_kijun',
448
+ 'ICHI_spanA',
449
+ 'ICHI_spanB',
450
+ 'ICHI_chikou',
451
+ 'RSI_14_series',
452
+ 'stoch_k_series',
453
+ 'stoch_d_series',
454
+ ];
455
+ keys.forEach((k) => {
456
+ const arr = chartIndicators[k] as NumericSeries | undefined;
457
+ if (!Array.isArray(arr)) return;
458
+ if (arr.length === targetLength) return;
459
+ if (arr.length < targetLength) {
460
+ const pad = Array.from<null>({ length: targetLength - arr.length }).fill(null);
461
+ (chartIndicators[k] as NumericSeries) = [...arr, ...pad];
462
+ } else {
463
+ (chartIndicators[k] as NumericSeries) = arr.slice(-targetLength);
464
+ }
465
+ });
466
+ }
467
+
468
+ function buildIndicatorsSummaryText(opts: {
469
+ pair: string;
470
+ type: string;
471
+ indicators: GetIndicatorsData['indicators'];
472
+ allCloses: number[];
473
+ rsi14_series: NumericSeries;
474
+ sma_25_series: NumericSeries;
475
+ sma_75_series: NumericSeries;
476
+ bb2: { upper: NumericSeries; middle: NumericSeries; lower: NumericSeries };
477
+ trend: TrendLabel;
478
+ normalized: Candle[];
479
+ displayCount: number;
480
+ }): string {
481
+ const {
482
+ pair,
483
+ type,
484
+ indicators,
485
+ allCloses,
486
+ rsi14_series: rsi14_s,
487
+ sma_25_series: sma25_s,
488
+ sma_75_series: sma75_s,
489
+ bb2: bb2_s,
490
+ trend,
491
+ normalized,
492
+ displayCount,
493
+ } = opts;
494
+
495
+ const latestIndicators: Record<string, number | null | undefined> = {
496
+ SMA_25: indicators.SMA_25,
497
+ SMA_75: indicators.SMA_75,
498
+ SMA_200: indicators.SMA_200,
499
+ RSI_14: indicators.RSI_14,
500
+ MACD_line: indicators.MACD_line,
501
+ MACD_signal: indicators.MACD_signal,
502
+ MACD_hist: indicators.MACD_hist,
503
+ };
504
+ if (indicators.ICHIMOKU_conversion) {
505
+ latestIndicators.ICHIMOKU_conversion = indicators.ICHIMOKU_conversion;
506
+ latestIndicators.ICHIMOKU_base = indicators.ICHIMOKU_base;
507
+ latestIndicators.ICHIMOKU_spanA = indicators.ICHIMOKU_spanA;
508
+ latestIndicators.ICHIMOKU_spanB = indicators.ICHIMOKU_spanB;
509
+ }
510
+
511
+ const baseSummary = formatSummary({
512
+ pair,
513
+ timeframe: String(type),
514
+ latest: allCloses.at(-1) ?? undefined,
515
+ extra: `RSI=${latestIndicators.RSI_14} trend=${trend} (count=${allCloses.length})`,
516
+ });
517
+
518
+ const indLines: string[] = [];
519
+ for (const [k, v] of Object.entries(latestIndicators)) {
520
+ if (v != null) indLines.push(`${k}:${v}`);
521
+ }
522
+ if (indicators.ICHIMOKU_conversion != null) {
523
+ indLines.push(`ICHI_conv:${indicators.ICHIMOKU_conversion}`);
524
+ indLines.push(`ICHI_base:${indicators.ICHIMOKU_base}`);
525
+ indLines.push(`ICHI_spanA:${indicators.ICHIMOKU_spanA}`);
526
+ indLines.push(`ICHI_spanB:${indicators.ICHIMOKU_spanB}`);
527
+ }
528
+
529
+ const recentN = Math.min(displayCount, normalized.length);
530
+ const recentSlice = normalized.slice(-recentN);
531
+ const recentLines = recentSlice.map((c, i) => {
532
+ const idx = normalized.length - recentN + i;
533
+ const t = c.isoTime ? String(c.isoTime).replace(/\.000Z$/, 'Z') : '?';
534
+ const r = rsi14_s[idx] != null ? ` RSI:${rsi14_s[idx]}` : '';
535
+ const s25 = sma25_s[idx] != null ? ` S25:${sma25_s[idx]}` : '';
536
+ const s75 = sma75_s[idx] != null ? ` S75:${sma75_s[idx]}` : '';
537
+ const bbu = bb2_s.upper[idx] != null ? ` BBu:${bb2_s.upper[idx]}` : '';
538
+ const bbl = bb2_s.lower[idx] != null ? ` BBl:${bb2_s.lower[idx]}` : '';
539
+ return `[${idx}] ${t} C:${c.close}${r}${s25}${s75}${bbu}${bbl}`;
540
+ });
541
+
542
+ return (
543
+ baseSummary +
544
+ `\n\n📊 最新インジケーター値:\n` +
545
+ indLines.join(' | ') +
546
+ `\n\n📋 直近${recentN}本のデータ:\n` +
547
+ recentLines.join('\n') +
548
+ `\n\n---\n📌 含まれるもの: RSI・MACD・SMA・BB・一目均衡表の計算値と時系列、トレンド判定` +
549
+ `\n📌 含まれないもの: 板情報、出来高フロー(CVD・売買内訳)、大口動向、チャートパターン` +
550
+ `\n📌 補完ツール: get_flow_metrics(フロー・CVD), get_orderbook(板情報), detect_whale_events(大口), detect_patterns(パターン)`
551
+ );
552
+ }
553
+
554
+ function analyzeTrend(
555
+ indicators: GetIndicatorsData['indicators'],
556
+ currentPrice: number | null | undefined,
557
+ ): TrendLabel {
558
+ if (!indicators.SMA_25 || !indicators.SMA_75 || currentPrice == null) return 'insufficient_data';
559
+
560
+ const sma25 = indicators.SMA_25 as number | null;
561
+ const sma75 = indicators.SMA_75 as number | null;
562
+ const sma200 = indicators.SMA_200 as number | null;
563
+ const rsi = indicators.RSI_14 as number | null;
564
+
565
+ if (
566
+ currentPrice > (sma25 ?? Number.POSITIVE_INFINITY) &&
567
+ (sma25 ?? Number.POSITIVE_INFINITY) > (sma75 ?? Number.NEGATIVE_INFINITY)
568
+ ) {
569
+ if (sma200 && currentPrice > sma200) return 'strong_uptrend';
570
+ return 'uptrend';
571
+ }
572
+
573
+ if (
574
+ currentPrice < (sma25 ?? Number.NEGATIVE_INFINITY) &&
575
+ (sma25 ?? Number.NEGATIVE_INFINITY) < (sma75 ?? Number.POSITIVE_INFINITY)
576
+ ) {
577
+ if (sma200 && currentPrice < sma200) return 'strong_downtrend';
578
+ return 'downtrend';
579
+ }
580
+
581
+ if (rsi != null && rsi >= RSI_OVERBOUGHT) return 'overbought';
582
+ if (rsi != null && rsi <= RSI_OVERSOLD) return 'oversold';
583
+ return 'sideways';
584
+ }
585
+
586
+ export default async function analyzeIndicators(
587
+ pair: string = 'btc_jpy',
588
+ type: CandleType | string = '1day',
589
+ limit: number | null = null,
590
+ ): Promise<OkResult<GetIndicatorsData, GetIndicatorsMeta> | FailResult> {
591
+ const chk = ensurePair(pair);
592
+ if (!chk.ok) return failFromValidation(chk);
593
+
594
+ const displayCount = limit || 60;
595
+
596
+ const indicatorKeys = [
597
+ 'SMA_5',
598
+ 'SMA_20',
599
+ 'SMA_25',
600
+ 'SMA_50',
601
+ 'SMA_75',
602
+ 'SMA_200',
603
+ 'EMA_12',
604
+ 'EMA_26',
605
+ 'EMA_50',
606
+ 'EMA_200',
607
+ 'RSI_14',
608
+ 'BB_20',
609
+ 'STOCH',
610
+ 'ICHIMOKU',
611
+ ] as const;
612
+ const fetchCount = getFetchCount(displayCount, indicatorKeys);
613
+
614
+ // Check cache before fetching & computing
615
+ const cacheKey = `${chk.pair}:${type}`;
616
+ const cached = indicatorCache.get(cacheKey);
617
+ let computed: IndicatorCacheComputed;
618
+
619
+ if (cached && cached.fetchCount >= fetchCount) {
620
+ computed = cached;
621
+ } else {
622
+ const candlesResult = await getCandles(chk.pair, type, undefined, fetchCount);
623
+ if (!candlesResult.ok) return fail(candlesResult.summary.replace(/^Error: /, ''), candlesResult.meta.errorType);
624
+
625
+ const normalized = candlesResult.data.normalized;
626
+ const allCloses = normalized.map((c) => c.close);
627
+
628
+ const indicators = computeAllIndicators(normalized);
629
+ const warnings = buildWarnings(allCloses.length, normalized.length);
630
+ const trend = analyzeTrend(indicators, allCloses.at(-1));
631
+
632
+ computed = {
633
+ normalized,
634
+ raw: candlesResult.data.raw,
635
+ indicators,
636
+ allCloses,
637
+ rsi14_series: indicators.RSI_14_series ?? [],
638
+ sma_25_series: indicators.sma_25_series ?? [],
639
+ sma_75_series: indicators.sma_75_series ?? [],
640
+ bb2: indicators.bb2_series ?? { upper: [], middle: [], lower: [] },
641
+ warnings,
642
+ trend,
643
+ fetchCount,
644
+ };
645
+
646
+ indicatorCache.set(cacheKey, computed);
647
+ }
648
+
649
+ // --- Build result from computed data (always uses current displayCount/fetchCount) ---
650
+ const { normalized, indicators, allCloses, warnings, trend } = computed;
651
+
652
+ const chartData = createChartData(normalized, indicators, displayCount);
653
+ padSeriesLengths(chartData.indicators as Record<string, unknown>, chartData.candles.length);
654
+
655
+ const summary = buildIndicatorsSummaryText({
656
+ pair: chk.pair,
657
+ type: String(type),
658
+ indicators,
659
+ allCloses,
660
+ rsi14_series: computed.rsi14_series,
661
+ sma_25_series: computed.sma_25_series,
662
+ sma_75_series: computed.sma_75_series,
663
+ bb2: computed.bb2,
664
+ trend,
665
+ normalized,
666
+ displayCount,
667
+ });
668
+
669
+ const data: GetIndicatorsData = {
670
+ summary,
671
+ raw: computed.raw,
672
+ normalized,
673
+ indicators,
674
+ trend,
675
+ chart: chartData,
676
+ } satisfies GetIndicatorsData;
677
+
678
+ const meta = createMeta(chk.pair, {
679
+ type,
680
+ count: allCloses.length,
681
+ requiredCount: fetchCount,
682
+ warnings: warnings.length > 0 ? warnings : undefined,
683
+ });
684
+
685
+ const parsedData = GetIndicatorsDataSchema.parse(data);
686
+ const parsedMeta = GetIndicatorsMetaSchema.parse(meta);
687
+ return parseAsResult<GetIndicatorsData, GetIndicatorsMeta>(
688
+ GetIndicatorsOutputSchema,
689
+ ok(summary, parsedData, parsedMeta),
690
+ );
691
+ }