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,530 @@
|
|
|
1
|
+
import { formatPair, formatPercent, formatPrice, timeframeLabel } from '../lib/formatter.js';
|
|
2
|
+
import { fail, failFromError, failFromValidation } from '../lib/result.js';
|
|
3
|
+
import { createMeta, ensurePair } from '../lib/validate.js';
|
|
4
|
+
import type { Pair } from '../src/schemas.js';
|
|
5
|
+
import { AnalyzeFibonacciInputSchema, AnalyzeFibonacciOutputSchema } from '../src/schemas.js';
|
|
6
|
+
import type { ToolDefinition } from '../src/tool-definition.js';
|
|
7
|
+
import getCandles from './get_candles.js';
|
|
8
|
+
|
|
9
|
+
// ── Constants ──
|
|
10
|
+
|
|
11
|
+
/** Standard Fibonacci retracement ratios */
|
|
12
|
+
const RETRACEMENT_RATIOS = [0, 0.236, 0.382, 0.5, 0.618, 0.786, 1.0];
|
|
13
|
+
|
|
14
|
+
/** Standard Fibonacci extension ratios */
|
|
15
|
+
const EXTENSION_RATIOS = [1.272, Math.SQRT2, 1.618, 2.0, 2.618];
|
|
16
|
+
|
|
17
|
+
// ── Types ──
|
|
18
|
+
|
|
19
|
+
interface NormalizedCandle {
|
|
20
|
+
open: number;
|
|
21
|
+
high: number;
|
|
22
|
+
low: number;
|
|
23
|
+
close: number;
|
|
24
|
+
volume?: number;
|
|
25
|
+
isoTime?: string | null;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
interface SwingPoint {
|
|
29
|
+
price: number;
|
|
30
|
+
date: string;
|
|
31
|
+
index: number;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
interface FibLevel {
|
|
35
|
+
ratio: number;
|
|
36
|
+
price: number;
|
|
37
|
+
distancePct: number;
|
|
38
|
+
isNearest: boolean;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
interface LevelStat {
|
|
42
|
+
ratio: number;
|
|
43
|
+
samplesCount: number;
|
|
44
|
+
bounceRate: number;
|
|
45
|
+
avgBounceReturnPct: number;
|
|
46
|
+
avgBreakthroughReturnPct: number;
|
|
47
|
+
medianDwellBars: number;
|
|
48
|
+
confidence: 'high' | 'medium' | 'low';
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// ── Swing Detection ──
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Detect the most significant swing high and swing low within the given candle range.
|
|
55
|
+
* Uses a simple approach: find the highest high and lowest low, then determine trend
|
|
56
|
+
* based on which came first.
|
|
57
|
+
*/
|
|
58
|
+
function detectSignificantSwings(candles: NormalizedCandle[]): {
|
|
59
|
+
swingHigh: SwingPoint;
|
|
60
|
+
swingLow: SwingPoint;
|
|
61
|
+
trend: 'up' | 'down';
|
|
62
|
+
} {
|
|
63
|
+
let highestIdx = 0;
|
|
64
|
+
let lowestIdx = 0;
|
|
65
|
+
|
|
66
|
+
for (let i = 1; i < candles.length; i++) {
|
|
67
|
+
if (candles[i].high > candles[highestIdx].high) highestIdx = i;
|
|
68
|
+
if (candles[i].low < candles[lowestIdx].low) lowestIdx = i;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const swingHigh: SwingPoint = {
|
|
72
|
+
price: candles[highestIdx].high,
|
|
73
|
+
date: candles[highestIdx].isoTime?.split('T')[0] ?? '',
|
|
74
|
+
index: highestIdx,
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
const swingLow: SwingPoint = {
|
|
78
|
+
price: candles[lowestIdx].low,
|
|
79
|
+
date: candles[lowestIdx].isoTime?.split('T')[0] ?? '',
|
|
80
|
+
index: lowestIdx,
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
// Trend is determined by which swing came last:
|
|
84
|
+
// If the low came after the high → downtrend (price fell from high to low)
|
|
85
|
+
// If the high came after the low → uptrend (price rose from low to high)
|
|
86
|
+
const trend: 'up' | 'down' = lowestIdx > highestIdx ? 'down' : 'up';
|
|
87
|
+
|
|
88
|
+
return { swingHigh, swingLow, trend };
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// ── Fibonacci Level Calculation ──
|
|
92
|
+
|
|
93
|
+
function calculateLevels(
|
|
94
|
+
swingHigh: SwingPoint,
|
|
95
|
+
swingLow: SwingPoint,
|
|
96
|
+
trend: 'up' | 'down',
|
|
97
|
+
currentPrice: number,
|
|
98
|
+
ratios: number[],
|
|
99
|
+
): FibLevel[] {
|
|
100
|
+
const range = swingHigh.price - swingLow.price;
|
|
101
|
+
|
|
102
|
+
return ratios.map((ratio) => {
|
|
103
|
+
// In downtrend: retracement goes up from low
|
|
104
|
+
// In uptrend: retracement goes down from high
|
|
105
|
+
const price = trend === 'down' ? swingLow.price + range * ratio : swingHigh.price - range * ratio;
|
|
106
|
+
|
|
107
|
+
const distancePct = ((price - currentPrice) / currentPrice) * 100;
|
|
108
|
+
|
|
109
|
+
return { ratio, price: Math.round(price), distancePct: Number(distancePct.toFixed(2)), isNearest: false };
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function calculateExtensions(
|
|
114
|
+
swingHigh: SwingPoint,
|
|
115
|
+
swingLow: SwingPoint,
|
|
116
|
+
trend: 'up' | 'down',
|
|
117
|
+
currentPrice: number,
|
|
118
|
+
ratios: number[],
|
|
119
|
+
): FibLevel[] {
|
|
120
|
+
const range = swingHigh.price - swingLow.price;
|
|
121
|
+
|
|
122
|
+
return ratios.map((ratio) => {
|
|
123
|
+
// Extensions project beyond the swing points
|
|
124
|
+
// Uptrend: project above swingHigh → swingLow + range * ratio
|
|
125
|
+
// Downtrend: project below swingLow → swingHigh - range * ratio
|
|
126
|
+
const price = trend === 'up' ? swingLow.price + range * ratio : swingHigh.price - range * ratio;
|
|
127
|
+
|
|
128
|
+
const distancePct = ((price - currentPrice) / currentPrice) * 100;
|
|
129
|
+
|
|
130
|
+
return { ratio, price: Math.round(price), distancePct: Number(distancePct.toFixed(2)), isNearest: false };
|
|
131
|
+
});
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function markNearest(levels: FibLevel[], currentPrice: number): FibLevel[] {
|
|
135
|
+
if (levels.length === 0) return levels;
|
|
136
|
+
|
|
137
|
+
let minDist = Infinity;
|
|
138
|
+
let nearestIdx = 0;
|
|
139
|
+
|
|
140
|
+
for (let i = 0; i < levels.length; i++) {
|
|
141
|
+
const dist = Math.abs(levels[i].price - currentPrice);
|
|
142
|
+
if (dist < minDist) {
|
|
143
|
+
minDist = dist;
|
|
144
|
+
nearestIdx = i;
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
return levels.map((l, i) => ({ ...l, isNearest: i === nearestIdx }));
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
function findPosition(
|
|
152
|
+
levels: FibLevel[],
|
|
153
|
+
currentPrice: number,
|
|
154
|
+
): { aboveLevel: FibLevel | null; belowLevel: FibLevel | null; nearestLevel: FibLevel | null } {
|
|
155
|
+
const sorted = [...levels].sort((a, b) => a.price - b.price);
|
|
156
|
+
|
|
157
|
+
let aboveLevel: FibLevel | null = null;
|
|
158
|
+
let belowLevel: FibLevel | null = null;
|
|
159
|
+
const nearestLevel = levels.find((l) => l.isNearest) ?? null;
|
|
160
|
+
|
|
161
|
+
for (const level of sorted) {
|
|
162
|
+
if (level.price <= currentPrice) belowLevel = level;
|
|
163
|
+
if (level.price > currentPrice && !aboveLevel) aboveLevel = level;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
return { aboveLevel, belowLevel, nearestLevel };
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
// ── Historical Reaction Statistics (Feature #3) ──
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* Analyze how price has historically reacted at each Fibonacci level.
|
|
173
|
+
* Uses past candle data to count bounces vs breakthroughs at each level zone.
|
|
174
|
+
*/
|
|
175
|
+
function calculateLevelStats(candles: NormalizedCandle[], levels: FibLevel[], tolerancePct: number = 0.5): LevelStat[] {
|
|
176
|
+
return levels.map((level) => {
|
|
177
|
+
const zone = level.price * (tolerancePct / 100);
|
|
178
|
+
const zoneMin = level.price - zone;
|
|
179
|
+
const zoneMax = level.price + zone;
|
|
180
|
+
|
|
181
|
+
let samplesCount = 0;
|
|
182
|
+
let bounceCount = 0;
|
|
183
|
+
const bounceReturns: number[] = [];
|
|
184
|
+
const breakthroughReturns: number[] = [];
|
|
185
|
+
const dwellBars: number[] = [];
|
|
186
|
+
|
|
187
|
+
for (let i = 0; i < candles.length; i++) {
|
|
188
|
+
const candle = candles[i];
|
|
189
|
+
|
|
190
|
+
// Check if price touched the zone
|
|
191
|
+
const touchedZone = candle.low <= zoneMax && candle.high >= zoneMin;
|
|
192
|
+
if (!touchedZone) continue;
|
|
193
|
+
|
|
194
|
+
samplesCount++;
|
|
195
|
+
|
|
196
|
+
// Count dwell bars (how many consecutive bars stayed in zone)
|
|
197
|
+
let dwell = 1;
|
|
198
|
+
for (let j = i + 1; j < candles.length; j++) {
|
|
199
|
+
if (candles[j].low <= zoneMax && candles[j].high >= zoneMin) {
|
|
200
|
+
dwell++;
|
|
201
|
+
} else {
|
|
202
|
+
break;
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
dwellBars.push(dwell);
|
|
206
|
+
|
|
207
|
+
// Check what happened after touching the zone (look ahead 5 bars)
|
|
208
|
+
const lookAhead = Math.min(i + dwell + 5, candles.length - 1);
|
|
209
|
+
if (lookAhead <= i + dwell) continue;
|
|
210
|
+
|
|
211
|
+
const afterCandle = candles[lookAhead];
|
|
212
|
+
const returnPct = ((afterCandle.close - candle.close) / candle.close) * 100;
|
|
213
|
+
|
|
214
|
+
// Determine if it bounced (reversed) or broke through
|
|
215
|
+
const priceWasAbove = candle.close > level.price;
|
|
216
|
+
const priceStayedAbove = afterCandle.close > level.price;
|
|
217
|
+
|
|
218
|
+
if (priceWasAbove === priceStayedAbove) {
|
|
219
|
+
// Bounced back to same side
|
|
220
|
+
bounceCount++;
|
|
221
|
+
bounceReturns.push(Math.abs(returnPct));
|
|
222
|
+
} else {
|
|
223
|
+
// Broke through
|
|
224
|
+
breakthroughReturns.push(returnPct);
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
// Skip the dwell period to avoid double-counting
|
|
228
|
+
i += dwell - 1;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
const bounceRate = samplesCount > 0 ? bounceCount / samplesCount : 0;
|
|
232
|
+
const avgBounceReturnPct =
|
|
233
|
+
bounceReturns.length > 0 ? bounceReturns.reduce((a, b) => a + b, 0) / bounceReturns.length : 0;
|
|
234
|
+
const avgBreakthroughReturnPct =
|
|
235
|
+
breakthroughReturns.length > 0 ? breakthroughReturns.reduce((a, b) => a + b, 0) / breakthroughReturns.length : 0;
|
|
236
|
+
|
|
237
|
+
// Median dwell bars
|
|
238
|
+
const sortedDwell = [...dwellBars].sort((a, b) => a - b);
|
|
239
|
+
const medianDwellBars = sortedDwell.length > 0 ? sortedDwell[Math.floor(sortedDwell.length / 2)] : 0;
|
|
240
|
+
|
|
241
|
+
const confidence: 'high' | 'medium' | 'low' = samplesCount >= 8 ? 'high' : samplesCount >= 4 ? 'medium' : 'low';
|
|
242
|
+
|
|
243
|
+
return {
|
|
244
|
+
ratio: level.ratio,
|
|
245
|
+
samplesCount,
|
|
246
|
+
bounceRate: Number(bounceRate.toFixed(3)),
|
|
247
|
+
avgBounceReturnPct: Number(avgBounceReturnPct.toFixed(2)),
|
|
248
|
+
avgBreakthroughReturnPct: Number(avgBreakthroughReturnPct.toFixed(2)),
|
|
249
|
+
medianDwellBars,
|
|
250
|
+
confidence,
|
|
251
|
+
};
|
|
252
|
+
});
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
// ── Content Generation ──
|
|
256
|
+
|
|
257
|
+
function generateContent(
|
|
258
|
+
pair: string,
|
|
259
|
+
timeframe: string,
|
|
260
|
+
currentPrice: number,
|
|
261
|
+
trend: 'up' | 'down',
|
|
262
|
+
swingHigh: SwingPoint,
|
|
263
|
+
swingLow: SwingPoint,
|
|
264
|
+
range: number,
|
|
265
|
+
levels: FibLevel[],
|
|
266
|
+
extensions: FibLevel[],
|
|
267
|
+
position: { aboveLevel: FibLevel | null; belowLevel: FibLevel | null; nearestLevel: FibLevel | null },
|
|
268
|
+
levelStats: LevelStat[],
|
|
269
|
+
mode: string,
|
|
270
|
+
lookbackDays: number,
|
|
271
|
+
): Array<{ type: 'text'; text: string }> {
|
|
272
|
+
const lines: string[] = [];
|
|
273
|
+
const pairLabel = formatPair(pair);
|
|
274
|
+
const tfLabel = timeframeLabel(timeframe);
|
|
275
|
+
|
|
276
|
+
lines.push(`【フィボナッチ分析】${pairLabel} ${tfLabel}(過去${lookbackDays}日)`);
|
|
277
|
+
lines.push(`現在価格: ${formatPrice(currentPrice, pair)}`);
|
|
278
|
+
lines.push(`トレンド: ${trend === 'up' ? '上昇↑' : '下降↓'}`);
|
|
279
|
+
lines.push('');
|
|
280
|
+
|
|
281
|
+
lines.push(`スイングハイ: ${formatPrice(swingHigh.price, pair)}(${swingHigh.date})`);
|
|
282
|
+
lines.push(`スイングロー: ${formatPrice(swingLow.price, pair)}(${swingLow.date})`);
|
|
283
|
+
lines.push(`レンジ幅: ${formatPrice(range, pair)}`);
|
|
284
|
+
lines.push('');
|
|
285
|
+
|
|
286
|
+
// Current position
|
|
287
|
+
if (position.nearestLevel) {
|
|
288
|
+
lines.push(
|
|
289
|
+
`現在位置: ${(position.nearestLevel.ratio * 100).toFixed(1)}% 水準付近(距離 ${formatPercent(position.nearestLevel.distancePct, { sign: true })})`,
|
|
290
|
+
);
|
|
291
|
+
if (position.belowLevel && position.aboveLevel) {
|
|
292
|
+
lines.push(
|
|
293
|
+
` 下: ${(position.belowLevel.ratio * 100).toFixed(1)}% = ${formatPrice(position.belowLevel.price, pair)}`,
|
|
294
|
+
);
|
|
295
|
+
lines.push(
|
|
296
|
+
` 上: ${(position.aboveLevel.ratio * 100).toFixed(1)}% = ${formatPrice(position.aboveLevel.price, pair)}`,
|
|
297
|
+
);
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
lines.push('');
|
|
301
|
+
|
|
302
|
+
// Retracement levels
|
|
303
|
+
if (mode !== 'extension') {
|
|
304
|
+
lines.push('【リトレースメント水準】');
|
|
305
|
+
for (const level of levels) {
|
|
306
|
+
const nearest = level.isNearest ? ' ← 最寄り' : '';
|
|
307
|
+
lines.push(
|
|
308
|
+
` ${(level.ratio * 100).toFixed(1)}%: ${formatPrice(level.price, pair)} (${formatPercent(level.distancePct, { sign: true })})${nearest}`,
|
|
309
|
+
);
|
|
310
|
+
}
|
|
311
|
+
lines.push('');
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
// Extension levels
|
|
315
|
+
if (mode !== 'retracement' && extensions.length > 0) {
|
|
316
|
+
lines.push('【エクステンション水準】');
|
|
317
|
+
for (const ext of extensions) {
|
|
318
|
+
lines.push(
|
|
319
|
+
` ${(ext.ratio * 100).toFixed(1)}%: ${formatPrice(ext.price, pair)} (${formatPercent(ext.distancePct, { sign: true })})`,
|
|
320
|
+
);
|
|
321
|
+
}
|
|
322
|
+
lines.push('');
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
// Reaction stats — all levels with full detail
|
|
326
|
+
const meaningfulStats = levelStats.filter((s) => s.samplesCount >= 1);
|
|
327
|
+
if (meaningfulStats.length > 0) {
|
|
328
|
+
lines.push('【過去の反応実績(各水準の統計)】');
|
|
329
|
+
for (const stat of meaningfulStats) {
|
|
330
|
+
const ratioLabel = `${(stat.ratio * 100).toFixed(1)}%`;
|
|
331
|
+
if (stat.samplesCount === 0) {
|
|
332
|
+
lines.push(` ${ratioLabel}: データなし`);
|
|
333
|
+
continue;
|
|
334
|
+
}
|
|
335
|
+
const bounceRatePct = (stat.bounceRate * 100).toFixed(0);
|
|
336
|
+
lines.push(
|
|
337
|
+
` ${ratioLabel}: 反発率 ${bounceRatePct}%(${stat.samplesCount}回中${Math.round(stat.bounceRate * stat.samplesCount)}回反発)`,
|
|
338
|
+
);
|
|
339
|
+
lines.push(` - 反発後の平均リターン: ${formatPercent(stat.avgBounceReturnPct, { sign: true })}`);
|
|
340
|
+
lines.push(` - ブレイク後の平均リターン: ${formatPercent(stat.avgBreakthroughReturnPct, { sign: true })}`);
|
|
341
|
+
lines.push(` - 水準付近の滞在足数(中央値): ${stat.medianDwellBars}本`);
|
|
342
|
+
lines.push(` - 信頼度: ${stat.confidence}(サンプル${stat.samplesCount}件)`);
|
|
343
|
+
}
|
|
344
|
+
lines.push('');
|
|
345
|
+
|
|
346
|
+
// Highlight best bounce
|
|
347
|
+
const bestBounce = [...meaningfulStats]
|
|
348
|
+
.filter((s) => s.samplesCount >= 2)
|
|
349
|
+
.sort((a, b) => b.bounceRate - a.bounceRate)[0];
|
|
350
|
+
if (bestBounce) {
|
|
351
|
+
lines.push(
|
|
352
|
+
`注目: ${(bestBounce.ratio * 100).toFixed(1)}% 水準が最も反発率が高い(${(bestBounce.bounceRate * 100).toFixed(0)}%、${bestBounce.samplesCount}回中)`,
|
|
353
|
+
);
|
|
354
|
+
lines.push('');
|
|
355
|
+
}
|
|
356
|
+
} else {
|
|
357
|
+
lines.push('【過去の反応実績】該当データなし(分析期間内に各水準へのタッチがありませんでした)');
|
|
358
|
+
lines.push('');
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
lines.push('【判定ロジック】');
|
|
362
|
+
lines.push(`- スイング検出: 期間内の最高値・最安値を自動検出`);
|
|
363
|
+
lines.push(`- トレンド判定: 高値→安値(下降)、安値→高値(上昇)の時系列順`);
|
|
364
|
+
lines.push(`- リトレースメント: 0%, 23.6%, 38.2%, 50%, 61.8%, 78.6%, 100%`);
|
|
365
|
+
lines.push(`- エクステンション: 127.2%, 141.4%, 161.8%, 200%, 261.8%`);
|
|
366
|
+
if (meaningfulStats.length > 0) {
|
|
367
|
+
lines.push(`- 反応実績: 各水準±0.5%ゾーンへのタッチを集計(過去データ)`);
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
return [{ type: 'text', text: lines.join('\n') }];
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
// ── Main Handler ──
|
|
374
|
+
|
|
375
|
+
export default async function analyzeFibonacci(opts: Record<string, unknown> = {}) {
|
|
376
|
+
const input = AnalyzeFibonacciInputSchema.parse(opts);
|
|
377
|
+
const pair = input.pair as string;
|
|
378
|
+
const { type: timeframe, lookbackDays, mode, historyLookbackDays } = input;
|
|
379
|
+
|
|
380
|
+
const chk = ensurePair(pair);
|
|
381
|
+
if (!chk.ok) return failFromValidation(chk, AnalyzeFibonacciOutputSchema);
|
|
382
|
+
|
|
383
|
+
try {
|
|
384
|
+
// Fetch candle data for analysis period
|
|
385
|
+
const candlesRes = await getCandles(chk.pair, timeframe, undefined, lookbackDays + 10);
|
|
386
|
+
if (!candlesRes.ok) {
|
|
387
|
+
return AnalyzeFibonacciOutputSchema.parse(
|
|
388
|
+
fail(candlesRes.summary || 'candles failed', candlesRes.meta.errorType || 'internal'),
|
|
389
|
+
);
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
const candles: NormalizedCandle[] = candlesRes.data.normalized || [];
|
|
393
|
+
if (candles.length < 10) {
|
|
394
|
+
return AnalyzeFibonacciOutputSchema.parse(fail('ローソク足データが不足しています(最低10本必要)', 'data'));
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
const currentPrice = candles[candles.length - 1].close;
|
|
398
|
+
|
|
399
|
+
// Detect swings
|
|
400
|
+
const { swingHigh, swingLow, trend } = detectSignificantSwings(candles);
|
|
401
|
+
const range = swingHigh.price - swingLow.price;
|
|
402
|
+
|
|
403
|
+
if (range <= 0) {
|
|
404
|
+
return AnalyzeFibonacciOutputSchema.parse(fail('スイングハイとスイングローの差が検出できません', 'data'));
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
// Calculate levels
|
|
408
|
+
let levels: FibLevel[] = [];
|
|
409
|
+
let extensions: FibLevel[] = [];
|
|
410
|
+
|
|
411
|
+
if (mode !== 'extension') {
|
|
412
|
+
levels = calculateLevels(swingHigh, swingLow, trend, currentPrice, RETRACEMENT_RATIOS);
|
|
413
|
+
levels = markNearest(levels, currentPrice);
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
if (mode !== 'retracement') {
|
|
417
|
+
extensions = calculateExtensions(swingHigh, swingLow, trend, currentPrice, EXTENSION_RATIOS);
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
const _allLevels = [...levels, ...extensions];
|
|
421
|
+
const position = findPosition(levels.length > 0 ? levels : extensions, currentPrice);
|
|
422
|
+
|
|
423
|
+
// Calculate historical reaction stats (Feature #3)
|
|
424
|
+
// Fetch extended history for statistics
|
|
425
|
+
let levelStats: LevelStat[] = [];
|
|
426
|
+
if (levels.length > 0) {
|
|
427
|
+
let historyCandles: NormalizedCandle[] = candles;
|
|
428
|
+
if (historyLookbackDays > lookbackDays) {
|
|
429
|
+
try {
|
|
430
|
+
const histRes = await getCandles(chk.pair, timeframe, undefined, historyLookbackDays + 10);
|
|
431
|
+
if (histRes.ok && histRes.data.normalized?.length > 0) {
|
|
432
|
+
historyCandles = histRes.data.normalized;
|
|
433
|
+
}
|
|
434
|
+
} catch {
|
|
435
|
+
// Fall back to current candle data
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
levelStats = calculateLevelStats(historyCandles, levels);
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
// Generate content
|
|
442
|
+
const content = generateContent(
|
|
443
|
+
chk.pair,
|
|
444
|
+
timeframe,
|
|
445
|
+
currentPrice,
|
|
446
|
+
trend,
|
|
447
|
+
swingHigh,
|
|
448
|
+
swingLow,
|
|
449
|
+
range,
|
|
450
|
+
levels,
|
|
451
|
+
extensions,
|
|
452
|
+
position,
|
|
453
|
+
levelStats,
|
|
454
|
+
mode,
|
|
455
|
+
lookbackDays,
|
|
456
|
+
);
|
|
457
|
+
|
|
458
|
+
const nearestLabel = position.nearestLevel ? `${(position.nearestLevel.ratio * 100).toFixed(1)}%水準付近` : '';
|
|
459
|
+
const summaryText = `${formatPair(chk.pair)} フィボナッチ分析: ${trend === 'up' ? '上昇' : '下降'}トレンド、${nearestLabel}(${formatPrice(currentPrice, chk.pair)})`;
|
|
460
|
+
|
|
461
|
+
const data = {
|
|
462
|
+
pair: chk.pair,
|
|
463
|
+
timeframe,
|
|
464
|
+
currentPrice,
|
|
465
|
+
trend,
|
|
466
|
+
swingHigh,
|
|
467
|
+
swingLow,
|
|
468
|
+
range,
|
|
469
|
+
levels,
|
|
470
|
+
extensions,
|
|
471
|
+
position,
|
|
472
|
+
levelStats: levelStats.length > 0 ? levelStats : undefined,
|
|
473
|
+
};
|
|
474
|
+
|
|
475
|
+
const meta = createMeta(chk.pair as Pair, {
|
|
476
|
+
timeframe,
|
|
477
|
+
lookbackDays,
|
|
478
|
+
mode,
|
|
479
|
+
historyLookbackDays: levelStats.length > 0 ? historyLookbackDays : undefined,
|
|
480
|
+
});
|
|
481
|
+
|
|
482
|
+
return AnalyzeFibonacciOutputSchema.parse({
|
|
483
|
+
ok: true,
|
|
484
|
+
summary: summaryText,
|
|
485
|
+
content,
|
|
486
|
+
data,
|
|
487
|
+
meta,
|
|
488
|
+
});
|
|
489
|
+
} catch (err: unknown) {
|
|
490
|
+
return failFromError(err, {
|
|
491
|
+
schema: AnalyzeFibonacciOutputSchema,
|
|
492
|
+
defaultMessage: 'フィボナッチ分析中にエラーが発生しました',
|
|
493
|
+
});
|
|
494
|
+
}
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
// ── MCP ツール定義(tool-registry から自動収集) ──
|
|
498
|
+
export const toolDef: ToolDefinition = {
|
|
499
|
+
name: 'analyze_fibonacci',
|
|
500
|
+
description: `フィボナッチ・リトレースメント/エクステンション水準を自動計算。
|
|
501
|
+
|
|
502
|
+
【機能】
|
|
503
|
+
- スイングハイ・スイングローを自動検出しトレンド判定
|
|
504
|
+
- リトレースメント水準(0%, 23.6%, 38.2%, 50%, 61.8%, 78.6%, 100%)を算出
|
|
505
|
+
- エクステンション水準(127.2%, 141.4%, 161.8%, 200%, 261.8%)を算出
|
|
506
|
+
- 現在価格と各水準の距離(%)、最寄り水準を特定
|
|
507
|
+
- 過去の反応実績(反発率・平均リターン・滞在期間)を統計
|
|
508
|
+
|
|
509
|
+
【出力】
|
|
510
|
+
- content: LLM 向けテキスト解説
|
|
511
|
+
- structuredContent.data: 全水準の価格・距離%・反応統計を含む JSON
|
|
512
|
+
→ render_chart_svg や HTML アーティファクトで即座に可視化可能
|
|
513
|
+
|
|
514
|
+
【パラメータ】
|
|
515
|
+
- pair: 通貨ペア(デフォルト: btc_jpy)
|
|
516
|
+
- type: 時間足(デフォルト: 1day)
|
|
517
|
+
- lookbackDays: 分析期間(デフォルト: 90日)
|
|
518
|
+
- mode: retracement / extension / both(デフォルト: both)
|
|
519
|
+
- historyLookbackDays: 反応実績の集計期間(デフォルト: 180日)
|
|
520
|
+
|
|
521
|
+
複数タイムフレーム分析が必要な場合は analyze_mtf_fibonacci を使用。`,
|
|
522
|
+
inputSchema: AnalyzeFibonacciInputSchema,
|
|
523
|
+
handler: async (args: {
|
|
524
|
+
pair?: string;
|
|
525
|
+
type?: string;
|
|
526
|
+
lookbackDays?: number;
|
|
527
|
+
mode?: 'retracement' | 'extension' | 'both';
|
|
528
|
+
historyLookbackDays?: number;
|
|
529
|
+
}) => analyzeFibonacci(args),
|
|
530
|
+
};
|