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,145 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import { formatPercent, formatPrice, formatVolumeJPY } from '../../lib/formatter.js';
|
|
3
|
+
import getTickersJpy from '../../tools/get_tickers_jpy.js';
|
|
4
|
+
import type { ToolDefinition } from '../tool-definition.js';
|
|
5
|
+
|
|
6
|
+
export interface NormalizedTicker {
|
|
7
|
+
pair: string;
|
|
8
|
+
lastN: number | null;
|
|
9
|
+
openN: number | null;
|
|
10
|
+
highN: number | null;
|
|
11
|
+
lowN: number | null;
|
|
12
|
+
buyN: number | null;
|
|
13
|
+
sellN: number | null;
|
|
14
|
+
changeN: number | null;
|
|
15
|
+
volN: number | null;
|
|
16
|
+
volumeInJPY: number | null;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/** ranked ビューのテキスト組み立て — テスト可能な純粋関数 */
|
|
20
|
+
export function buildTickersJpyRankedText(
|
|
21
|
+
totalItems: number,
|
|
22
|
+
ranked: NormalizedTicker[],
|
|
23
|
+
sortBy: string,
|
|
24
|
+
order: string,
|
|
25
|
+
limit: number,
|
|
26
|
+
): string {
|
|
27
|
+
const lines = ranked.map((r, i) => {
|
|
28
|
+
const chg = formatPercent(r.changeN, { sign: true, digits: 2 });
|
|
29
|
+
const px = formatPrice(r.lastN);
|
|
30
|
+
const volTxt = formatVolumeJPY(r.volumeInJPY);
|
|
31
|
+
return `${i + 1}. ${String(r.pair).toUpperCase().replace('_', '/')} ${chg}(${px}、出来高${volTxt})`;
|
|
32
|
+
});
|
|
33
|
+
return [`全${totalItems}ペア取得(sortBy=${sortBy}, ${order}, top${limit})`, '', lines.join('\n')].join('\n');
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** items ビューのテキスト組み立て — テスト可能な純粋関数 */
|
|
37
|
+
export function buildTickersJpyItemsText(items: NormalizedTicker[]): string {
|
|
38
|
+
const lines: string[] = [];
|
|
39
|
+
lines.push(`全${items.length}ペア取得`);
|
|
40
|
+
lines.push('');
|
|
41
|
+
const top5 = items.slice(0, 5);
|
|
42
|
+
for (const it of top5) {
|
|
43
|
+
const pairDisplay = String(it.pair).toUpperCase().replace('_', '/');
|
|
44
|
+
const priceStr = formatPrice(it.lastN);
|
|
45
|
+
const changeStr = formatPercent(it.changeN, { sign: true, digits: 2 });
|
|
46
|
+
const volStr = formatVolumeJPY(it.volumeInJPY);
|
|
47
|
+
lines.push(`${pairDisplay}: ${priceStr} (${changeStr}) 出来高${volStr}`);
|
|
48
|
+
}
|
|
49
|
+
if (items.length > 5) {
|
|
50
|
+
lines.push(`... 他${items.length - 5}ペア`);
|
|
51
|
+
}
|
|
52
|
+
return lines.join('\n');
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const InputSchema = z.object({
|
|
56
|
+
view: z.enum(['items', 'ranked']).optional().default('ranked'),
|
|
57
|
+
sortBy: z.enum(['change24h', 'volume', 'name']).optional().default('change24h'),
|
|
58
|
+
order: z.enum(['asc', 'desc']).optional().default('desc'),
|
|
59
|
+
limit: z.number().int().min(1).max(50).optional().default(5),
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
export const toolDef: ToolDefinition = {
|
|
63
|
+
name: 'get_tickers_jpy',
|
|
64
|
+
description:
|
|
65
|
+
'[All Tickers / Market Overview] 全JPYペアのティッカー一覧(tickers / ranking / market overview)を取得。変化率・出来高でランキング表示可能。',
|
|
66
|
+
inputSchema: InputSchema,
|
|
67
|
+
handler: async (args: Record<string, unknown>) => {
|
|
68
|
+
const parsed = InputSchema.parse(args);
|
|
69
|
+
const { view, sortBy, order, limit } = parsed;
|
|
70
|
+
const res = await getTickersJpy();
|
|
71
|
+
if (!res?.ok) return res;
|
|
72
|
+
const items = (Array.isArray(res?.data) ? res.data : []) as Array<{
|
|
73
|
+
pair: string;
|
|
74
|
+
last?: unknown;
|
|
75
|
+
open?: unknown;
|
|
76
|
+
high?: unknown;
|
|
77
|
+
low?: unknown;
|
|
78
|
+
buy?: unknown;
|
|
79
|
+
sell?: unknown;
|
|
80
|
+
change24h?: unknown;
|
|
81
|
+
change24hPct?: unknown;
|
|
82
|
+
vol?: unknown;
|
|
83
|
+
[key: string]: unknown;
|
|
84
|
+
}>;
|
|
85
|
+
|
|
86
|
+
// normalize numeric fields(open/high/low 追加)
|
|
87
|
+
const norm = items.map((it) => {
|
|
88
|
+
const lastN = it?.last != null ? Number(it.last) : null;
|
|
89
|
+
const openN = it?.open != null ? Number(it.open) : null;
|
|
90
|
+
const highN = it?.high != null ? Number(it.high) : null;
|
|
91
|
+
const lowN = it?.low != null ? Number(it.low) : null;
|
|
92
|
+
const buyN = it?.buy != null ? Number(it.buy) : null;
|
|
93
|
+
const sellN = it?.sell != null ? Number(it.sell) : null;
|
|
94
|
+
const change = it?.change24h ?? it?.change24hPct;
|
|
95
|
+
const changeN =
|
|
96
|
+
change != null
|
|
97
|
+
? Number(change)
|
|
98
|
+
: openN != null && openN > 0 && lastN != null
|
|
99
|
+
? Number((((lastN - openN) / openN) * 100).toFixed(2))
|
|
100
|
+
: null;
|
|
101
|
+
const volN = it?.vol != null ? Number(it.vol) : null;
|
|
102
|
+
const volumeInJPY =
|
|
103
|
+
volN != null && lastN != null && Number.isFinite(volN) && Number.isFinite(lastN) ? volN * lastN : null;
|
|
104
|
+
return { ...it, lastN, openN, highN, lowN, buyN, sellN, changeN, volN, volumeInJPY };
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
// ranking logic
|
|
108
|
+
const cmpNum = (a?: number | null, b?: number | null) => {
|
|
109
|
+
const aa = a == null || Number.isNaN(a) ? -Infinity : a;
|
|
110
|
+
const bb = b == null || Number.isNaN(b) ? -Infinity : b;
|
|
111
|
+
return aa - bb;
|
|
112
|
+
};
|
|
113
|
+
const sorted = [...norm].sort((a, b) => {
|
|
114
|
+
if (sortBy === 'name') {
|
|
115
|
+
return String(a.pair).localeCompare(String(b.pair));
|
|
116
|
+
}
|
|
117
|
+
if (sortBy === 'volume') {
|
|
118
|
+
return cmpNum(a.volumeInJPY, b.volumeInJPY);
|
|
119
|
+
}
|
|
120
|
+
return cmpNum(a.changeN, b.changeN);
|
|
121
|
+
});
|
|
122
|
+
if ((order || 'desc') === 'desc') sorted.reverse();
|
|
123
|
+
const ranked = sorted.slice(0, Number(limit || 5));
|
|
124
|
+
|
|
125
|
+
if (view === 'ranked') {
|
|
126
|
+
const text = buildTickersJpyRankedText(items.length, ranked, sortBy, order, limit);
|
|
127
|
+
return {
|
|
128
|
+
content: [{ type: 'text', text }],
|
|
129
|
+
structuredContent: {
|
|
130
|
+
ok: true,
|
|
131
|
+
summary: `ranked ${ranked.length}/${items.length}`,
|
|
132
|
+
data: { items: norm, ranked },
|
|
133
|
+
meta: res?.meta ?? {},
|
|
134
|
+
} as Record<string, unknown>,
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// view=items: 全データ一覧(上位5件をサマリ表示)
|
|
139
|
+
const text = buildTickersJpyItemsText(norm);
|
|
140
|
+
return {
|
|
141
|
+
content: [{ type: 'text', text }],
|
|
142
|
+
structuredContent: { ...res, data: { items: norm } } as Record<string, unknown>,
|
|
143
|
+
};
|
|
144
|
+
},
|
|
145
|
+
};
|
|
@@ -0,0 +1,234 @@
|
|
|
1
|
+
import type { z } from 'zod';
|
|
2
|
+
import { toIsoTime } from '../../lib/datetime.js';
|
|
3
|
+
import {
|
|
4
|
+
formatCurrency,
|
|
5
|
+
formatCurrencyShort,
|
|
6
|
+
formatPercent,
|
|
7
|
+
formatPriceJPY,
|
|
8
|
+
formatTrendArrow,
|
|
9
|
+
} from '../../lib/formatter.js';
|
|
10
|
+
import { stddev } from '../../lib/math.js';
|
|
11
|
+
import getVolatilityMetrics from '../../tools/get_volatility_metrics.js';
|
|
12
|
+
import { GetVolMetricsInputSchema } from '../schemas.js';
|
|
13
|
+
import type { ToolDefinition } from '../tool-definition.js';
|
|
14
|
+
|
|
15
|
+
export interface VolViewInput {
|
|
16
|
+
pair: string;
|
|
17
|
+
type: string;
|
|
18
|
+
lastClose: number | null;
|
|
19
|
+
ann: boolean;
|
|
20
|
+
annFactor: number;
|
|
21
|
+
annFactorFull: number;
|
|
22
|
+
sampleSize: number | string;
|
|
23
|
+
rvAnn: number | null;
|
|
24
|
+
pkAnn: number | null;
|
|
25
|
+
gkAnn: number | null;
|
|
26
|
+
rsAnn: number | null;
|
|
27
|
+
atrAbs: number | null;
|
|
28
|
+
atrPct: number | null;
|
|
29
|
+
tagsAll: string[];
|
|
30
|
+
rolling: Array<{
|
|
31
|
+
window: number;
|
|
32
|
+
rv_std: number;
|
|
33
|
+
rv_std_ann?: number;
|
|
34
|
+
atr?: number;
|
|
35
|
+
parkinson?: number;
|
|
36
|
+
garmanKlass?: number;
|
|
37
|
+
rogersSatchell?: number;
|
|
38
|
+
}>;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/** beginner ビューのテキスト組み立て */
|
|
42
|
+
export function buildVolatilityBeginnerText(input: VolViewInput): string {
|
|
43
|
+
const { pair, type, lastClose, atrAbs, atrPct, rvAnn, tagsAll } = input;
|
|
44
|
+
const rvPct = formatPercent(rvAnn, { multiply: true, digits: 0 });
|
|
45
|
+
const atrJpy = formatPriceJPY(atrAbs);
|
|
46
|
+
const atrPctStr = formatPercent(atrPct, { multiply: true });
|
|
47
|
+
const closeStr = formatPriceJPY(lastClose);
|
|
48
|
+
return [
|
|
49
|
+
`${String(pair).toUpperCase()} [${String(type)}] 現在価格: ${closeStr}`,
|
|
50
|
+
`・年間のおおよその動き: 約${rvPct}(1年でこのくらい上下しやすい目安)`,
|
|
51
|
+
`・1日の平均的な動き: 約${atrJpy}(約${atrPctStr})`,
|
|
52
|
+
tagsAll.length ? `・今の傾向: ${tagsAll.map((t) => t.replaceAll('_', ' ')).join(', ')}` : null,
|
|
53
|
+
]
|
|
54
|
+
.filter(Boolean)
|
|
55
|
+
.join('\n');
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/** summary ビューのテキスト組み立て */
|
|
59
|
+
export function buildVolatilitySummaryText(input: VolViewInput): string {
|
|
60
|
+
const { pair, type, sampleSize, rvAnn, pkAnn, gkAnn, rsAnn, atrAbs, tagsAll } = input;
|
|
61
|
+
const fp = (x: number | null) => formatPercent(x, { multiply: true });
|
|
62
|
+
const fmtCurrShort = (p: string, v: number | null) => formatCurrencyShort(v, p);
|
|
63
|
+
return `${String(pair).toUpperCase()} [${String(type)}] samples=${sampleSize ?? 'n/a'} RV=${fp(rvAnn)} ATR=${fmtCurrShort(pair, atrAbs)} PK=${fp(pkAnn)} GK=${fp(gkAnn)} RS=${fp(rsAnn)} Tags: ${tagsAll.join(', ')}`;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export interface VolDetailedInput extends VolViewInput {
|
|
67
|
+
series?: {
|
|
68
|
+
ts: number[];
|
|
69
|
+
close: number[];
|
|
70
|
+
ret: number[];
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/** detailed/full ビューのテキスト組み立て */
|
|
75
|
+
export function buildVolatilityDetailedText(input: VolDetailedInput, view: 'detailed' | 'full'): string {
|
|
76
|
+
const { pair, type, lastClose, ann, annFactor, sampleSize, rvAnn, pkAnn, gkAnn, rsAnn, atrAbs, tagsAll, rolling } =
|
|
77
|
+
input;
|
|
78
|
+
const fp = (x: number | null | undefined) => formatPercent(x, { multiply: true });
|
|
79
|
+
const fmtCurr = (p: string, v: number | null) => formatCurrency(v, p);
|
|
80
|
+
|
|
81
|
+
const windowsList = rolling.map((r) => r.window).join('/');
|
|
82
|
+
const header = `${String(pair).toUpperCase()} [${String(type)}] close=${lastClose != null ? Number(lastClose).toLocaleString('ja-JP') : 'n/a'}\n`;
|
|
83
|
+
const block1 = `【Volatility Metrics${ann ? ' (annualized)' : ''}, ${sampleSize ?? 'n/a'} samples】\nRV (std): ${fp(rvAnn)}\nATR: ${fmtCurr(pair, atrAbs)}\nParkinson: ${fp(pkAnn)}\nGarman-Klass: ${fp(gkAnn)}\nRogers-Satchell: ${fp(rsAnn)}`;
|
|
84
|
+
|
|
85
|
+
const maxW = rolling.length ? Math.max(...rolling.map((r) => r.window)) : null;
|
|
86
|
+
const baseVal =
|
|
87
|
+
maxW != null
|
|
88
|
+
? (rolling.find((r) => r.window === maxW)?.rv_std_ann ??
|
|
89
|
+
((rolling.find((r) => r.window === maxW)?.rv_std ?? null) as number) * (ann ? annFactor : 1))
|
|
90
|
+
: null;
|
|
91
|
+
const trendLines = rolling.map((r) => {
|
|
92
|
+
const now = r.rv_std_ann ?? (r.rv_std != null ? r.rv_std * (ann ? annFactor : 1) : null);
|
|
93
|
+
return `${r.window}-day RV: ${fp(now)} ${formatTrendArrow(now, baseVal)}`;
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
let text =
|
|
97
|
+
header +
|
|
98
|
+
'\n' +
|
|
99
|
+
block1 +
|
|
100
|
+
'\n\n' +
|
|
101
|
+
`【Rolling Trends (${windowsList}-day windows)】\n` +
|
|
102
|
+
trendLines.join('\n') +
|
|
103
|
+
'\n\n' +
|
|
104
|
+
`【Assessment】\nTags: ${tagsAll.join(', ')}`;
|
|
105
|
+
|
|
106
|
+
if (view === 'full' && input.series) {
|
|
107
|
+
const { ts: tsArr, close: cArr, ret: retArr } = input.series;
|
|
108
|
+
const firstIso = tsArr.length ? (toIsoTime(tsArr[0]) ?? 'n/a') : 'n/a';
|
|
109
|
+
const lastIso = tsArr.length ? (toIsoTime(tsArr[tsArr.length - 1]) ?? 'n/a') : 'n/a';
|
|
110
|
+
const minClose = cArr.length ? Math.min(...cArr) : null;
|
|
111
|
+
const maxClose = cArr.length ? Math.max(...cArr) : null;
|
|
112
|
+
const mean = retArr.length ? retArr.reduce((s, v) => s + v, 0) / retArr.length : null;
|
|
113
|
+
const std = retArr.length ? stddev(retArr) : null;
|
|
114
|
+
text += `\n\n【Series】\nTotal: ${sampleSize ?? cArr.length} candles\nFirst: ${firstIso} , Last: ${lastIso}\nClose range: ${minClose != null ? Number(minClose).toLocaleString('ja-JP') : 'n/a'} - ${maxClose != null ? Number(maxClose).toLocaleString('ja-JP') : 'n/a'} JPY\nReturns: mean=${formatPercent(mean, { multiply: true, digits: 2 })}, std=${formatPercent(std, { multiply: true, digits: 2 })}${ann ? ' (base interval)' : ''}`;
|
|
115
|
+
}
|
|
116
|
+
return text;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
export const toolDef: ToolDefinition = {
|
|
120
|
+
name: 'get_volatility_metrics',
|
|
121
|
+
description:
|
|
122
|
+
'[Volatility / ATR / RV] ボラティリティ指標(volatility / ATR / realized vol)を算出。RV・ATR・Parkinson・Garman-Klass・Rogers-Satchell。年率換算対応。',
|
|
123
|
+
inputSchema: GetVolMetricsInputSchema,
|
|
124
|
+
handler: async ({
|
|
125
|
+
pair,
|
|
126
|
+
type,
|
|
127
|
+
limit,
|
|
128
|
+
windows,
|
|
129
|
+
useLogReturns,
|
|
130
|
+
annualize,
|
|
131
|
+
view,
|
|
132
|
+
}: z.infer<typeof GetVolMetricsInputSchema>) => {
|
|
133
|
+
const res = await getVolatilityMetrics(pair, type, limit, windows, { useLogReturns, annualize });
|
|
134
|
+
if (!res?.ok) return res;
|
|
135
|
+
const meta = res?.data?.meta || {};
|
|
136
|
+
const a = res?.data?.aggregates || {};
|
|
137
|
+
const roll: VolViewInput['rolling'] = Array.isArray(res?.data?.rolling) ? res.data.rolling : [];
|
|
138
|
+
const closeSeries: number[] = Array.isArray(res?.data?.series?.close) ? res.data.series.close : [];
|
|
139
|
+
const lastClose = closeSeries.at(-1) ?? null;
|
|
140
|
+
const ann = !!meta.annualize;
|
|
141
|
+
const baseMs = Number(meta.baseIntervalMs ?? 0);
|
|
142
|
+
const annFactorFull = baseMs > 0 ? Math.sqrt((365 * 24 * 3600 * 1000) / baseMs) : 1;
|
|
143
|
+
const annFactor = ann ? annFactorFull : 1;
|
|
144
|
+
const rvAnn = a.rv_std_ann != null ? a.rv_std_ann : a.rv_std != null ? a.rv_std * annFactor : null;
|
|
145
|
+
const pkAnn = a.parkinson != null ? a.parkinson * (ann ? annFactor : 1) : null;
|
|
146
|
+
const gkAnn = a.garmanKlass != null ? a.garmanKlass * (ann ? annFactor : 1) : null;
|
|
147
|
+
const rsAnn = a.rogersSatchell != null ? a.rogersSatchell * (ann ? annFactor : 1) : null;
|
|
148
|
+
const atrAbs = a.atr != null ? a.atr : null;
|
|
149
|
+
const atrPct = lastClose ? (atrAbs as number) / lastClose : null;
|
|
150
|
+
|
|
151
|
+
// tags: base + derived
|
|
152
|
+
const tagsBase: string[] = Array.isArray(res?.data?.tags) ? [...res.data.tags] : [];
|
|
153
|
+
const tagsDerived: string[] = [];
|
|
154
|
+
// Always use annualized values for tag thresholds (consistent regardless of annualize flag)
|
|
155
|
+
if (Array.isArray(roll) && roll.length >= 2) {
|
|
156
|
+
const minW = Math.min(...roll.map((r) => r.window));
|
|
157
|
+
const maxW = Math.max(...roll.map((r) => r.window));
|
|
158
|
+
const short = roll.find((r) => r.window === minW);
|
|
159
|
+
const long = roll.find((r) => r.window === maxW);
|
|
160
|
+
const shortVal = short
|
|
161
|
+
? (short.rv_std_ann ?? (short.rv_std != null ? short.rv_std * annFactorFull : null))
|
|
162
|
+
: null;
|
|
163
|
+
const longVal = long ? (long.rv_std_ann ?? (long.rv_std != null ? long.rv_std * annFactorFull : null)) : null;
|
|
164
|
+
if (shortVal != null && longVal != null) {
|
|
165
|
+
if (shortVal > longVal * 1.05) tagsDerived.push('expanding_vol');
|
|
166
|
+
else if (shortVal < longVal * 0.95) tagsDerived.push('contracting_vol');
|
|
167
|
+
if (shortVal > 0.4) tagsDerived.push('high_short_term_vol');
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
// Use annualized RV for threshold comparison even when annualize=false
|
|
171
|
+
const rvAnnForTags = a.rv_std_ann ?? (a.rv_std != null ? a.rv_std * annFactorFull : null);
|
|
172
|
+
if (rvAnnForTags != null) {
|
|
173
|
+
if (rvAnnForTags > 0.5) tagsDerived.push('high_vol');
|
|
174
|
+
if (rvAnnForTags < 0.2) tagsDerived.push('low_vol');
|
|
175
|
+
}
|
|
176
|
+
if (rvAnn != null && atrPct != null && rvAnn > 0) {
|
|
177
|
+
const diff = Math.abs(atrPct - rvAnn) / rvAnn;
|
|
178
|
+
if (diff > 0.2) tagsDerived.push('atr_divergence');
|
|
179
|
+
}
|
|
180
|
+
const tagsAll = [...new Set([...(tagsBase || []), ...tagsDerived])];
|
|
181
|
+
|
|
182
|
+
const viewInput: VolViewInput = {
|
|
183
|
+
pair,
|
|
184
|
+
type,
|
|
185
|
+
lastClose,
|
|
186
|
+
ann,
|
|
187
|
+
annFactor,
|
|
188
|
+
annFactorFull,
|
|
189
|
+
sampleSize: meta.sampleSize ?? 'n/a',
|
|
190
|
+
rvAnn,
|
|
191
|
+
pkAnn,
|
|
192
|
+
gkAnn,
|
|
193
|
+
rsAnn,
|
|
194
|
+
atrAbs,
|
|
195
|
+
atrPct,
|
|
196
|
+
tagsAll,
|
|
197
|
+
rolling: roll,
|
|
198
|
+
};
|
|
199
|
+
|
|
200
|
+
// beginner view (plain language for non-experts)
|
|
201
|
+
if (view === 'beginner') {
|
|
202
|
+
const text = buildVolatilityBeginnerText(viewInput);
|
|
203
|
+
return {
|
|
204
|
+
content: [{ type: 'text', text }],
|
|
205
|
+
structuredContent: { ...res, data: { ...res.data, tags: tagsAll } } as Record<string, unknown>,
|
|
206
|
+
};
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
// summary view
|
|
210
|
+
if (view === 'summary') {
|
|
211
|
+
const text = buildVolatilitySummaryText(viewInput);
|
|
212
|
+
return {
|
|
213
|
+
content: [{ type: 'text', text }],
|
|
214
|
+
structuredContent: { ...res, data: { ...res.data, tags: tagsAll } } as Record<string, unknown>,
|
|
215
|
+
};
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
// detailed/full
|
|
219
|
+
const series = res?.data?.series || {};
|
|
220
|
+
const detailedInput: VolDetailedInput = {
|
|
221
|
+
...viewInput,
|
|
222
|
+
series: {
|
|
223
|
+
ts: Array.isArray(series.ts) ? series.ts : [],
|
|
224
|
+
close: Array.isArray(series.close) ? series.close : [],
|
|
225
|
+
ret: Array.isArray(series.ret) ? series.ret : [],
|
|
226
|
+
},
|
|
227
|
+
};
|
|
228
|
+
const text = buildVolatilityDetailedText(detailedInput, view === 'full' ? 'full' : 'detailed');
|
|
229
|
+
return {
|
|
230
|
+
content: [{ type: 'text', text }],
|
|
231
|
+
structuredContent: { ...res, data: { ...res.data, tags: tagsAll } } as Record<string, unknown>,
|
|
232
|
+
};
|
|
233
|
+
},
|
|
234
|
+
};
|