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 type { z } from 'zod';
|
|
2
|
+
import { toNum } from '../lib/conversions.js';
|
|
3
|
+
import { nowIso } from '../lib/datetime.js';
|
|
4
|
+
import { formatPercent, formatPriceJPY, formatVolumeJPY } from '../lib/formatter.js';
|
|
5
|
+
import { fail, failFromError, ok } from '../lib/result.js';
|
|
6
|
+
import {
|
|
7
|
+
type AnalyzeCurrencyStrengthDataSchemaOut,
|
|
8
|
+
AnalyzeCurrencyStrengthInputSchema,
|
|
9
|
+
type AnalyzeCurrencyStrengthMetaSchemaOut,
|
|
10
|
+
AnalyzeCurrencyStrengthOutputSchema,
|
|
11
|
+
} from '../src/schemas.js';
|
|
12
|
+
import type { ToolDefinition } from '../src/tool-definition.js';
|
|
13
|
+
import analyzeIndicators from './analyze_indicators.js';
|
|
14
|
+
import getTickersJpy from './get_tickers_jpy.js';
|
|
15
|
+
|
|
16
|
+
// ── Types ──
|
|
17
|
+
|
|
18
|
+
interface TickerItem {
|
|
19
|
+
pair: string;
|
|
20
|
+
last: string;
|
|
21
|
+
open: string;
|
|
22
|
+
vol: string;
|
|
23
|
+
change24h?: number | null;
|
|
24
|
+
change24hPct?: number | null;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
interface RankedItem {
|
|
28
|
+
pair: string;
|
|
29
|
+
currency: string;
|
|
30
|
+
score: number;
|
|
31
|
+
rank: number;
|
|
32
|
+
components: {
|
|
33
|
+
change24h: number | null;
|
|
34
|
+
rsi: number | null;
|
|
35
|
+
smaDeviation: number | null;
|
|
36
|
+
volumeRank: number;
|
|
37
|
+
};
|
|
38
|
+
price: number | null;
|
|
39
|
+
volumeJPY: number | null;
|
|
40
|
+
interpretation: 'strong_bullish' | 'bullish' | 'neutral' | 'bearish' | 'strong_bearish';
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// ── Helpers ──
|
|
44
|
+
|
|
45
|
+
function clamp(x: number, min: number, max: number) {
|
|
46
|
+
return Math.max(min, Math.min(max, x));
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function interpret(score: number): RankedItem['interpretation'] {
|
|
50
|
+
if (score >= 50) return 'strong_bullish';
|
|
51
|
+
if (score >= 20) return 'bullish';
|
|
52
|
+
if (score <= -50) return 'strong_bearish';
|
|
53
|
+
if (score <= -20) return 'bearish';
|
|
54
|
+
return 'neutral';
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function extractCurrency(pair: string): string {
|
|
58
|
+
return pair.replace(/_jpy$/i, '').toUpperCase();
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// ── Main ──
|
|
62
|
+
|
|
63
|
+
export default async function analyzeCurrencyStrength(topN: number = 10, type: string = '1day') {
|
|
64
|
+
try {
|
|
65
|
+
// 1. Fetch all tickers
|
|
66
|
+
const tickerRes = await getTickersJpy();
|
|
67
|
+
if (!tickerRes?.ok) {
|
|
68
|
+
return AnalyzeCurrencyStrengthOutputSchema.parse(fail(tickerRes?.summary || 'tickers fetch failed', 'upstream'));
|
|
69
|
+
}
|
|
70
|
+
const tickers: TickerItem[] = Array.isArray(tickerRes.data) ? tickerRes.data : [];
|
|
71
|
+
if (tickers.length === 0) {
|
|
72
|
+
return AnalyzeCurrencyStrengthOutputSchema.parse(fail('ティッカーデータが空です', 'upstream'));
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// 2. Deduplicate by pair (keep first occurrence), sort by volume descending, pick top N
|
|
76
|
+
const seenPairs = new Set<string>();
|
|
77
|
+
const uniqueTickers = tickers.filter((t) => {
|
|
78
|
+
if (seenPairs.has(t.pair)) return false;
|
|
79
|
+
seenPairs.add(t.pair);
|
|
80
|
+
return true;
|
|
81
|
+
});
|
|
82
|
+
const withVolume = uniqueTickers.map((t) => {
|
|
83
|
+
const lastN = toNum(t.last);
|
|
84
|
+
const volN = toNum(t.vol);
|
|
85
|
+
const volumeJPY = lastN != null && volN != null ? lastN * volN : 0;
|
|
86
|
+
const openN = toNum(t.open);
|
|
87
|
+
const change24h =
|
|
88
|
+
t.change24hPct != null
|
|
89
|
+
? toNum(t.change24hPct)
|
|
90
|
+
: openN != null && openN > 0 && lastN != null
|
|
91
|
+
? Number((((lastN - openN) / openN) * 100).toFixed(2))
|
|
92
|
+
: null;
|
|
93
|
+
return { ...t, lastN, volumeJPY, change24h };
|
|
94
|
+
});
|
|
95
|
+
withVolume.sort((a, b) => b.volumeJPY - a.volumeJPY);
|
|
96
|
+
const targets = withVolume.slice(0, topN);
|
|
97
|
+
|
|
98
|
+
// 3. Fetch RSI & SMA for each target (parallel, with concurrency limit)
|
|
99
|
+
const MAX_CONCURRENT = 3;
|
|
100
|
+
const indicatorResults: PromiseSettledResult<Awaited<ReturnType<typeof analyzeIndicators>>>[] = [];
|
|
101
|
+
for (let i = 0; i < targets.length; i += MAX_CONCURRENT) {
|
|
102
|
+
const batch = targets.slice(i, i + MAX_CONCURRENT);
|
|
103
|
+
const batchResults = await Promise.allSettled(batch.map((t) => analyzeIndicators(t.pair, type, 60)));
|
|
104
|
+
indicatorResults.push(...batchResults);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// 3b. Check failures
|
|
108
|
+
const failedIndicatorPairs: string[] = [];
|
|
109
|
+
for (let i = 0; i < indicatorResults.length; i++) {
|
|
110
|
+
const r = indicatorResults[i];
|
|
111
|
+
if (r.status === 'rejected' || !r.value.ok) {
|
|
112
|
+
failedIndicatorPairs.push(targets[i].pair);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
const allIndicatorsFailed = failedIndicatorPairs.length === targets.length;
|
|
116
|
+
if (allIndicatorsFailed) {
|
|
117
|
+
return AnalyzeCurrencyStrengthOutputSchema.parse(fail('全銘柄のテクニカル指標取得に失敗しました', 'upstream'));
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// 4. Compute composite score for each
|
|
121
|
+
const items: RankedItem[] = targets.map((t, i) => {
|
|
122
|
+
const indResult = indicatorResults[i];
|
|
123
|
+
const ind = indResult.status === 'fulfilled' && indResult.value.ok ? indResult.value.data : null;
|
|
124
|
+
|
|
125
|
+
// Component: 24h change → score [-100, +100]
|
|
126
|
+
// ±5% → ±100
|
|
127
|
+
const changePct = t.change24h;
|
|
128
|
+
const changeScore = changePct != null ? clamp((changePct / 5) * 100, -100, 100) : 0;
|
|
129
|
+
|
|
130
|
+
// Component: RSI → score [-100, +100]
|
|
131
|
+
// RSI 50 = 0, RSI 70+ = +100, RSI 30- = -100
|
|
132
|
+
const rsi = (ind?.indicators?.RSI_14 as number | null) ?? null;
|
|
133
|
+
const rsiScore = rsi != null ? clamp(((rsi - 50) / 20) * 100, -100, 100) : 0;
|
|
134
|
+
|
|
135
|
+
// Component: SMA25 deviation → score [-100, +100]
|
|
136
|
+
// price > SMA25 = positive, price < SMA25 = negative
|
|
137
|
+
// ±3% deviation → ±100
|
|
138
|
+
const sma25 = (ind?.indicators?.SMA_25 as number | null) ?? null;
|
|
139
|
+
const latestClose = ind?.normalized?.at(-1)?.close as number | undefined;
|
|
140
|
+
let smaDeviation: number | null = null;
|
|
141
|
+
let smaScore = 0;
|
|
142
|
+
if (sma25 != null && latestClose != null && sma25 > 0) {
|
|
143
|
+
smaDeviation = Number((((latestClose - sma25) / sma25) * 100).toFixed(2));
|
|
144
|
+
smaScore = clamp((smaDeviation / 3) * 100, -100, 100);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// Component: volume rank bonus (higher volume → small positive bias)
|
|
148
|
+
const volumeRank = i + 1;
|
|
149
|
+
const volumeBonus = clamp(((topN - i) / topN) * 10, 0, 10);
|
|
150
|
+
|
|
151
|
+
// Composite: 40% change + 30% RSI + 25% SMA + 5% volume
|
|
152
|
+
const score = Number((changeScore * 0.4 + rsiScore * 0.3 + smaScore * 0.25 + volumeBonus * 0.05).toFixed(1));
|
|
153
|
+
|
|
154
|
+
return {
|
|
155
|
+
pair: t.pair,
|
|
156
|
+
currency: extractCurrency(t.pair),
|
|
157
|
+
score,
|
|
158
|
+
rank: 0, // assigned after sorting
|
|
159
|
+
components: {
|
|
160
|
+
change24h: changePct,
|
|
161
|
+
rsi,
|
|
162
|
+
smaDeviation,
|
|
163
|
+
volumeRank,
|
|
164
|
+
},
|
|
165
|
+
price: Number.isFinite(t.lastN) ? t.lastN : null,
|
|
166
|
+
volumeJPY: t.volumeJPY > 0 ? t.volumeJPY : null,
|
|
167
|
+
interpretation: interpret(score),
|
|
168
|
+
};
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
// 5. Sort by score descending and assign ranks
|
|
172
|
+
items.sort((a, b) => b.score - a.score);
|
|
173
|
+
items.forEach((item, i) => {
|
|
174
|
+
item.rank = i + 1;
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
// 6. Build summary
|
|
178
|
+
const strongBullish = items.filter((it) => it.interpretation === 'strong_bullish').map((it) => it.currency);
|
|
179
|
+
const strongBearish = items.filter((it) => it.interpretation === 'strong_bearish').map((it) => it.currency);
|
|
180
|
+
const avgScore = Number((items.reduce((s, it) => s + it.score, 0) / items.length).toFixed(1));
|
|
181
|
+
const marketBias =
|
|
182
|
+
avgScore >= 15 ? ('bullish' as const) : avgScore <= -15 ? ('bearish' as const) : ('neutral' as const);
|
|
183
|
+
|
|
184
|
+
const data = {
|
|
185
|
+
rankings: items,
|
|
186
|
+
summary: {
|
|
187
|
+
totalPairs: tickers.length,
|
|
188
|
+
analyzedPairs: items.length,
|
|
189
|
+
strongBullish,
|
|
190
|
+
strongBearish,
|
|
191
|
+
marketBias,
|
|
192
|
+
avgScore,
|
|
193
|
+
},
|
|
194
|
+
};
|
|
195
|
+
|
|
196
|
+
const meta: Record<string, unknown> = {
|
|
197
|
+
fetchedAt: nowIso(),
|
|
198
|
+
type,
|
|
199
|
+
topN,
|
|
200
|
+
};
|
|
201
|
+
if (failedIndicatorPairs.length > 0) {
|
|
202
|
+
meta.warning = `⚠️ ${targets.length}銘柄中${failedIndicatorPairs.length}銘柄の指標取得に失敗しました: ${failedIndicatorPairs.join(', ')}`;
|
|
203
|
+
meta.failedPairs = failedIndicatorPairs;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
// Build text summary
|
|
207
|
+
const lines: string[] = [];
|
|
208
|
+
if (failedIndicatorPairs.length > 0) {
|
|
209
|
+
lines.push(
|
|
210
|
+
`⚠️ ${targets.length}銘柄中${failedIndicatorPairs.length}銘柄の指標取得に失敗(${failedIndicatorPairs.join(', ')})。該当銘柄のスコアは価格変動のみで算出されています。`,
|
|
211
|
+
);
|
|
212
|
+
lines.push('');
|
|
213
|
+
}
|
|
214
|
+
lines.push(`📊 通貨強弱ランキング(出来高上位${items.length}銘柄 / 全${tickers.length}ペア)`);
|
|
215
|
+
lines.push(
|
|
216
|
+
`市場バイアス: ${marketBias === 'bullish' ? '🟢 強気' : marketBias === 'bearish' ? '🔴 弱気' : '⚪ 中立'}(平均スコア: ${avgScore})`,
|
|
217
|
+
);
|
|
218
|
+
lines.push('');
|
|
219
|
+
for (const item of items) {
|
|
220
|
+
const emoji =
|
|
221
|
+
item.score >= 50 ? '🟢' : item.score >= 20 ? '🔵' : item.score <= -50 ? '🔴' : item.score <= -20 ? '🟠' : '⚪';
|
|
222
|
+
const chgStr =
|
|
223
|
+
item.components.change24h != null ? formatPercent(item.components.change24h, { sign: true, digits: 2 }) : 'n/a';
|
|
224
|
+
const rsiStr = item.components.rsi != null ? `RSI=${Math.round(item.components.rsi)}` : 'RSI=n/a';
|
|
225
|
+
const smaStr =
|
|
226
|
+
item.components.smaDeviation != null
|
|
227
|
+
? `SMA25乖離${formatPercent(item.components.smaDeviation, { sign: true, digits: 2 })}`
|
|
228
|
+
: '';
|
|
229
|
+
const volStr = formatVolumeJPY(item.volumeJPY);
|
|
230
|
+
const priceStr = item.price != null ? formatPriceJPY(item.price) : 'n/a';
|
|
231
|
+
lines.push(
|
|
232
|
+
`${item.rank}. ${emoji} ${item.currency} ${priceStr} score=${item.score} | 24h:${chgStr} | ${rsiStr} | ${smaStr} | 出来高${volStr}`,
|
|
233
|
+
);
|
|
234
|
+
}
|
|
235
|
+
if (strongBullish.length > 0) {
|
|
236
|
+
lines.push('');
|
|
237
|
+
lines.push(`🟢 注目(強気): ${strongBullish.join(', ')}`);
|
|
238
|
+
}
|
|
239
|
+
if (strongBearish.length > 0) {
|
|
240
|
+
lines.push(`🔴 注意(弱気): ${strongBearish.join(', ')}`);
|
|
241
|
+
}
|
|
242
|
+
lines.push('');
|
|
243
|
+
lines.push('---');
|
|
244
|
+
lines.push('📌 含まれるもの: 24h変化率・RSI・SMA乖離率に基づく相対強弱スコアと市場バイアス');
|
|
245
|
+
lines.push('📌 含まれないもの: 板情報・フロー分析・サポレジ・チャートパターン');
|
|
246
|
+
lines.push(
|
|
247
|
+
'📌 補完ツール: analyze_market_signal(個別銘柄の詳細分析), analyze_volume_profile(出来高プロファイル)',
|
|
248
|
+
);
|
|
249
|
+
|
|
250
|
+
const summaryText = lines.join('\n');
|
|
251
|
+
|
|
252
|
+
return AnalyzeCurrencyStrengthOutputSchema.parse(
|
|
253
|
+
ok(
|
|
254
|
+
summaryText,
|
|
255
|
+
data as z.infer<typeof AnalyzeCurrencyStrengthDataSchemaOut>,
|
|
256
|
+
meta as z.infer<typeof AnalyzeCurrencyStrengthMetaSchemaOut>,
|
|
257
|
+
),
|
|
258
|
+
);
|
|
259
|
+
} catch (e: unknown) {
|
|
260
|
+
return failFromError(e, { schema: AnalyzeCurrencyStrengthOutputSchema });
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
// ── ToolDef ──
|
|
265
|
+
|
|
266
|
+
export const toolDef: ToolDefinition = {
|
|
267
|
+
name: 'analyze_currency_strength',
|
|
268
|
+
description:
|
|
269
|
+
'[Currency Strength / Ranking / Screening] 通貨強弱ランキング(currency strength / relative strength / ranking / screening)。' +
|
|
270
|
+
'全JPYペアを複合スコア(変化率・RSI・SMA乖離・出来高)で判定。注目銘柄の発見・スクリーニングに。',
|
|
271
|
+
inputSchema: AnalyzeCurrencyStrengthInputSchema,
|
|
272
|
+
handler: async ({ topN, type }: { topN?: number; type?: string }) => analyzeCurrencyStrength(Number(topN), type),
|
|
273
|
+
};
|
|
@@ -0,0 +1,183 @@
|
|
|
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 AnalyzeEmaSnapshotDataSchemaOut,
|
|
22
|
+
AnalyzeEmaSnapshotInputSchema,
|
|
23
|
+
AnalyzeEmaSnapshotOutputSchema,
|
|
24
|
+
} from '../src/schemas.js';
|
|
25
|
+
import type { ToolDefinition } from '../src/tool-definition.js';
|
|
26
|
+
import analyzeIndicators, { ema } from './analyze_indicators.js';
|
|
27
|
+
import getCandles from './get_candles.js';
|
|
28
|
+
|
|
29
|
+
const FIXED_EMA_PERIODS = [12, 26, 50, 200] as const;
|
|
30
|
+
|
|
31
|
+
export type { CrossStatus, MaLineEntry, RecentCrossEntry };
|
|
32
|
+
|
|
33
|
+
export interface BuildEmaSnapshotTextInput {
|
|
34
|
+
baseSummary: string;
|
|
35
|
+
type: string;
|
|
36
|
+
maLines: MaLineEntry[];
|
|
37
|
+
crossStatuses: CrossStatus[];
|
|
38
|
+
recentCrosses: RecentCrossEntry[];
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const EMA_FOOTER = [
|
|
42
|
+
'📌 含まれるもの: EMA値・傾き・クロス状態・配列パターン・価格との乖離',
|
|
43
|
+
'📌 含まれないもの: SMA・RSI・MACD・BB・一目均衡表、出来高フロー、板情報',
|
|
44
|
+
'📌 補完ツール: analyze_sma_snapshot(SMA), analyze_indicators(他指標), analyze_bb_snapshot(BB), get_flow_metrics(出来高)',
|
|
45
|
+
];
|
|
46
|
+
|
|
47
|
+
/** テキスト組み立て(EMAスナップショット)— テスト可能な純粋関数 */
|
|
48
|
+
export function buildEmaSnapshotText(input: BuildEmaSnapshotTextInput): string {
|
|
49
|
+
return buildMaSnapshotText({ ...input, prefix: 'EMA', footerLines: EMA_FOOTER });
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export default async function analyzeEmaSnapshot(
|
|
53
|
+
pair: string = 'btc_jpy',
|
|
54
|
+
type: string = '1day',
|
|
55
|
+
limit: number = 220,
|
|
56
|
+
periods: number[] = [12, 26, 50, 200],
|
|
57
|
+
) {
|
|
58
|
+
const chk = ensurePair(pair);
|
|
59
|
+
if (!chk.ok) return failFromValidation(chk, AnalyzeEmaSnapshotOutputSchema);
|
|
60
|
+
try {
|
|
61
|
+
const maxPeriod = Math.max(...periods, 200);
|
|
62
|
+
const fetchLimit = Math.max(maxPeriod, limit);
|
|
63
|
+
|
|
64
|
+
const hasCustomPeriods = periods.some((p) => !(FIXED_EMA_PERIODS as readonly number[]).includes(p));
|
|
65
|
+
|
|
66
|
+
let close: number | null = null;
|
|
67
|
+
let chartInd: Record<string, unknown> = {};
|
|
68
|
+
let candles: Array<{ isoTime?: string | null }> = [];
|
|
69
|
+
let normalizedLen = 0;
|
|
70
|
+
const map: Record<string, number | null> = {};
|
|
71
|
+
|
|
72
|
+
if (hasCustomPeriods) {
|
|
73
|
+
const candlesResult = await getCandles(chk.pair, type, undefined, fetchLimit);
|
|
74
|
+
if (!candlesResult.ok)
|
|
75
|
+
return AnalyzeEmaSnapshotOutputSchema.parse(
|
|
76
|
+
fail(candlesResult.summary || 'candles failed', candlesResult.meta.errorType || 'internal'),
|
|
77
|
+
);
|
|
78
|
+
const normalized = candlesResult.data.normalized;
|
|
79
|
+
const allCloses = normalized.map((c) => c.close);
|
|
80
|
+
close = allCloses.at(-1) ?? null;
|
|
81
|
+
candles = normalized;
|
|
82
|
+
normalizedLen = normalized.length;
|
|
83
|
+
|
|
84
|
+
for (const p of periods) {
|
|
85
|
+
const series = ema(allCloses, p);
|
|
86
|
+
const key = `EMA_${p}`;
|
|
87
|
+
map[key] = series.at(-1) ?? null;
|
|
88
|
+
chartInd[key] = series;
|
|
89
|
+
}
|
|
90
|
+
} else {
|
|
91
|
+
const indRes = await analyzeIndicators(chk.pair, type, fetchLimit);
|
|
92
|
+
if (!indRes.ok)
|
|
93
|
+
return AnalyzeEmaSnapshotOutputSchema.parse(
|
|
94
|
+
fail(indRes.summary || 'indicators failed', indRes.meta.errorType || 'internal'),
|
|
95
|
+
);
|
|
96
|
+
close = indRes.data.normalized.at(-1)?.close ?? null;
|
|
97
|
+
chartInd = indRes?.data?.chart?.indicators ?? {};
|
|
98
|
+
candles = Array.isArray(indRes?.data?.chart?.candles)
|
|
99
|
+
? indRes.data.chart.candles
|
|
100
|
+
: Array.isArray(indRes?.data?.normalized)
|
|
101
|
+
? indRes.data.normalized
|
|
102
|
+
: [];
|
|
103
|
+
normalizedLen = indRes.data.normalized.length;
|
|
104
|
+
|
|
105
|
+
const indRecord = indRes.data.indicators as Record<string, number[] | number | null>;
|
|
106
|
+
for (const p of periods) {
|
|
107
|
+
const key = `EMA_${p}`;
|
|
108
|
+
map[key] = (indRecord[key] as number | null) ?? null;
|
|
109
|
+
if (!chartInd[key]) {
|
|
110
|
+
chartInd[key] = indRecord[`ema_${p}_series`] ?? [];
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
const crossPairs = generateCrossPairs(periods);
|
|
116
|
+
const crosses = detectCrossStatuses(crossPairs, map, 'EMA');
|
|
117
|
+
const recentCrosses = detectRecentCrosses(crossPairs, chartInd, candles, 'EMA');
|
|
118
|
+
|
|
119
|
+
const sorted = [...new Set(periods)].sort((a, b) => a - b);
|
|
120
|
+
const vals = sorted.map((p) => map[`EMA_${p}`]);
|
|
121
|
+
const alignment = detectAlignment(vals, { minPeriods: 3, strict: false });
|
|
122
|
+
|
|
123
|
+
const tags: string[] = [];
|
|
124
|
+
if (alignment === 'bullish') tags.push('ema_bullish_alignment');
|
|
125
|
+
if (alignment === 'bearish') tags.push('ema_bearish_alignment');
|
|
126
|
+
|
|
127
|
+
const emaVals = periods.map((p) => map[`EMA_${p}`]).filter((v): v is number => v != null);
|
|
128
|
+
const position = detectPosition(close, emaVals);
|
|
129
|
+
|
|
130
|
+
const emasExt: Record<string, MaExtEntry> = {};
|
|
131
|
+
for (const p of periods) {
|
|
132
|
+
const val = map[`EMA_${p}`];
|
|
133
|
+
const series = getSeries(chartInd, 'EMA', p);
|
|
134
|
+
emasExt[String(p)] = computeMaExt(close, val, series, type);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
const maLines = buildMaLines(periods, emasExt);
|
|
138
|
+
const summaryText = buildEmaSnapshotText({
|
|
139
|
+
baseSummary: formatSummary({
|
|
140
|
+
pair: chk.pair,
|
|
141
|
+
latest: close ?? undefined,
|
|
142
|
+
extra: `align=${alignment} pos=${position}`,
|
|
143
|
+
}),
|
|
144
|
+
type,
|
|
145
|
+
maLines,
|
|
146
|
+
crossStatuses: crosses,
|
|
147
|
+
recentCrosses,
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
const data: z.infer<typeof AnalyzeEmaSnapshotDataSchemaOut> = {
|
|
151
|
+
latest: { close },
|
|
152
|
+
ema: map,
|
|
153
|
+
crosses,
|
|
154
|
+
alignment,
|
|
155
|
+
tags,
|
|
156
|
+
summary: { close, align: alignment, position },
|
|
157
|
+
emas: emasExt,
|
|
158
|
+
recentCrosses,
|
|
159
|
+
};
|
|
160
|
+
const meta = createMeta(chk.pair, { type, count: normalizedLen, periods });
|
|
161
|
+
return AnalyzeEmaSnapshotOutputSchema.parse(ok(summaryText, data, meta));
|
|
162
|
+
} catch (e: unknown) {
|
|
163
|
+
return failFromError(e, { schema: AnalyzeEmaSnapshotOutputSchema });
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
export const toolDef: ToolDefinition = {
|
|
168
|
+
name: 'analyze_ema_snapshot',
|
|
169
|
+
description:
|
|
170
|
+
'[EMA / Exponential Moving Average] EMA(exponential moving average / trend / slope)の最新値・整列・クロス・傾きを返す(既定: 12/26/50/200)。\n\n⚠️ 最新値のみ。時系列チャート描画 → prepare_chart_data(indicators: ["EMA_12","EMA_26"] 等)。',
|
|
171
|
+
inputSchema: AnalyzeEmaSnapshotInputSchema,
|
|
172
|
+
handler: async ({
|
|
173
|
+
pair,
|
|
174
|
+
type,
|
|
175
|
+
limit,
|
|
176
|
+
periods,
|
|
177
|
+
}: {
|
|
178
|
+
pair?: string;
|
|
179
|
+
type?: string;
|
|
180
|
+
limit?: number;
|
|
181
|
+
periods?: number[];
|
|
182
|
+
}) => analyzeEmaSnapshot(pair, type, limit, periods),
|
|
183
|
+
};
|