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,424 @@
|
|
|
1
|
+
import type { z } from 'zod';
|
|
2
|
+
import { dayjs } from '../lib/datetime.js';
|
|
3
|
+
import { fail, failFromError, ok } from '../lib/result.js';
|
|
4
|
+
import { DetectPatternsOutputSchema, type PatternTypeEnum } from '../src/schemas.js';
|
|
5
|
+
import analyzeIndicators from './analyze_indicators.js';
|
|
6
|
+
import { buildStatistics } from './patterns/aftermath.js';
|
|
7
|
+
import { resolveParams } from './patterns/config.js';
|
|
8
|
+
// --- 各パターン検出モジュール ---
|
|
9
|
+
import { detectDoubles } from './patterns/detect_doubles.js';
|
|
10
|
+
import { detectHeadAndShoulders } from './patterns/detect_hs.js';
|
|
11
|
+
import { detectPennantsFlags } from './patterns/detect_pennants.js';
|
|
12
|
+
import { detectTriangles } from './patterns/detect_triangles.js';
|
|
13
|
+
import { detectTriples } from './patterns/detect_triples.js';
|
|
14
|
+
import { detectWedges } from './patterns/detect_wedges.js';
|
|
15
|
+
import { globalDedup } from './patterns/helpers.js';
|
|
16
|
+
import { linearRegressionWithR2, near as nearFn, pct as pctFn } from './patterns/regression.js';
|
|
17
|
+
import { type Candle, detectSwingPoints, filterPeaks, filterValleys } from './patterns/swing.js';
|
|
18
|
+
import type { CandDebugEntry, DeduplicablePattern, DetectContext } from './patterns/types.js';
|
|
19
|
+
|
|
20
|
+
/** Summary generation section で使う拡張型(DeduplicablePattern + パターン固有フィールド) */
|
|
21
|
+
interface SummaryPattern extends DeduplicablePattern {
|
|
22
|
+
type: string;
|
|
23
|
+
confidence: number;
|
|
24
|
+
range: { start: string; end: string; current?: string };
|
|
25
|
+
status?: string;
|
|
26
|
+
breakoutDirection?: string;
|
|
27
|
+
outcome?: string;
|
|
28
|
+
neckline?: Array<{ y?: number }>;
|
|
29
|
+
trendlineLabel?: string;
|
|
30
|
+
daysToApex?: number;
|
|
31
|
+
breakoutTarget?: number;
|
|
32
|
+
targetMethod?: string;
|
|
33
|
+
targetReachedPct?: number;
|
|
34
|
+
poleDirection?: string;
|
|
35
|
+
priorTrendDirection?: string;
|
|
36
|
+
flagpoleHeight?: number;
|
|
37
|
+
retracementRatio?: number;
|
|
38
|
+
isTrendContinuation?: boolean;
|
|
39
|
+
timeframe?: string;
|
|
40
|
+
timeframeLabel?: string;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* detect_patterns - チャートパターン検出(完成済み+形成中)
|
|
45
|
+
*
|
|
46
|
+
* 設計思想:
|
|
47
|
+
* - 目的: チャートパターンを検出し、統計的に信頼性の高いデータを提供
|
|
48
|
+
* - 特徴: swingDepth パラメータによる厳密なスイング検出でパターン品質を重視
|
|
49
|
+
* - ブレイク検出: ATR * 0.5 バッファ、最初の明確なブレイクで終点を確定
|
|
50
|
+
* - 用途: 「過去の成功率は?」「典型的な期間は?」「aftermath は?」
|
|
51
|
+
*
|
|
52
|
+
* オプション:
|
|
53
|
+
* - includeCompleted: true (デフォルト) → 完成済みパターンを検出
|
|
54
|
+
* - includeForming: true → 形成中パターンも検出(早期警告向け)
|
|
55
|
+
*/
|
|
56
|
+
|
|
57
|
+
export default async function detectPatterns(
|
|
58
|
+
pair: string = 'btc_jpy',
|
|
59
|
+
type: string = '1day',
|
|
60
|
+
limit: number = 90,
|
|
61
|
+
opts: Partial<{
|
|
62
|
+
swingDepth: number;
|
|
63
|
+
tolerancePct: number;
|
|
64
|
+
minBarsBetweenSwings: number;
|
|
65
|
+
strictPivots: boolean;
|
|
66
|
+
patterns: Array<z.infer<typeof PatternTypeEnum>>;
|
|
67
|
+
requireCurrentInPattern: boolean;
|
|
68
|
+
currentRelevanceDays: number;
|
|
69
|
+
// 統合オプション
|
|
70
|
+
includeForming: boolean;
|
|
71
|
+
includeCompleted: boolean;
|
|
72
|
+
includeInvalid: boolean;
|
|
73
|
+
view: 'summary' | 'detailed' | 'full' | 'debug';
|
|
74
|
+
}> = {},
|
|
75
|
+
) {
|
|
76
|
+
try {
|
|
77
|
+
// --- パラメータ解決(patterns/config.ts から) ---
|
|
78
|
+
const { swingDepth, tolerancePct, minBarsBetweenSwings: minDist, autoScaled } = resolveParams(type, opts);
|
|
79
|
+
const strictPivots = opts.strictPivots !== false; // 既定: 厳格
|
|
80
|
+
// 統合オプション
|
|
81
|
+
const includeForming = opts.includeForming ?? false;
|
|
82
|
+
const includeCompleted = opts.includeCompleted ?? true;
|
|
83
|
+
const includeInvalid = opts.includeInvalid ?? false;
|
|
84
|
+
const want = new Set(opts.patterns || []);
|
|
85
|
+
// 'triangle' が指定された場合は3種を含む互換挙動
|
|
86
|
+
if (want.has('triangle')) {
|
|
87
|
+
want.add('triangle_ascending');
|
|
88
|
+
want.add('triangle_descending');
|
|
89
|
+
want.add('triangle_symmetrical');
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
const res = await analyzeIndicators(pair, type, limit);
|
|
93
|
+
if (!res.ok) return DetectPatternsOutputSchema.parse(fail(res.summary || 'failed', 'internal'));
|
|
94
|
+
|
|
95
|
+
const candles = res.data.chart.candles as Array<{
|
|
96
|
+
open: number;
|
|
97
|
+
close: number;
|
|
98
|
+
high: number;
|
|
99
|
+
low: number;
|
|
100
|
+
isoTime?: string;
|
|
101
|
+
}>;
|
|
102
|
+
if (!Array.isArray(candles) || candles.length < 20) {
|
|
103
|
+
return DetectPatternsOutputSchema.parse(ok('insufficient data', { patterns: [] }, { pair, type, count: 0 }));
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// 1) Swing points(patterns/swing.ts から)
|
|
107
|
+
const pivots = detectSwingPoints(candles as Candle[], { swingDepth, strictPivots });
|
|
108
|
+
|
|
109
|
+
// debug buffers
|
|
110
|
+
const debugSwings = pivots.map((p) => ({
|
|
111
|
+
idx: p.idx,
|
|
112
|
+
price: p.price,
|
|
113
|
+
kind: p.kind,
|
|
114
|
+
isoTime: candles[p.idx]?.isoTime,
|
|
115
|
+
}));
|
|
116
|
+
const debugCandidates: CandDebugEntry[] = [];
|
|
117
|
+
|
|
118
|
+
// --- 共有コンテキスト構築 ---
|
|
119
|
+
const ctx: DetectContext = {
|
|
120
|
+
candles,
|
|
121
|
+
pivots,
|
|
122
|
+
allPeaks: filterPeaks(pivots),
|
|
123
|
+
allValleys: filterValleys(pivots),
|
|
124
|
+
tolerancePct,
|
|
125
|
+
minDist,
|
|
126
|
+
want,
|
|
127
|
+
includeForming,
|
|
128
|
+
debugCandidates,
|
|
129
|
+
type,
|
|
130
|
+
swingDepth,
|
|
131
|
+
near: (a: number, b: number) => nearFn(a, b, tolerancePct),
|
|
132
|
+
pct: pctFn,
|
|
133
|
+
lrWithR2: linearRegressionWithR2,
|
|
134
|
+
};
|
|
135
|
+
|
|
136
|
+
// --- 各パターン検出を実行 ---
|
|
137
|
+
let patterns: DeduplicablePattern[] = [];
|
|
138
|
+
|
|
139
|
+
// 2) Double top/bottom
|
|
140
|
+
const doubles = detectDoubles(ctx);
|
|
141
|
+
patterns.push(...doubles.patterns);
|
|
142
|
+
|
|
143
|
+
// 3) Head & Shoulders
|
|
144
|
+
const hs = detectHeadAndShoulders(ctx);
|
|
145
|
+
patterns.push(...hs.patterns);
|
|
146
|
+
|
|
147
|
+
// 4) Triangles + Pennant (Trendoscope 2-stage: triangle → pole check → pennant reclassification)
|
|
148
|
+
const triangles = detectTriangles(ctx);
|
|
149
|
+
patterns.push(...triangles.patterns);
|
|
150
|
+
|
|
151
|
+
// 4b-4d) Wedges
|
|
152
|
+
const wedges = detectWedges(ctx);
|
|
153
|
+
patterns.push(...wedges.patterns);
|
|
154
|
+
|
|
155
|
+
// 5) Flag detection (parallel channel with pole; pennant is now handled by detectTriangles)
|
|
156
|
+
const flags = detectPennantsFlags(ctx);
|
|
157
|
+
patterns.push(...flags.patterns);
|
|
158
|
+
|
|
159
|
+
// 6) Triple Top / Triple Bottom
|
|
160
|
+
const triples = detectTriples(ctx);
|
|
161
|
+
patterns.push(...triples.patterns);
|
|
162
|
+
|
|
163
|
+
// グローバル重複排除: 全パターン種別横断で期間が70%以上重複する同一タイプを統合
|
|
164
|
+
patterns = globalDedup(patterns);
|
|
165
|
+
|
|
166
|
+
// Optional filter: only patterns whose end is within N days from now (current relevance)
|
|
167
|
+
{
|
|
168
|
+
const requireCurrent = !!opts.requireCurrentInPattern;
|
|
169
|
+
const defaultDaysByType = (tf: string): number => {
|
|
170
|
+
if (tf === '1month') return 60; // ~2 months
|
|
171
|
+
if (tf === '1week') return 21; // ~3 weeks
|
|
172
|
+
return 7; // default for daily and intraday
|
|
173
|
+
};
|
|
174
|
+
const maxAgeDays = Number.isFinite(opts.currentRelevanceDays)
|
|
175
|
+
? Number(opts.currentRelevanceDays)
|
|
176
|
+
: defaultDaysByType(String(type));
|
|
177
|
+
if (requireCurrent && patterns.length) {
|
|
178
|
+
const nowMs = Date.now();
|
|
179
|
+
const inDays = (iso?: string) => {
|
|
180
|
+
if (!iso) return Infinity;
|
|
181
|
+
const t = Date.parse(iso);
|
|
182
|
+
if (!Number.isFinite(t)) return Infinity;
|
|
183
|
+
return Math.abs(nowMs - t) / 86400000;
|
|
184
|
+
};
|
|
185
|
+
patterns = patterns.filter((p) => inDays(p?.range?.end) <= maxAgeDays);
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
// Aftermath analysis + statistics(patterns/aftermath.ts へ抽出済み)
|
|
190
|
+
const { statistics } = buildStatistics(patterns, candles);
|
|
191
|
+
|
|
192
|
+
// includeForming / includeCompleted に基づくフィルタリング
|
|
193
|
+
let filteredPatterns = patterns;
|
|
194
|
+
if (!includeForming || !includeCompleted) {
|
|
195
|
+
filteredPatterns = patterns.filter((p) => {
|
|
196
|
+
const isForming = p.status === 'forming' || p.status === 'near_completion';
|
|
197
|
+
const isCompleted = p.status === 'completed' || p.status === 'invalid' || !p.status;
|
|
198
|
+
if (includeForming && isForming) return true;
|
|
199
|
+
if (includeCompleted && isCompleted) return true;
|
|
200
|
+
return false;
|
|
201
|
+
});
|
|
202
|
+
}
|
|
203
|
+
// includeInvalid に基づくフィルタリング
|
|
204
|
+
if (!includeInvalid) {
|
|
205
|
+
filteredPatterns = filteredPatterns.filter((p) => p.status !== 'invalid');
|
|
206
|
+
}
|
|
207
|
+
patterns = filteredPatterns;
|
|
208
|
+
|
|
209
|
+
// 時間足ラベル(各パターンに注入 + summary 用)
|
|
210
|
+
const tfMap: Record<string, string> = {
|
|
211
|
+
'1min': '1分足',
|
|
212
|
+
'5min': '5分足',
|
|
213
|
+
'15min': '15分足',
|
|
214
|
+
'30min': '30分足',
|
|
215
|
+
'1hour': '1時間足',
|
|
216
|
+
'4hour': '4時間足',
|
|
217
|
+
'8hour': '8時間足',
|
|
218
|
+
'12hour': '12時間足',
|
|
219
|
+
'1day': '日足',
|
|
220
|
+
'1week': '週足',
|
|
221
|
+
'1month': '月足',
|
|
222
|
+
};
|
|
223
|
+
const tfLabel = tfMap[String(type)] || String(type);
|
|
224
|
+
|
|
225
|
+
// 全パターンに timeframe / timeframeLabel を付与(LLM が個別パターンから時間足を即座に読み取れるようにする)
|
|
226
|
+
for (const p of patterns) {
|
|
227
|
+
p.timeframe = String(type);
|
|
228
|
+
p.timeframeLabel = tfLabel;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
// --- ここから先は SummaryPattern として扱う(検出モジュールが付与した固有フィールドにアクセスするため) ---
|
|
232
|
+
const summaryPatterns = patterns as SummaryPattern[];
|
|
233
|
+
|
|
234
|
+
// overlays: パターン範囲をそのまま帯描画できるように提供
|
|
235
|
+
const ranges = summaryPatterns.map((p) => ({ start: p.range.start, end: p.range.end, label: p.type }));
|
|
236
|
+
const warnings: Array<{ type: string; message: string; suggestedParams?: Record<string, unknown> }> = [];
|
|
237
|
+
if (patterns.length <= 1) {
|
|
238
|
+
warnings.push({
|
|
239
|
+
type: 'low_detection_count',
|
|
240
|
+
message: '検出数が少ないです。tolerancePct や minBarsBetweenSwings の調整を推奨します',
|
|
241
|
+
suggestedParams: { tolerancePct: 0.03, minBarsBetweenSwings: 2 },
|
|
242
|
+
});
|
|
243
|
+
}
|
|
244
|
+
// --- サイズ抑制: debug 配列を上限でトリム(view未指定で返却が肥大化しやすいため) ---
|
|
245
|
+
// ただし accepted を優先的に残す(accepted → rejected の順で cap まで)
|
|
246
|
+
const cap = 200;
|
|
247
|
+
const swingsTrimmed = Array.isArray(debugSwings) ? debugSwings.slice(0, cap) : [];
|
|
248
|
+
let candidatesTrimmed: CandDebugEntry[] = [];
|
|
249
|
+
if (Array.isArray(debugCandidates)) {
|
|
250
|
+
const acc = debugCandidates.filter((c) => !!c?.accepted);
|
|
251
|
+
const rej = debugCandidates.filter((c) => !c?.accepted);
|
|
252
|
+
candidatesTrimmed = [...acc, ...rej].slice(0, cap);
|
|
253
|
+
}
|
|
254
|
+
const debugTrimmed = {
|
|
255
|
+
swings: swingsTrimmed,
|
|
256
|
+
candidates: candidatesTrimmed,
|
|
257
|
+
};
|
|
258
|
+
|
|
259
|
+
// summary 生成: LLM が content から読み取れるように詳細を含める
|
|
260
|
+
const patternSummaries = summaryPatterns
|
|
261
|
+
.map((p, idx) => {
|
|
262
|
+
const startDate = p.range?.start?.substring(0, 10) || '?';
|
|
263
|
+
const endDate = p.range?.end?.substring(0, 10) || '?';
|
|
264
|
+
let detail = `${idx + 1}. ${p.type}【${tfLabel}】(パターン整合度: ${p.confidence})\n - 時間足: ${tfLabel}(${type})\n - 期間: ${startDate} ~ ${endDate}`;
|
|
265
|
+
|
|
266
|
+
// status(全パターン共通)
|
|
267
|
+
if (p.status) {
|
|
268
|
+
const statusJa: Record<string, string> = {
|
|
269
|
+
completed: '完成(ブレイクアウト確認済み)',
|
|
270
|
+
invalid: '無効(期待と逆方向にブレイク)',
|
|
271
|
+
forming: '形成中',
|
|
272
|
+
near_completion: 'ほぼ完成(apex接近)',
|
|
273
|
+
};
|
|
274
|
+
detail += `\n - 状態: ${statusJa[p.status] || p.status}`;
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
// ブレイクアウト情報(全パターン共通)
|
|
278
|
+
if (p.breakoutDirection && p.outcome) {
|
|
279
|
+
const directionJa = p.breakoutDirection === 'up' ? '上方' : '下方';
|
|
280
|
+
const outcomeJa = p.outcome === 'success' ? '成功' : '失敗';
|
|
281
|
+
|
|
282
|
+
// パターン別の期待方向と意味付け
|
|
283
|
+
const expectedDirMap: Record<string, string | undefined> = {
|
|
284
|
+
falling_wedge: '上方',
|
|
285
|
+
rising_wedge: '下方',
|
|
286
|
+
triangle_ascending: '上方',
|
|
287
|
+
triangle_descending: '下方',
|
|
288
|
+
pennant: p.poleDirection === 'up' ? '上方' : p.poleDirection === 'down' ? '下方' : undefined,
|
|
289
|
+
flag: undefined,
|
|
290
|
+
};
|
|
291
|
+
const expectedDir = expectedDirMap[p.type];
|
|
292
|
+
|
|
293
|
+
const meaningMap: Record<string, Record<string, string>> = {
|
|
294
|
+
falling_wedge: { success: '強気転換', failure: '弱気継続' },
|
|
295
|
+
rising_wedge: { success: '弱気転換', failure: '強気継続' },
|
|
296
|
+
triangle_ascending: { success: '上方ブレイク(強気)', failure: '下方ブレイク(弱気転換)' },
|
|
297
|
+
triangle_descending: { success: '下方ブレイク(弱気)', failure: '上方ブレイク(強気転換)' },
|
|
298
|
+
pennant: {
|
|
299
|
+
success: `トレンド継続(${p.poleDirection === 'up' ? '強気' : '弱気'})`,
|
|
300
|
+
failure: `ダマシ(${p.poleDirection === 'up' ? '弱気転換' : '強気転換'})`,
|
|
301
|
+
},
|
|
302
|
+
};
|
|
303
|
+
const meaning = meaningMap[p.type]?.[p.outcome] || `${directionJa}ブレイク`;
|
|
304
|
+
|
|
305
|
+
detail += `\n - ブレイク方向: ${directionJa}ブレイク`;
|
|
306
|
+
if (expectedDir) detail += `(本来は${expectedDir}ブレイクが期待されるパターン)`;
|
|
307
|
+
detail += `\n - パターン結果: ${outcomeJa}(${meaning})`;
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
// ネックライン/トレンドラインがある場合(用語正規化ラベルを使用)
|
|
311
|
+
if (p.neckline && Array.isArray(p.neckline) && p.neckline.length >= 2) {
|
|
312
|
+
const label = p.trendlineLabel || 'ネックライン';
|
|
313
|
+
detail += `\n - ${label}: ${Math.round(p.neckline[0]?.y || 0).toLocaleString('ja-JP')}円 → ${Math.round(p.neckline[1]?.y || 0).toLocaleString('ja-JP')}円`;
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
// ウェッジ固有: Apex(頂点)情報
|
|
317
|
+
if ((p.type === 'falling_wedge' || p.type === 'rising_wedge') && p.daysToApex != null) {
|
|
318
|
+
detail += `\n - Apex(収束点)まで: ${p.daysToApex}本`;
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
// ターゲット価格情報(全パターン共通)
|
|
322
|
+
if (p.breakoutTarget != null) {
|
|
323
|
+
const methodJa: Record<string, string> = {
|
|
324
|
+
flagpole_projection: 'フラッグポール値幅投影',
|
|
325
|
+
pattern_height: 'パターン高さ投影',
|
|
326
|
+
neckline_projection: 'ネックライン投影',
|
|
327
|
+
};
|
|
328
|
+
detail += `\n - ターゲット価格: ${Math.round(p.breakoutTarget).toLocaleString('ja-JP')}円(${(p.targetMethod && methodJa[p.targetMethod]) || p.targetMethod || '不明'})`;
|
|
329
|
+
if (p.targetReachedPct != null) {
|
|
330
|
+
detail += `\n - ターゲット進捗: ${p.targetReachedPct}%${p.targetReachedPct >= 100 ? '(到達済み)' : ''}`;
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
// ペナント固有フィールド
|
|
335
|
+
if (p.type === 'pennant') {
|
|
336
|
+
if (p.poleDirection) {
|
|
337
|
+
detail += `\n - フラッグポール方向: ${p.poleDirection === 'up' ? '上昇' : '下降'}`;
|
|
338
|
+
}
|
|
339
|
+
if (p.priorTrendDirection) {
|
|
340
|
+
detail += `\n - 先行トレンド: ${p.priorTrendDirection === 'bullish' ? '強気(上昇トレンド)' : '弱気(下降トレンド)'}`;
|
|
341
|
+
}
|
|
342
|
+
if (p.flagpoleHeight != null) {
|
|
343
|
+
detail += `\n - フラッグポール値幅: ${Math.round(p.flagpoleHeight).toLocaleString('ja-JP')}円`;
|
|
344
|
+
}
|
|
345
|
+
if (p.retracementRatio != null) {
|
|
346
|
+
const pctStr = (p.retracementRatio * 100).toFixed(0);
|
|
347
|
+
detail += `\n - 戻し比率: ${pctStr}%${p.retracementRatio > 0.38 ? '(高め — トライアングル寄り)' : '(正常範囲)'}`;
|
|
348
|
+
}
|
|
349
|
+
if (p.isTrendContinuation !== undefined) {
|
|
350
|
+
detail += `\n - トレンド継続: ${p.isTrendContinuation ? 'はい(成功)' : 'いいえ(ダマシ)'}`;
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
return detail;
|
|
355
|
+
})
|
|
356
|
+
.join('\n\n');
|
|
357
|
+
|
|
358
|
+
// aftermath 統計をテキストに含める(LLM が structuredContent.data を読めない対策)
|
|
359
|
+
const statsText =
|
|
360
|
+
statistics && Object.keys(statistics).length > 0
|
|
361
|
+
? '\n\n【統計情報】\n' +
|
|
362
|
+
Object.entries(statistics)
|
|
363
|
+
.map(([k, v]) => `${k}: ${JSON.stringify(v)}`)
|
|
364
|
+
.join('\n')
|
|
365
|
+
: '';
|
|
366
|
+
// 検出対象期間を算出
|
|
367
|
+
let detectionPeriodText = '';
|
|
368
|
+
{
|
|
369
|
+
const allStarts = summaryPatterns
|
|
370
|
+
.map((p) => p.range?.start)
|
|
371
|
+
.filter((s): s is string => !!s)
|
|
372
|
+
.map((s) => Date.parse(s))
|
|
373
|
+
.filter(Number.isFinite);
|
|
374
|
+
const allEnds = summaryPatterns
|
|
375
|
+
.map((p) => p.range?.end)
|
|
376
|
+
.filter((s): s is string => !!s)
|
|
377
|
+
.map((s) => Date.parse(s))
|
|
378
|
+
.filter(Number.isFinite);
|
|
379
|
+
if (allStarts.length && allEnds.length) {
|
|
380
|
+
const s = dayjs(Math.min(...allStarts))
|
|
381
|
+
.toISOString()
|
|
382
|
+
.slice(0, 10);
|
|
383
|
+
const e = dayjs(Math.max(...allEnds))
|
|
384
|
+
.toISOString()
|
|
385
|
+
.slice(0, 10);
|
|
386
|
+
const days = Math.max(1, Math.round((Math.max(...allEnds) - Math.min(...allStarts)) / 86400000));
|
|
387
|
+
detectionPeriodText = `\n検出対象期間: ${s} ~ ${e}(${days}日間)`;
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
// タイプ別件数を集約(例: rising_wedge×3, falling_wedge×2)
|
|
391
|
+
const typeCounts: Record<string, number> = {};
|
|
392
|
+
for (const p of summaryPatterns) {
|
|
393
|
+
typeCounts[p.type] = (typeCounts[p.type] || 0) + 1;
|
|
394
|
+
}
|
|
395
|
+
const typeCountStr = Object.entries(typeCounts)
|
|
396
|
+
.map(([t, c]) => `${t}×${c}`)
|
|
397
|
+
.join(', ');
|
|
398
|
+
|
|
399
|
+
const summaryText =
|
|
400
|
+
`${pair.toUpperCase()} ${tfLabel}(${type}) ${limit}本から${patterns.length}件を検出(${typeCountStr})${detectionPeriodText}\n\n【検出パターン(全件)】\n${patternSummaries || 'なし'}${statsText}\n\nチャート連携: data.overlays を render_chart_svg.overlays に渡すと注釈/範囲を描画できます。\n\nパターン整合度について(形状一致度・対称性・期間から算出):\n 0.8以上 = 理想的な形状(教科書的パターン)\n 0.7-0.8 = 標準的な形状(他指標と併用推奨)\n 0.6-0.7 = やや不明瞭(慎重に判断)\n 0.6未満 = 形状不十分` +
|
|
401
|
+
`\n\n---\n📌 含まれるもの: チャートパターン検出(種類・整合度・期間)、ブレイク情報、統計` +
|
|
402
|
+
`\n📌 含まれないもの: 出来高によるパターン確認、テクニカル指標値、板情報` +
|
|
403
|
+
`\n📌 補完ツール: analyze_indicators(指標でパターンを裏付け), get_flow_metrics(出来高確認), get_orderbook(板情報)`;
|
|
404
|
+
|
|
405
|
+
const out = ok(
|
|
406
|
+
summaryText,
|
|
407
|
+
{ patterns, overlays: { ranges }, warnings, statistics },
|
|
408
|
+
{
|
|
409
|
+
pair,
|
|
410
|
+
type,
|
|
411
|
+
count: patterns.length,
|
|
412
|
+
effective_params: { swingDepth, minBarsBetweenSwings: minDist, tolerancePct, autoScaled },
|
|
413
|
+
visualization_hints: {
|
|
414
|
+
preferred_style: 'line',
|
|
415
|
+
highlight_patterns: patterns.map((p) => p.type).slice(0, 3),
|
|
416
|
+
},
|
|
417
|
+
debug: debugTrimmed,
|
|
418
|
+
},
|
|
419
|
+
);
|
|
420
|
+
return DetectPatternsOutputSchema.parse(out);
|
|
421
|
+
} catch (e: unknown) {
|
|
422
|
+
return failFromError(e, { schema: DetectPatternsOutputSchema });
|
|
423
|
+
}
|
|
424
|
+
}
|
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import { TtlCache } from '../lib/cache.js';
|
|
3
|
+
import { nowIso } from '../lib/datetime.js';
|
|
4
|
+
import getDepth from '../lib/get-depth.js';
|
|
5
|
+
import { fail, failFromError, failFromValidation, ok } from '../lib/result.js';
|
|
6
|
+
import { createMeta, ensurePair } from '../lib/validate.js';
|
|
7
|
+
import type { Result } from '../src/schemas.js';
|
|
8
|
+
import type { ToolDefinition } from '../src/tool-definition.js';
|
|
9
|
+
import getCandles from './get_candles.js';
|
|
10
|
+
|
|
11
|
+
type Lookback = '30min' | '1hour' | '2hour';
|
|
12
|
+
|
|
13
|
+
const cache = new TtlCache<Result>({ ttlMs: 60_000 });
|
|
14
|
+
|
|
15
|
+
function extractLargeOrders(levels: Array<[number, number]>, minSize: number) {
|
|
16
|
+
return (levels || [])
|
|
17
|
+
.filter(([_p, s]) => Number(s) >= minSize)
|
|
18
|
+
.map(([p, s]) => ({ price: Number(p), size: Number(s) }));
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function analyzeTrend(buyVol: number, sellVol: number): 'accumulation' | 'distribution' | 'neutral' {
|
|
22
|
+
if (buyVol > sellVol * 1.2) return 'accumulation';
|
|
23
|
+
if (sellVol > buyVol * 1.2) return 'distribution';
|
|
24
|
+
return 'neutral';
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function generateRecommendation(trend: string): string {
|
|
28
|
+
if (trend === 'accumulation') return '買い圧力が優勢。段階的なエントリーを検討。';
|
|
29
|
+
if (trend === 'distribution') return '売り圧力が優勢。押し目待ち/警戒。';
|
|
30
|
+
return '均衡。レンジ内の値動きを想定。';
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export default async function detectWhaleEvents(
|
|
34
|
+
pair: string = 'btc_jpy',
|
|
35
|
+
lookback: Lookback = '1hour',
|
|
36
|
+
minSize: number = 0.5,
|
|
37
|
+
) {
|
|
38
|
+
const chk = ensurePair(pair);
|
|
39
|
+
if (!chk.ok) return failFromValidation(chk);
|
|
40
|
+
|
|
41
|
+
const cacheKey = `${chk.pair}:${lookback}:${minSize}`;
|
|
42
|
+
const hit = cache.get(cacheKey);
|
|
43
|
+
if (hit) return hit;
|
|
44
|
+
|
|
45
|
+
try {
|
|
46
|
+
const dep = await getDepth(chk.pair, { maxLevels: 200 });
|
|
47
|
+
if (!dep?.ok)
|
|
48
|
+
return fail(dep?.summary || 'depth failed', (dep?.meta as { errorType?: string })?.errorType || 'internal');
|
|
49
|
+
const rawAsks: Array<[string, string]> | undefined = dep?.data?.asks;
|
|
50
|
+
const rawBids: Array<[string, string]> | undefined = dep?.data?.bids;
|
|
51
|
+
if (!rawAsks || !rawBids) return fail('depth response missing asks/bids', 'upstream');
|
|
52
|
+
const asks: Array<[number, number]> = rawAsks.map(([p, s]) => [Number(p), Number(s)]);
|
|
53
|
+
const bids: Array<[number, number]> = rawBids.map(([p, s]) => [Number(p), Number(s)]);
|
|
54
|
+
const bestBid = bids.length ? Math.max(...bids.map(([p]) => p)) : null;
|
|
55
|
+
const bestAsk = asks.length ? Math.min(...asks.map(([p]) => p)) : null;
|
|
56
|
+
const mid = bestBid != null && bestAsk != null ? (bestBid + bestAsk) / 2 : null;
|
|
57
|
+
|
|
58
|
+
const lbMap: Record<Lookback, { type: string; limit: number }> = {
|
|
59
|
+
'30min': { type: '5min', limit: 6 },
|
|
60
|
+
'1hour': { type: '5min', limit: 12 },
|
|
61
|
+
'2hour': { type: '5min', limit: 24 },
|
|
62
|
+
};
|
|
63
|
+
const lb = lbMap[lookback] || lbMap['1hour'];
|
|
64
|
+
const candlesRes = await getCandles(chk.pair, lb.type, undefined, lb.limit);
|
|
65
|
+
if (!candlesRes?.ok)
|
|
66
|
+
return fail(
|
|
67
|
+
candlesRes?.summary || 'candles failed',
|
|
68
|
+
(candlesRes?.meta as { errorType?: string })?.errorType || 'internal',
|
|
69
|
+
);
|
|
70
|
+
const candles: Array<{ close: number }> = candlesRes?.data?.normalized || [];
|
|
71
|
+
const validCloses = candles
|
|
72
|
+
.map((c) => c.close)
|
|
73
|
+
.filter((v): v is number => typeof v === 'number' && Number.isFinite(v));
|
|
74
|
+
const priceChange =
|
|
75
|
+
validCloses.length >= 2 ? (validCloses[validCloses.length - 1] - validCloses[0]) / validCloses[0] : 0;
|
|
76
|
+
|
|
77
|
+
const largeBids = extractLargeOrders(bids, minSize);
|
|
78
|
+
const largeAsks = extractLargeOrders(asks, minSize);
|
|
79
|
+
|
|
80
|
+
const buyVol = largeBids.reduce((s, o) => s + o.size, 0);
|
|
81
|
+
const sellVol = largeAsks.reduce((s, o) => s + o.size, 0);
|
|
82
|
+
const trend = analyzeTrend(buyVol, sellVol);
|
|
83
|
+
const recommendation = generateRecommendation(trend);
|
|
84
|
+
|
|
85
|
+
const annotate = (side: 'buy' | 'sell') => (o: { price: number; size: number }) => ({
|
|
86
|
+
side,
|
|
87
|
+
price: o.price,
|
|
88
|
+
size: Number(o.size.toFixed(3)),
|
|
89
|
+
distancePct: mid ? Number((((o.price - mid) / mid) * 100).toFixed(2)) : null,
|
|
90
|
+
});
|
|
91
|
+
const events = [...largeBids.map(annotate('buy')), ...largeAsks.map(annotate('sell'))]
|
|
92
|
+
.sort((a, b) => Math.abs(a.distancePct || 0) - Math.abs(b.distancePct || 0))
|
|
93
|
+
.slice(0, 20);
|
|
94
|
+
|
|
95
|
+
// Visualization: buy/sell balance
|
|
96
|
+
const totalVol = buyVol + sellVol;
|
|
97
|
+
const buyPct = totalVol > 0 ? buyVol / totalVol : 0;
|
|
98
|
+
const sellPct = totalVol > 0 ? sellVol / totalVol : 0;
|
|
99
|
+
const barLen = 14;
|
|
100
|
+
const buyBars = '█'.repeat(Math.max(0, Math.round(buyPct * barLen)));
|
|
101
|
+
const sellBars = '█'.repeat(Math.max(0, Math.round(sellPct * barLen)));
|
|
102
|
+
|
|
103
|
+
// Distance stats
|
|
104
|
+
const buyDists = largeBids
|
|
105
|
+
.map((o) => (mid ? ((o.price - mid) / mid) * 100 : null))
|
|
106
|
+
.filter((x): x is number => x != null);
|
|
107
|
+
const sellDists = largeAsks
|
|
108
|
+
.map((o) => (mid ? ((o.price - mid) / mid) * 100 : null))
|
|
109
|
+
.filter((x): x is number => x != null);
|
|
110
|
+
const avg = (arr: number[]) => (arr.length ? arr.reduce((s, v) => s + v, 0) / arr.length : 0);
|
|
111
|
+
const avgBuyDist = avg(buyDists);
|
|
112
|
+
const avgSellDist = avg(sellDists);
|
|
113
|
+
|
|
114
|
+
const text = [
|
|
115
|
+
`=== ${chk.pair.toUpperCase()} 大口動向分析(過去${lookback})===`,
|
|
116
|
+
'',
|
|
117
|
+
`🐋 検出された大口: ${events.length}件`,
|
|
118
|
+
`買い: ${largeBids.length}件(合計${buyVol.toFixed(2)} BTC)`,
|
|
119
|
+
`売り: ${largeAsks.length}件(合計${sellVol.toFixed(2)} BTC)`,
|
|
120
|
+
'',
|
|
121
|
+
'📊 買い/売りバランス:',
|
|
122
|
+
` 買い: ${buyBars} ${buyVol.toFixed(2)} BTC (${(buyPct * 100).toFixed(0)}%)`,
|
|
123
|
+
` 売り: ${sellBars} ${sellVol.toFixed(2)} BTC (${(sellPct * 100).toFixed(0)}%)`,
|
|
124
|
+
'',
|
|
125
|
+
'📏 距離の統計:',
|
|
126
|
+
` 平均距離: 買い ${avgBuyDist.toFixed(2)}%, 売り ${avgSellDist.toFixed(2)}%`,
|
|
127
|
+
'',
|
|
128
|
+
'📋 主要な大口:',
|
|
129
|
+
...events.map(
|
|
130
|
+
(e) =>
|
|
131
|
+
`${e.side === 'buy' ? '🟢' : '🔴'} ${e.price.toLocaleString('ja-JP')}円に${e.size} BTC(${e.side === 'buy' ? '買い' : '売り'})距離: ${e.distancePct != null ? `${(e.distancePct >= 0 ? '+' : '') + e.distancePct}%` : 'n/a'}`,
|
|
132
|
+
),
|
|
133
|
+
'',
|
|
134
|
+
`📈 過去${lookback}の価格変化: ${(priceChange * 100).toFixed(2)}%`,
|
|
135
|
+
'',
|
|
136
|
+
`💡 総合評価: ${trend === 'accumulation' ? '買い圧力優勢' : trend === 'distribution' ? '売り圧力優勢' : '均衡'}(${trend})`,
|
|
137
|
+
recommendation,
|
|
138
|
+
'',
|
|
139
|
+
'※ 注: 推測ベースの簡易分析です(実約定・寿命照合は未実装)。',
|
|
140
|
+
'',
|
|
141
|
+
'---',
|
|
142
|
+
'📌 含まれるもの: 現在の板から検出した大口注文(買い/売り・価格・サイズ・距離)、バランス分析',
|
|
143
|
+
'📌 含まれないもの: 過去の大口動向の時系列変化、全体の出来高フロー、テクニカル指標',
|
|
144
|
+
'📌 補完ツール: get_flow_metrics(出来高フロー・CVD), get_orderbook(板の詳細分析), analyze_indicators(指標)',
|
|
145
|
+
].join('\n');
|
|
146
|
+
|
|
147
|
+
const data = {
|
|
148
|
+
events,
|
|
149
|
+
stats: {
|
|
150
|
+
buyOrders: largeBids.length,
|
|
151
|
+
sellOrders: largeAsks.length,
|
|
152
|
+
buyVolume: Number(buyVol.toFixed(3)),
|
|
153
|
+
sellVolume: Number(sellVol.toFixed(3)),
|
|
154
|
+
trend,
|
|
155
|
+
recommendation,
|
|
156
|
+
},
|
|
157
|
+
meta: { lookback, minSize },
|
|
158
|
+
};
|
|
159
|
+
|
|
160
|
+
const meta = createMeta(chk.pair, { fetchedAt: nowIso() });
|
|
161
|
+
const out = ok(text, data, meta);
|
|
162
|
+
cache.set(cacheKey, out);
|
|
163
|
+
return out;
|
|
164
|
+
} catch (e: unknown) {
|
|
165
|
+
return failFromError(e);
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
// ── MCP ツール定義(tool-registry から自動収集) ──
|
|
170
|
+
export const toolDef: ToolDefinition = {
|
|
171
|
+
name: 'detect_whale_events',
|
|
172
|
+
description:
|
|
173
|
+
'[Whale / Large Orders / Big Players] 大口投資家の動向検出(whale / large orders / big players / smart money)。板×ローソク足で大口注文を簡易検出。推測ベース。',
|
|
174
|
+
inputSchema: z.object({
|
|
175
|
+
pair: z.string().default('btc_jpy'),
|
|
176
|
+
lookback: z.enum(['30min', '1hour', '2hour']).default('1hour'),
|
|
177
|
+
minSize: z.number().min(0).default(0.5),
|
|
178
|
+
}),
|
|
179
|
+
handler: async ({ pair, lookback, minSize }: { pair: string; lookback: Lookback; minSize: number }) =>
|
|
180
|
+
detectWhaleEvents(pair, lookback, minSize),
|
|
181
|
+
};
|