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,273 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import { formatPair, formatPercent, formatPrice } from '../lib/formatter.js';
|
|
3
|
+
import { failFromError, failFromValidation } from '../lib/result.js';
|
|
4
|
+
import { createMeta, ensurePair } from '../lib/validate.js';
|
|
5
|
+
import { AnalyzeMtfFibonacciInputSchema as _BaseInputSchema, AnalyzeMtfFibonacciOutputSchema } from '../src/schemas.js';
|
|
6
|
+
import analyzeFibonacci from './analyze_fibonacci.js';
|
|
7
|
+
|
|
8
|
+
const AnalyzeMtfFibonacciInputSchema = _BaseInputSchema.extend({
|
|
9
|
+
lookbackDays: z.array(z.number().int().min(14).max(365)).nonempty().optional().default([30, 90, 180]),
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
import type { Pair } from '../src/schemas.js';
|
|
13
|
+
import type { ToolDefinition } from '../src/tool-definition.js';
|
|
14
|
+
|
|
15
|
+
// ── Types ──
|
|
16
|
+
|
|
17
|
+
interface FibLevel {
|
|
18
|
+
ratio: number;
|
|
19
|
+
price: number;
|
|
20
|
+
distancePct: number;
|
|
21
|
+
isNearest: boolean;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
interface ConfluenceZone {
|
|
25
|
+
priceZone: [number, number];
|
|
26
|
+
matchedLevels: Array<{ lookbackDays: number; ratio: number; price: number }>;
|
|
27
|
+
strength: 'strong' | 'moderate' | 'weak';
|
|
28
|
+
distancePct: number;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// ── Confluence Detection ──
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Find price zones where Fibonacci levels from different lookback periods cluster together.
|
|
35
|
+
* A confluence zone indicates stronger support/resistance.
|
|
36
|
+
*/
|
|
37
|
+
function detectConfluence(
|
|
38
|
+
periodResults: Array<{ lookbackDays: number; levels: FibLevel[] }>,
|
|
39
|
+
currentPrice: number,
|
|
40
|
+
tolerancePct: number = 1.0,
|
|
41
|
+
): ConfluenceZone[] {
|
|
42
|
+
// Collect all levels from all periods
|
|
43
|
+
const allLevels: Array<{ lookbackDays: number; ratio: number; price: number }> = [];
|
|
44
|
+
|
|
45
|
+
for (const period of periodResults) {
|
|
46
|
+
for (const level of period.levels) {
|
|
47
|
+
// Skip 0% and 100% as they are just swing points
|
|
48
|
+
if (level.ratio === 0 || level.ratio === 1.0) continue;
|
|
49
|
+
allLevels.push({
|
|
50
|
+
lookbackDays: period.lookbackDays,
|
|
51
|
+
ratio: level.ratio,
|
|
52
|
+
price: level.price,
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
if (allLevels.length < 2) return [];
|
|
58
|
+
|
|
59
|
+
// Sort by price
|
|
60
|
+
const sorted = [...allLevels].sort((a, b) => a.price - b.price);
|
|
61
|
+
|
|
62
|
+
// Cluster nearby levels using tolerance
|
|
63
|
+
const zones: ConfluenceZone[] = [];
|
|
64
|
+
const used = new Set<number>();
|
|
65
|
+
|
|
66
|
+
for (let i = 0; i < sorted.length; i++) {
|
|
67
|
+
if (used.has(i)) continue;
|
|
68
|
+
|
|
69
|
+
const cluster = [sorted[i]];
|
|
70
|
+
used.add(i);
|
|
71
|
+
|
|
72
|
+
for (let j = i + 1; j < sorted.length; j++) {
|
|
73
|
+
if (used.has(j)) continue;
|
|
74
|
+
|
|
75
|
+
const avgPrice = cluster.reduce((s, c) => s + c.price, 0) / cluster.length;
|
|
76
|
+
const pctDiff = (Math.abs(sorted[j].price - avgPrice) / avgPrice) * 100;
|
|
77
|
+
|
|
78
|
+
if (pctDiff <= tolerancePct) {
|
|
79
|
+
cluster.push(sorted[j]);
|
|
80
|
+
used.add(j);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// Only consider as confluence if levels from at least 2 different periods match
|
|
85
|
+
const uniquePeriods = new Set(cluster.map((c) => c.lookbackDays));
|
|
86
|
+
if (uniquePeriods.size < 2) continue;
|
|
87
|
+
|
|
88
|
+
const prices = cluster.map((c) => c.price);
|
|
89
|
+
const zoneMin = Math.min(...prices);
|
|
90
|
+
const zoneMax = Math.max(...prices);
|
|
91
|
+
const zoneCenter = (zoneMin + zoneMax) / 2;
|
|
92
|
+
const distancePct = ((zoneCenter - currentPrice) / currentPrice) * 100;
|
|
93
|
+
|
|
94
|
+
const strength: 'strong' | 'moderate' | 'weak' =
|
|
95
|
+
uniquePeriods.size >= 3 ? 'strong' : uniquePeriods.size >= 2 && cluster.length >= 3 ? 'moderate' : 'weak';
|
|
96
|
+
|
|
97
|
+
zones.push({
|
|
98
|
+
priceZone: [Math.round(zoneMin), Math.round(zoneMax)],
|
|
99
|
+
matchedLevels: cluster,
|
|
100
|
+
strength,
|
|
101
|
+
distancePct: Number(distancePct.toFixed(2)),
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// Sort by distance from current price
|
|
106
|
+
zones.sort((a, b) => Math.abs(a.distancePct) - Math.abs(b.distancePct));
|
|
107
|
+
|
|
108
|
+
return zones;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// ── Content Generation ──
|
|
112
|
+
|
|
113
|
+
function generateMtfContent(
|
|
114
|
+
pair: string,
|
|
115
|
+
currentPrice: number,
|
|
116
|
+
periodResults: Array<{ lookbackDays: number; data: Record<string, unknown> }>,
|
|
117
|
+
confluence: ConfluenceZone[],
|
|
118
|
+
): Array<{ type: 'text'; text: string }> {
|
|
119
|
+
const lines: string[] = [];
|
|
120
|
+
const pairLabel = formatPair(pair);
|
|
121
|
+
|
|
122
|
+
lines.push(`【マルチタイムフレーム・フィボナッチ分析】${pairLabel}`);
|
|
123
|
+
lines.push(`現在価格: ${formatPrice(currentPrice, pair)}`);
|
|
124
|
+
lines.push('');
|
|
125
|
+
|
|
126
|
+
// Per-period summaries
|
|
127
|
+
for (const pr of periodResults) {
|
|
128
|
+
const d = pr.data;
|
|
129
|
+
const swingHigh = d.swingHigh as { price: number; date: string };
|
|
130
|
+
const swingLow = d.swingLow as { price: number; date: string };
|
|
131
|
+
lines.push(`--- ${pr.lookbackDays}日 ---`);
|
|
132
|
+
lines.push(` トレンド: ${d.trend === 'up' ? '上昇↑' : '下降↓'}`);
|
|
133
|
+
lines.push(` スイングハイ: ${formatPrice(swingHigh.price, pair)}(${swingHigh.date})`);
|
|
134
|
+
lines.push(` スイングロー: ${formatPrice(swingLow.price, pair)}(${swingLow.date})`);
|
|
135
|
+
|
|
136
|
+
// All retracement levels (0% and 100% are swing points, still useful for reference)
|
|
137
|
+
const allLevels = (d.levels as FibLevel[]) ?? [];
|
|
138
|
+
for (const kl of allLevels) {
|
|
139
|
+
const nearest = kl.isNearest ? ' ← 最寄り' : '';
|
|
140
|
+
lines.push(
|
|
141
|
+
` ${(kl.ratio * 100).toFixed(1)}%: ${formatPrice(kl.price, pair)} (${formatPercent(kl.distancePct, { sign: true })})${nearest}`,
|
|
142
|
+
);
|
|
143
|
+
}
|
|
144
|
+
lines.push('');
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// Confluence zones
|
|
148
|
+
if (confluence.length > 0) {
|
|
149
|
+
lines.push('【コンフルエンス(合流)ゾーン】');
|
|
150
|
+
for (const zone of confluence) {
|
|
151
|
+
const strengthJa = zone.strength === 'strong' ? '強' : zone.strength === 'moderate' ? '中' : '弱';
|
|
152
|
+
lines.push(
|
|
153
|
+
` ${formatPrice(zone.priceZone[0], pair)} 〜 ${formatPrice(zone.priceZone[1], pair)} (${formatPercent(zone.distancePct, { sign: true })}) [信頼度: ${strengthJa}]`,
|
|
154
|
+
);
|
|
155
|
+
for (const ml of zone.matchedLevels) {
|
|
156
|
+
lines.push(` - ${ml.lookbackDays}日: ${(ml.ratio * 100).toFixed(1)}% = ${formatPrice(ml.price, pair)}`);
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
lines.push('');
|
|
160
|
+
} else {
|
|
161
|
+
lines.push('【コンフルエンス】合流ゾーンは検出されませんでした');
|
|
162
|
+
lines.push('');
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
lines.push('【判定ロジック】');
|
|
166
|
+
lines.push('- 各ルックバック期間で独立にスイング検出・フィボナッチ水準を算出');
|
|
167
|
+
lines.push('- 異なる期間の水準が±1%以内に集中する「合流ゾーン」を検出');
|
|
168
|
+
lines.push('- 3期間以上の合流 → 信頼度「強」、2期間の合流 → 信頼度「中〜弱」');
|
|
169
|
+
|
|
170
|
+
return [{ type: 'text', text: lines.join('\n') }];
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
// ── Main Handler ──
|
|
174
|
+
|
|
175
|
+
export default async function analyzeMtfFibonacci(pair: string = 'btc_jpy', lookbackDays: number[] = [30, 90, 180]) {
|
|
176
|
+
const chk = ensurePair(pair);
|
|
177
|
+
if (!chk.ok) return failFromValidation(chk, AnalyzeMtfFibonacciOutputSchema);
|
|
178
|
+
|
|
179
|
+
try {
|
|
180
|
+
// Deduplicate lookback periods
|
|
181
|
+
const uniqueDays = [...new Set(lookbackDays)];
|
|
182
|
+
|
|
183
|
+
// Run all lookback periods in parallel
|
|
184
|
+
const results = await Promise.all(
|
|
185
|
+
uniqueDays.map(async (days) => {
|
|
186
|
+
const res = await analyzeFibonacci({
|
|
187
|
+
pair: chk.pair,
|
|
188
|
+
type: '1day',
|
|
189
|
+
lookbackDays: days,
|
|
190
|
+
mode: 'retracement',
|
|
191
|
+
historyLookbackDays: days,
|
|
192
|
+
});
|
|
193
|
+
return { lookbackDays: days, result: res as Record<string, unknown> };
|
|
194
|
+
}),
|
|
195
|
+
);
|
|
196
|
+
|
|
197
|
+
// Build per-period data
|
|
198
|
+
const periods: Record<string, unknown> = {};
|
|
199
|
+
const periodSummaries: Array<{ lookbackDays: number; data: Record<string, unknown>; levels: FibLevel[] }> = [];
|
|
200
|
+
let currentPrice = 0;
|
|
201
|
+
|
|
202
|
+
for (const { lookbackDays: days, result } of results) {
|
|
203
|
+
if (result?.ok && result?.data) {
|
|
204
|
+
const d = result.data as Record<string, unknown>;
|
|
205
|
+
const swingHigh = d.swingHigh as { price: number; date: string };
|
|
206
|
+
const swingLow = d.swingLow as { price: number; date: string };
|
|
207
|
+
const levels = (d.levels as FibLevel[]) ?? [];
|
|
208
|
+
periods[String(days)] = {
|
|
209
|
+
lookbackDays: days,
|
|
210
|
+
trend: d.trend,
|
|
211
|
+
swingHigh: { price: swingHigh.price, date: swingHigh.date },
|
|
212
|
+
swingLow: { price: swingLow.price, date: swingLow.date },
|
|
213
|
+
levels,
|
|
214
|
+
};
|
|
215
|
+
periodSummaries.push({ lookbackDays: days, data: d, levels });
|
|
216
|
+
if (d.currentPrice) currentPrice = d.currentPrice as number;
|
|
217
|
+
} else {
|
|
218
|
+
periods[String(days)] = {
|
|
219
|
+
lookbackDays: days,
|
|
220
|
+
trend: 'down',
|
|
221
|
+
swingHigh: { price: 0, date: '' },
|
|
222
|
+
swingLow: { price: 0, date: '' },
|
|
223
|
+
levels: [],
|
|
224
|
+
};
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
// Detect confluence
|
|
229
|
+
const confluenceInput = periodSummaries.map((ps) => ({
|
|
230
|
+
lookbackDays: ps.lookbackDays,
|
|
231
|
+
levels: ps.levels,
|
|
232
|
+
}));
|
|
233
|
+
const confluence = detectConfluence(confluenceInput, currentPrice);
|
|
234
|
+
|
|
235
|
+
// Generate content
|
|
236
|
+
const content = generateMtfContent(chk.pair, currentPrice, periodSummaries, confluence);
|
|
237
|
+
|
|
238
|
+
const confluenceCount = confluence.length;
|
|
239
|
+
const strongCount = confluence.filter((z) => z.strength === 'strong').length;
|
|
240
|
+
const summaryText =
|
|
241
|
+
confluenceCount > 0
|
|
242
|
+
? `${formatPair(chk.pair)} MTFフィボナッチ: ${confluenceCount}個の合流ゾーン検出(強: ${strongCount}個)`
|
|
243
|
+
: `${formatPair(chk.pair)} MTFフィボナッチ: 合流ゾーンなし(各期間の水準が分散)`;
|
|
244
|
+
|
|
245
|
+
const data = {
|
|
246
|
+
pair: chk.pair,
|
|
247
|
+
currentPrice,
|
|
248
|
+
periods,
|
|
249
|
+
confluence,
|
|
250
|
+
};
|
|
251
|
+
|
|
252
|
+
const meta = createMeta(chk.pair as Pair, { lookbackDays: uniqueDays });
|
|
253
|
+
|
|
254
|
+
return AnalyzeMtfFibonacciOutputSchema.parse({
|
|
255
|
+
ok: true,
|
|
256
|
+
summary: summaryText,
|
|
257
|
+
content,
|
|
258
|
+
data,
|
|
259
|
+
meta,
|
|
260
|
+
});
|
|
261
|
+
} catch (e: unknown) {
|
|
262
|
+
return failFromError(e, { schema: AnalyzeMtfFibonacciOutputSchema });
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
// ── MCP ツール定義(tool-registry から自動収集) ──
|
|
267
|
+
export const toolDef: ToolDefinition = {
|
|
268
|
+
name: 'analyze_mtf_fibonacci',
|
|
269
|
+
description: `[Multi-Timeframe Fibonacci / Confluence] 複数期間フィボナッチ一括分析(MTF fibonacci / confluence zone)。複数ルックバック期間の水準を並列計算し、コンフルエンス(合流)ゾーンを自動検出。analyze_fibonacci を個別に呼ぶ必要なし。`,
|
|
270
|
+
inputSchema: AnalyzeMtfFibonacciInputSchema,
|
|
271
|
+
handler: async ({ pair, lookbackDays }: { pair?: string; lookbackDays?: number[] }) =>
|
|
272
|
+
analyzeMtfFibonacci(pair, lookbackDays),
|
|
273
|
+
};
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
import type { z } from 'zod';
|
|
2
|
+
import { failFromError, failFromValidation, ok } from '../lib/result.js';
|
|
3
|
+
import { createMeta, ensurePair } from '../lib/validate.js';
|
|
4
|
+
import {
|
|
5
|
+
type AnalyzeMtfSmaDataSchemaOut,
|
|
6
|
+
AnalyzeMtfSmaInputSchema,
|
|
7
|
+
type AnalyzeMtfSmaMetaSchemaOut,
|
|
8
|
+
AnalyzeMtfSmaOutputSchema,
|
|
9
|
+
} from '../src/schemas.js';
|
|
10
|
+
import type { ToolDefinition } from '../src/tool-definition.js';
|
|
11
|
+
import analyzeSmaSnapshot from './analyze_sma_snapshot.js';
|
|
12
|
+
|
|
13
|
+
const ALIGNMENT_ICON: Record<string, string> = {
|
|
14
|
+
bullish: '🟢',
|
|
15
|
+
bearish: '🔴',
|
|
16
|
+
mixed: '🟡',
|
|
17
|
+
unknown: '⚪',
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
/** 単調減少なら "25>75>200"、単調増加なら "200>75>25"、それ以外は "mixed"。 */
|
|
21
|
+
function deriveSmaOrder(periods: number[], smaMap: Record<string, number | null> | undefined): string {
|
|
22
|
+
if (!smaMap) return 'mixed';
|
|
23
|
+
const uniq = [...new Set(periods)].sort((a, b) => a - b);
|
|
24
|
+
const vals = uniq.map((p) => smaMap[`SMA_${p}`]);
|
|
25
|
+
if (vals.some((v) => v == null || !Number.isFinite(v))) return 'mixed';
|
|
26
|
+
const nums = vals as number[];
|
|
27
|
+
const allDesc = nums.every((v, i) => i === 0 || v < nums[i - 1]);
|
|
28
|
+
const allAsc = nums.every((v, i) => i === 0 || v > nums[i - 1]);
|
|
29
|
+
if (allDesc) return uniq.join('>');
|
|
30
|
+
if (allAsc) return [...uniq].reverse().join('>');
|
|
31
|
+
return 'mixed';
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export default async function analyzeMtfSma(
|
|
35
|
+
pair: string = 'btc_jpy',
|
|
36
|
+
timeframes: string[] = ['1hour', '4hour', '1day'],
|
|
37
|
+
periods: number[] = [25, 75, 200],
|
|
38
|
+
) {
|
|
39
|
+
const chk = ensurePair(pair);
|
|
40
|
+
if (!chk.ok) return failFromValidation(chk, AnalyzeMtfSmaOutputSchema);
|
|
41
|
+
|
|
42
|
+
try {
|
|
43
|
+
// Deduplicate timeframes to avoid redundant calls
|
|
44
|
+
const uniqueTimeframes = [...new Set(timeframes)];
|
|
45
|
+
|
|
46
|
+
// Run all timeframes in parallel — each triggers analyzeIndicators
|
|
47
|
+
// which has a 30s TTL cache, so same pair+type won't re-fetch.
|
|
48
|
+
const results = await Promise.all(
|
|
49
|
+
uniqueTimeframes.map(async (tf) => {
|
|
50
|
+
const res = await analyzeSmaSnapshot(chk.pair, tf, 220, periods);
|
|
51
|
+
return { timeframe: tf, result: res as Record<string, unknown> };
|
|
52
|
+
}),
|
|
53
|
+
);
|
|
54
|
+
|
|
55
|
+
// Build per-timeframe results (pick fields relevant to MTF view)
|
|
56
|
+
const byTimeframe: Record<string, unknown> = {};
|
|
57
|
+
const alignments: string[] = [];
|
|
58
|
+
|
|
59
|
+
for (const { timeframe, result } of results) {
|
|
60
|
+
if (result?.ok && result?.data) {
|
|
61
|
+
const d = result.data as Record<string, unknown>;
|
|
62
|
+
const summary = d.summary as Record<string, unknown> | undefined;
|
|
63
|
+
const alignment = d.alignment as string;
|
|
64
|
+
const latest = d.latest as { close: number | null } | undefined;
|
|
65
|
+
const smaMap = d.sma as Record<string, number | null> | undefined;
|
|
66
|
+
byTimeframe[timeframe] = {
|
|
67
|
+
alignment,
|
|
68
|
+
alignmentIcon: ALIGNMENT_ICON[alignment] ?? '⚪',
|
|
69
|
+
position: (summary?.position as string) ?? 'unknown',
|
|
70
|
+
price: latest?.close ?? null,
|
|
71
|
+
latest: latest ?? { close: null },
|
|
72
|
+
sma: smaMap,
|
|
73
|
+
smas: d.smas,
|
|
74
|
+
smaOrder: deriveSmaOrder(periods, smaMap),
|
|
75
|
+
crosses: d.crosses,
|
|
76
|
+
recentCrosses: d.recentCrosses,
|
|
77
|
+
tags: d.tags,
|
|
78
|
+
};
|
|
79
|
+
alignments.push(alignment);
|
|
80
|
+
} else {
|
|
81
|
+
byTimeframe[timeframe] = {
|
|
82
|
+
alignment: 'unknown',
|
|
83
|
+
alignmentIcon: ALIGNMENT_ICON.unknown,
|
|
84
|
+
position: 'unknown',
|
|
85
|
+
price: null,
|
|
86
|
+
latest: { close: null },
|
|
87
|
+
smaOrder: 'mixed',
|
|
88
|
+
recentCrosses: [],
|
|
89
|
+
};
|
|
90
|
+
alignments.push('unknown');
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// Confluence judgment — any unknown in requested timeframes → aligned=false, direction=unknown
|
|
95
|
+
let direction: 'bullish' | 'bearish' | 'mixed' | 'unknown';
|
|
96
|
+
let aligned: boolean;
|
|
97
|
+
|
|
98
|
+
if (alignments.some((a) => a === 'unknown')) {
|
|
99
|
+
direction = 'unknown';
|
|
100
|
+
aligned = false;
|
|
101
|
+
} else if (alignments.every((a) => a === 'bullish')) {
|
|
102
|
+
direction = 'bullish';
|
|
103
|
+
aligned = true;
|
|
104
|
+
} else if (alignments.every((a) => a === 'bearish')) {
|
|
105
|
+
direction = 'bearish';
|
|
106
|
+
aligned = true;
|
|
107
|
+
} else {
|
|
108
|
+
direction = 'mixed';
|
|
109
|
+
aligned = false;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
const dirLabel = direction === 'bullish' ? '上昇' : direction === 'bearish' ? '下降' : '混合';
|
|
113
|
+
const tfEntry = (tf: string) => byTimeframe[tf] as Record<string, unknown> | undefined;
|
|
114
|
+
const summary = aligned
|
|
115
|
+
? `全時間軸が${dirLabel}方向で一致`
|
|
116
|
+
: `時間軸間で方向が分かれている(${timeframes.map((tf) => `${tf}:${tfEntry(tf)?.alignment}`).join(', ')})`;
|
|
117
|
+
|
|
118
|
+
const summaryText = `${timeframes.map((tf) => `${tf}: ${tfEntry(tf)?.alignment}`).join(' / ')} → ${summary}`;
|
|
119
|
+
|
|
120
|
+
const data: z.infer<typeof AnalyzeMtfSmaDataSchemaOut> = {
|
|
121
|
+
timeframes: byTimeframe as z.infer<typeof AnalyzeMtfSmaDataSchemaOut>['timeframes'],
|
|
122
|
+
confluence: { aligned, direction, summary },
|
|
123
|
+
};
|
|
124
|
+
|
|
125
|
+
const meta = createMeta(chk.pair, { timeframes, periods }) as z.infer<typeof AnalyzeMtfSmaMetaSchemaOut>;
|
|
126
|
+
return AnalyzeMtfSmaOutputSchema.parse(ok(summaryText, data, meta));
|
|
127
|
+
} catch (e: unknown) {
|
|
128
|
+
return failFromError(e, { schema: AnalyzeMtfSmaOutputSchema });
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
type MtfSmaEntry = z.infer<typeof AnalyzeMtfSmaDataSchemaOut>['timeframes'][string];
|
|
133
|
+
|
|
134
|
+
/** timeframe 1件分の構造化要約を組み立てる(LLM が content テキストから詳細表示を組めるよう) */
|
|
135
|
+
function buildTimeframeLine(tf: string, entry: MtfSmaEntry | undefined): string {
|
|
136
|
+
if (!entry) return `${tf}: no data`;
|
|
137
|
+
const ent = entry as unknown as Record<string, unknown>;
|
|
138
|
+
const icon = (ent.alignmentIcon as string) ?? '';
|
|
139
|
+
const alignment = ent.alignment as string;
|
|
140
|
+
const price = ent.price as number | null;
|
|
141
|
+
const smaOrder = (ent.smaOrder as string) ?? 'mixed';
|
|
142
|
+
const smas = (ent.smas ?? {}) as Record<string, Record<string, unknown>>;
|
|
143
|
+
const smaParts = Object.entries(smas).map(([period, info]) => {
|
|
144
|
+
const value = info.value;
|
|
145
|
+
const pos = info.pricePosition ? (info.pricePosition === 'above' ? '▲' : '▼') : '=';
|
|
146
|
+
const devStr =
|
|
147
|
+
info.distancePct != null ? ` (${(info.distancePct as number) >= 0 ? '+' : ''}${info.distancePct}%)` : '';
|
|
148
|
+
const slope = info.slope ? ` slope=${info.slope}` : '';
|
|
149
|
+
return `SMA${period}=${value ?? 'n/a'} ${pos}${devStr}${slope}`;
|
|
150
|
+
});
|
|
151
|
+
const recent = (ent.recentCrosses ?? []) as Array<{ type: string; pair: [number, number]; barsAgo: number }>;
|
|
152
|
+
const recentStr =
|
|
153
|
+
recent.length > 0
|
|
154
|
+
? ` / recent: ${recent.map((r) => `${r.type}(${r.pair.join('/')}, ${r.barsAgo}ago)`).join(', ')}`
|
|
155
|
+
: '';
|
|
156
|
+
return `${tf} ${icon} ${alignment} price=${price ?? 'n/a'} order=${smaOrder}\n ${smaParts.join(' | ')}${recentStr}`;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// ── MCP ツール定義(tool-registry から自動収集) ──
|
|
160
|
+
export const toolDef: ToolDefinition = {
|
|
161
|
+
name: 'analyze_mtf_sma',
|
|
162
|
+
description:
|
|
163
|
+
'[Multi-Timeframe SMA / MTF] 複数タイムフレームSMA一括分析(multi-timeframe / MTF / SMA alignment / confluence)。整列方向とコンフルエンスを判定。',
|
|
164
|
+
inputSchema: AnalyzeMtfSmaInputSchema,
|
|
165
|
+
handler: async ({ pair, timeframes, periods }: { pair?: string; timeframes?: string[]; periods?: number[] }) => {
|
|
166
|
+
const res = await analyzeMtfSma(pair, timeframes, periods);
|
|
167
|
+
if (!res?.ok) return res;
|
|
168
|
+
const requestedTfs = timeframes ?? ['1hour', '4hour', '1day'];
|
|
169
|
+
const lines = requestedTfs.map((tf) => buildTimeframeLine(tf, res.data.timeframes[tf]));
|
|
170
|
+
const conf = res.data.confluence;
|
|
171
|
+
const confLine = `Confluence: aligned=${conf.aligned} direction=${conf.direction} — ${conf.summary}`;
|
|
172
|
+
const text = `${res.summary}\n\n${lines.join('\n')}\n\n${confLine}`;
|
|
173
|
+
return { content: [{ type: 'text', text }], structuredContent: res as unknown as Record<string, unknown> };
|
|
174
|
+
},
|
|
175
|
+
};
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
import type { z } from 'zod';
|
|
2
|
+
import { formatSummary } from '../lib/formatter.js';
|
|
3
|
+
import {
|
|
4
|
+
buildMaLines,
|
|
5
|
+
buildMaSnapshotText,
|
|
6
|
+
type CrossStatus,
|
|
7
|
+
computeMaExt,
|
|
8
|
+
detectAlignment,
|
|
9
|
+
detectCrossStatuses,
|
|
10
|
+
detectPosition,
|
|
11
|
+
detectRecentCrosses,
|
|
12
|
+
generateCrossPairs,
|
|
13
|
+
getSeries,
|
|
14
|
+
type MaExtEntry,
|
|
15
|
+
type MaLineEntry,
|
|
16
|
+
type RecentCrossEntry,
|
|
17
|
+
} from '../lib/ma-snapshot-utils.js';
|
|
18
|
+
import { fail, failFromError, failFromValidation, ok } from '../lib/result.js';
|
|
19
|
+
import { createMeta, ensurePair } from '../lib/validate.js';
|
|
20
|
+
import {
|
|
21
|
+
type AnalyzeSmaSnapshotDataSchemaOut,
|
|
22
|
+
AnalyzeSmaSnapshotInputSchema,
|
|
23
|
+
AnalyzeSmaSnapshotOutputSchema,
|
|
24
|
+
} from '../src/schemas.js';
|
|
25
|
+
import type { ToolDefinition } from '../src/tool-definition.js';
|
|
26
|
+
import analyzeIndicators from './analyze_indicators.js';
|
|
27
|
+
|
|
28
|
+
export type { CrossStatus, MaLineEntry, RecentCrossEntry };
|
|
29
|
+
|
|
30
|
+
export interface BuildSmaSnapshotTextInput {
|
|
31
|
+
baseSummary: string;
|
|
32
|
+
type: string;
|
|
33
|
+
maLines: MaLineEntry[];
|
|
34
|
+
crossStatuses: CrossStatus[];
|
|
35
|
+
recentCrosses: RecentCrossEntry[];
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const SMA_FOOTER = [
|
|
39
|
+
'📌 含まれるもの: SMA値・傾き・クロス状態・配列パターン・価格との乖離',
|
|
40
|
+
'📌 含まれないもの: 他のテクニカル指標(RSI・MACD・BB・一目均衡表)、出来高フロー、板情報',
|
|
41
|
+
'📌 補完ツール: analyze_indicators(他指標), analyze_bb_snapshot(BB), get_flow_metrics(出来高), get_orderbook(板情報)',
|
|
42
|
+
];
|
|
43
|
+
|
|
44
|
+
/** テキスト組み立て(SMAスナップショット)— テスト可能な純粋関数 */
|
|
45
|
+
export function buildSmaSnapshotText(input: BuildSmaSnapshotTextInput): string {
|
|
46
|
+
return buildMaSnapshotText({ ...input, prefix: 'SMA', footerLines: SMA_FOOTER });
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export default async function analyzeSmaSnapshot(
|
|
50
|
+
pair: string = 'btc_jpy',
|
|
51
|
+
type: string = '1day',
|
|
52
|
+
limit: number = 220,
|
|
53
|
+
periods: number[] = [25, 75, 200],
|
|
54
|
+
) {
|
|
55
|
+
const chk = ensurePair(pair);
|
|
56
|
+
if (!chk.ok) return failFromValidation(chk, AnalyzeSmaSnapshotOutputSchema);
|
|
57
|
+
try {
|
|
58
|
+
const indRes = await analyzeIndicators(chk.pair, type, Math.max(Math.max(...periods, 200), limit));
|
|
59
|
+
if (!indRes.ok)
|
|
60
|
+
return AnalyzeSmaSnapshotOutputSchema.parse(
|
|
61
|
+
fail(indRes.summary || 'indicators failed', indRes.meta.errorType || 'internal'),
|
|
62
|
+
);
|
|
63
|
+
|
|
64
|
+
const close = indRes.data.normalized.at(-1)?.close ?? null;
|
|
65
|
+
const map: Record<string, number | null> = {};
|
|
66
|
+
const indRecord = indRes.data.indicators as Record<string, number[] | number | null>;
|
|
67
|
+
const get = (p: number) => (indRecord[`SMA_${p}`] as number | null) ?? null;
|
|
68
|
+
for (const p of periods) map[`SMA_${p}`] = get(p);
|
|
69
|
+
|
|
70
|
+
const chartInd: Record<string, unknown> = indRes?.data?.chart?.indicators ?? {};
|
|
71
|
+
const candles: Array<{ isoTime?: string | null }> = Array.isArray(indRes?.data?.chart?.candles)
|
|
72
|
+
? indRes.data.chart.candles
|
|
73
|
+
: Array.isArray(indRes?.data?.normalized)
|
|
74
|
+
? indRes.data.normalized
|
|
75
|
+
: [];
|
|
76
|
+
|
|
77
|
+
const crossPairs = generateCrossPairs(periods);
|
|
78
|
+
const crosses = detectCrossStatuses(crossPairs, map, 'SMA');
|
|
79
|
+
const recentCrosses = detectRecentCrosses(crossPairs, chartInd, candles, 'SMA');
|
|
80
|
+
|
|
81
|
+
const sortedPeriods = [...new Set(periods)].sort((a, b) => a - b);
|
|
82
|
+
const sortedVals = sortedPeriods.map((p) => map[`SMA_${p}`]);
|
|
83
|
+
const alignment = detectAlignment(sortedVals, { minPeriods: 2, strict: true });
|
|
84
|
+
|
|
85
|
+
const tags: string[] = [];
|
|
86
|
+
if (alignment === 'bullish') tags.push('sma_bullish_alignment');
|
|
87
|
+
if (alignment === 'bearish') tags.push('sma_bearish_alignment');
|
|
88
|
+
|
|
89
|
+
const smaVals = periods.map((p) => map[`SMA_${p}`]).filter((v): v is number => v != null);
|
|
90
|
+
const position = detectPosition(close, smaVals);
|
|
91
|
+
|
|
92
|
+
const smasExt: Record<string, MaExtEntry> = {};
|
|
93
|
+
for (const p of periods) {
|
|
94
|
+
const val = map[`SMA_${p}`];
|
|
95
|
+
const series = getSeries(chartInd, 'SMA', p);
|
|
96
|
+
smasExt[String(p)] = computeMaExt(close, val, series, type);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
const maLines = buildMaLines(periods, smasExt);
|
|
100
|
+
const summaryText = buildSmaSnapshotText({
|
|
101
|
+
baseSummary: formatSummary({
|
|
102
|
+
pair: chk.pair,
|
|
103
|
+
latest: close ?? undefined,
|
|
104
|
+
extra: `align=${alignment} pos=${position}`,
|
|
105
|
+
}),
|
|
106
|
+
type,
|
|
107
|
+
maLines,
|
|
108
|
+
crossStatuses: crosses,
|
|
109
|
+
recentCrosses,
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
const data: z.infer<typeof AnalyzeSmaSnapshotDataSchemaOut> = {
|
|
113
|
+
latest: { close },
|
|
114
|
+
sma: map,
|
|
115
|
+
crosses,
|
|
116
|
+
alignment,
|
|
117
|
+
tags,
|
|
118
|
+
summary: { close, align: alignment, position },
|
|
119
|
+
smas: smasExt,
|
|
120
|
+
recentCrosses,
|
|
121
|
+
};
|
|
122
|
+
const meta = createMeta(chk.pair, { type, count: indRes.data.normalized.length, periods });
|
|
123
|
+
return AnalyzeSmaSnapshotOutputSchema.parse(ok(summaryText, data, meta));
|
|
124
|
+
} catch (e: unknown) {
|
|
125
|
+
return failFromError(e, { schema: AnalyzeSmaSnapshotOutputSchema });
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// ── MCP ツール定義(tool-registry から自動収集) ──
|
|
130
|
+
export const toolDef: ToolDefinition = {
|
|
131
|
+
name: 'analyze_sma_snapshot',
|
|
132
|
+
description:
|
|
133
|
+
'[SMA / Moving Average / Golden Cross] SMA(simple moving average / golden cross / dead cross)の数値スナップショット。最新値・クロス検出・整列状態(bullish/bearish/mixed)。\n\n⚠️ 最新値のみ。時系列チャート描画 → prepare_chart_data(indicators: ["SMA_25","SMA_75"] 等)。',
|
|
134
|
+
inputSchema: AnalyzeSmaSnapshotInputSchema,
|
|
135
|
+
handler: async ({
|
|
136
|
+
pair,
|
|
137
|
+
type,
|
|
138
|
+
limit,
|
|
139
|
+
periods,
|
|
140
|
+
}: {
|
|
141
|
+
pair?: string;
|
|
142
|
+
type?: string;
|
|
143
|
+
limit?: number;
|
|
144
|
+
periods?: number[];
|
|
145
|
+
}) => analyzeSmaSnapshot(pair, type, limit, periods),
|
|
146
|
+
};
|