bitbank-lab-mcp 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +388 -0
- package/assets/lightweight-charts.standalone.js +7 -0
- package/bin/bitbank-lab-mcp.js +20 -0
- package/lib/cache.ts +70 -0
- package/lib/candle-utils.ts +48 -0
- package/lib/candle-validate.ts +434 -0
- package/lib/conversions.ts +25 -0
- package/lib/datetime.ts +157 -0
- package/lib/depth-analysis.ts +51 -0
- package/lib/error.ts +15 -0
- package/lib/formatter.ts +296 -0
- package/lib/get-depth.ts +111 -0
- package/lib/http.ts +132 -0
- package/lib/indicator-config.ts +39 -0
- package/lib/indicator_buffer.ts +41 -0
- package/lib/indicators.ts +579 -0
- package/lib/logger.ts +120 -0
- package/lib/ma-snapshot-utils.ts +277 -0
- package/lib/math.ts +89 -0
- package/lib/pattern-diagrams.ts +562 -0
- package/lib/result.ts +104 -0
- package/lib/validate.ts +154 -0
- package/lib/volatility.ts +132 -0
- package/package.json +79 -0
- package/src/env.ts +4 -0
- package/src/handlers/analyzeCandlePatternsHandler.ts +383 -0
- package/src/handlers/analyzeFibonacciHandler.ts +54 -0
- package/src/handlers/analyzeIndicatorsHandler.ts +682 -0
- package/src/handlers/analyzeMarketSignalHandler.ts +272 -0
- package/src/handlers/analyzeMyPortfolioHandler.ts +800 -0
- package/src/handlers/detectPatternsHandler.ts +77 -0
- package/src/handlers/detectPatternsViewsHandler.ts +518 -0
- package/src/handlers/getTickersJpyHandler.ts +145 -0
- package/src/handlers/getVolatilityMetricsHandler.ts +234 -0
- package/src/handlers/portfolio/calc.ts +549 -0
- package/src/handlers/portfolio/fetch.ts +318 -0
- package/src/handlers/portfolio/types.ts +170 -0
- package/src/handlers/renderChartSvgHandler.ts +69 -0
- package/src/handlers/runBacktestHandler.ts +70 -0
- package/src/http.ts +107 -0
- package/src/private/auth.ts +104 -0
- package/src/private/client.ts +298 -0
- package/src/private/config.ts +25 -0
- package/src/private/confirmation.ts +185 -0
- package/src/private/schemas.ts +866 -0
- package/src/prompts.ts +2296 -0
- package/src/resources/app-resources.ts +79 -0
- package/src/schema/analysis.ts +942 -0
- package/src/schema/backtest.ts +100 -0
- package/src/schema/base.ts +88 -0
- package/src/schema/candle-validate.ts +135 -0
- package/src/schema/chart.ts +399 -0
- package/src/schema/index.ts +11 -0
- package/src/schema/indicators.ts +125 -0
- package/src/schema/market-data.ts +298 -0
- package/src/schema/patterns.ts +382 -0
- package/src/schema/types.ts +97 -0
- package/src/schemas.d.ts +37 -0
- package/src/schemas.ts +7 -0
- package/src/server.ts +405 -0
- package/src/tool-definition.ts +44 -0
- package/src/tool-registry.ts +174 -0
- package/src/types/express-shim.d.ts +9 -0
- package/src/types/schemas.generated.d.ts +23 -0
- package/tools/analyze_bb_snapshot.ts +385 -0
- package/tools/analyze_candle_patterns.ts +810 -0
- package/tools/analyze_currency_strength.ts +273 -0
- package/tools/analyze_ema_snapshot.ts +183 -0
- package/tools/analyze_fibonacci.ts +530 -0
- package/tools/analyze_ichimoku_snapshot.ts +606 -0
- package/tools/analyze_indicators.ts +691 -0
- package/tools/analyze_market_signal.ts +665 -0
- package/tools/analyze_mtf_fibonacci.ts +273 -0
- package/tools/analyze_mtf_sma.ts +175 -0
- package/tools/analyze_sma_snapshot.ts +146 -0
- package/tools/analyze_stoch_snapshot.ts +276 -0
- package/tools/analyze_support_resistance.ts +817 -0
- package/tools/analyze_volume_profile.ts +546 -0
- package/tools/chart/ichimoku-cloud.ts +113 -0
- package/tools/chart/render-depth.ts +139 -0
- package/tools/chart/render-sub-panels.ts +208 -0
- package/tools/chart/svg-utils.ts +102 -0
- package/tools/detect_macd_cross.ts +691 -0
- package/tools/detect_patterns.ts +424 -0
- package/tools/detect_whale_events.ts +181 -0
- package/tools/get_candles.ts +487 -0
- package/tools/get_flow_metrics.ts +596 -0
- package/tools/get_orderbook.ts +540 -0
- package/tools/get_ticker.ts +132 -0
- package/tools/get_tickers_jpy.ts +240 -0
- package/tools/get_transactions.ts +209 -0
- package/tools/get_volatility_metrics.ts +302 -0
- package/tools/patterns/aftermath.ts +212 -0
- package/tools/patterns/config.ts +151 -0
- package/tools/patterns/detect_doubles.ts +650 -0
- package/tools/patterns/detect_hs.ts +635 -0
- package/tools/patterns/detect_pennants.ts +373 -0
- package/tools/patterns/detect_triangles.ts +820 -0
- package/tools/patterns/detect_triples.ts +633 -0
- package/tools/patterns/detect_wedges.ts +1072 -0
- package/tools/patterns/helpers.ts +517 -0
- package/tools/patterns/index.ts +40 -0
- package/tools/patterns/regression.ts +153 -0
- package/tools/patterns/smoothing.ts +168 -0
- package/tools/patterns/swing.ts +91 -0
- package/tools/patterns/types.ts +193 -0
- package/tools/prepare_chart_data.ts +294 -0
- package/tools/prepare_depth_data.ts +189 -0
- package/tools/private/analyze_my_portfolio.ts +21 -0
- package/tools/private/cancel_order.ts +127 -0
- package/tools/private/cancel_orders.ts +121 -0
- package/tools/private/create_order.ts +236 -0
- package/tools/private/get_margin_positions.ts +134 -0
- package/tools/private/get_margin_status.ts +155 -0
- package/tools/private/get_margin_trade_history.ts +156 -0
- package/tools/private/get_my_assets.ts +207 -0
- package/tools/private/get_my_deposit_withdrawal.ts +500 -0
- package/tools/private/get_my_orders.ts +157 -0
- package/tools/private/get_my_trade_history.ts +229 -0
- package/tools/private/get_order.ts +95 -0
- package/tools/private/get_orders_info.ts +90 -0
- package/tools/private/preview_cancel_order.ts +172 -0
- package/tools/private/preview_cancel_orders.ts +137 -0
- package/tools/private/preview_order.ts +292 -0
- package/tools/render_candle_pattern_diagram.ts +389 -0
- package/tools/render_chart_svg.ts +799 -0
- package/tools/render_depth_svg.ts +274 -0
- package/tools/trading_process/index.ts +7 -0
- package/tools/trading_process/lib/backtest_engine.ts +252 -0
- package/tools/trading_process/lib/equity.ts +131 -0
- package/tools/trading_process/lib/fetch_candles.ts +181 -0
- package/tools/trading_process/lib/sma.ts +62 -0
- package/tools/trading_process/lib/strategies/bb_breakout.ts +141 -0
- package/tools/trading_process/lib/strategies/index.ts +52 -0
- package/tools/trading_process/lib/strategies/macd_cross.ts +256 -0
- package/tools/trading_process/lib/strategies/rsi.ts +133 -0
- package/tools/trading_process/lib/strategies/sma_cross.ts +214 -0
- package/tools/trading_process/lib/strategies/types.ts +118 -0
- package/tools/trading_process/lib/svg_to_png.ts +64 -0
- package/tools/trading_process/render_backtest_chart_generic.ts +729 -0
- package/tools/trading_process/run_backtest.ts +243 -0
- package/tools/trading_process/types.ts +85 -0
- package/tools/validate_candle_data.ts +260 -0
- package/tsconfig.json +17 -0
- package/ui/cancel-confirm/dist/cancel-confirm.html +99 -0
- package/ui/order-confirm/dist/order-confirm.html +99 -0
|
@@ -0,0 +1,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
|
+
};
|