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,77 @@
|
|
|
1
|
+
import type { z } from 'zod';
|
|
2
|
+
import { timeframeLabel } from '../../lib/formatter.js';
|
|
3
|
+
import { failFromValidation } from '../../lib/result.js';
|
|
4
|
+
import { ensurePair } from '../../lib/validate.js';
|
|
5
|
+
import detectPatterns from '../../tools/detect_patterns.js';
|
|
6
|
+
import { DetectPatternsInputSchema, DetectPatternsOutputSchema } from '../schemas.js';
|
|
7
|
+
import type { ToolDefinition } from '../tool-definition.js';
|
|
8
|
+
import {
|
|
9
|
+
buildPeriodLine,
|
|
10
|
+
buildTypeSummary,
|
|
11
|
+
formatDebugView,
|
|
12
|
+
formatDetailedView,
|
|
13
|
+
formatFullView,
|
|
14
|
+
formatSummaryView,
|
|
15
|
+
} from './detectPatternsViewsHandler.js';
|
|
16
|
+
|
|
17
|
+
type DetectPatternsInput = z.infer<typeof DetectPatternsInputSchema>;
|
|
18
|
+
type DetectPatternsOutput = z.infer<typeof DetectPatternsOutputSchema>;
|
|
19
|
+
|
|
20
|
+
export const toolDef: ToolDefinition = {
|
|
21
|
+
name: 'detect_patterns',
|
|
22
|
+
description:
|
|
23
|
+
'[Chart Patterns / Double Top / Head and Shoulders / Triangle] チャートパターン検出(chart patterns / double top / double bottom / head and shoulders / triangle / wedge / flag)。形成中+完成済みを統合検出。\n\n視覚確認: 結果の overlays を render_chart_svg に渡して描画可能。\n描画結果は必ずビジュアルとしてレンダリングして表示すること。\nチャット本文へのSVGコード直接出力は禁止。',
|
|
24
|
+
inputSchema: DetectPatternsInputSchema,
|
|
25
|
+
handler: async ({
|
|
26
|
+
pair,
|
|
27
|
+
type,
|
|
28
|
+
limit,
|
|
29
|
+
patterns,
|
|
30
|
+
swingDepth,
|
|
31
|
+
tolerancePct,
|
|
32
|
+
minBarsBetweenSwings,
|
|
33
|
+
view,
|
|
34
|
+
requireCurrentInPattern,
|
|
35
|
+
currentRelevanceDays,
|
|
36
|
+
includeForming,
|
|
37
|
+
includeCompleted,
|
|
38
|
+
includeInvalid,
|
|
39
|
+
}: DetectPatternsInput) => {
|
|
40
|
+
const chk = ensurePair(pair);
|
|
41
|
+
if (!chk.ok) return failFromValidation(chk);
|
|
42
|
+
const out = await detectPatterns(chk.pair, type, limit, {
|
|
43
|
+
patterns,
|
|
44
|
+
swingDepth,
|
|
45
|
+
tolerancePct,
|
|
46
|
+
minBarsBetweenSwings,
|
|
47
|
+
requireCurrentInPattern,
|
|
48
|
+
currentRelevanceDays,
|
|
49
|
+
includeForming,
|
|
50
|
+
includeCompleted,
|
|
51
|
+
includeInvalid,
|
|
52
|
+
});
|
|
53
|
+
const res: DetectPatternsOutput = DetectPatternsOutputSchema.parse(out);
|
|
54
|
+
if (!res.ok) return res;
|
|
55
|
+
const pats = Array.isArray(res.data.patterns) ? res.data.patterns : [];
|
|
56
|
+
const meta = res.meta;
|
|
57
|
+
const count = Number(meta.count ?? pats.length ?? 0);
|
|
58
|
+
const tfLabel = timeframeLabel(String(type));
|
|
59
|
+
const hdr = `${String(pair).toUpperCase()} ${tfLabel}(${String(type)}) ${limit ?? count}本から${pats.length}件を検出`;
|
|
60
|
+
|
|
61
|
+
if (view === 'debug') {
|
|
62
|
+
return formatDebugView(hdr, meta, pats, res);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const periodLine = buildPeriodLine(pats);
|
|
66
|
+
const typeSummary = buildTypeSummary(pats);
|
|
67
|
+
|
|
68
|
+
if ((view || 'detailed') === 'summary') {
|
|
69
|
+
return formatSummaryView(hdr, pats, periodLine, typeSummary, patterns, includeForming, res);
|
|
70
|
+
}
|
|
71
|
+
if ((view || 'detailed') === 'full') {
|
|
72
|
+
return formatFullView(hdr, pats, periodLine, typeSummary, meta, res);
|
|
73
|
+
}
|
|
74
|
+
// detailed (default)
|
|
75
|
+
return formatDetailedView(hdr, pats, periodLine, typeSummary, meta, tolerancePct, patterns, res);
|
|
76
|
+
},
|
|
77
|
+
};
|
|
@@ -0,0 +1,518 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* detectPatternsHandler のビュー別フォーマッタ
|
|
3
|
+
* debug / summary / full / detailed の4モードを分離
|
|
4
|
+
*/
|
|
5
|
+
import { toIsoTime } from '../../lib/datetime.js';
|
|
6
|
+
import { formatFixed, formatInt, formatPctFromRatio, formatRounded } from '../../lib/formatter.js';
|
|
7
|
+
import { toStructured } from '../../lib/result.js';
|
|
8
|
+
import type { PatternEntry } from '../../tools/patterns/types.js';
|
|
9
|
+
import type { McpResponse } from '../tool-definition.js';
|
|
10
|
+
|
|
11
|
+
/** デバッグスイング情報 */
|
|
12
|
+
interface SwingDebug {
|
|
13
|
+
kind: string;
|
|
14
|
+
idx: number;
|
|
15
|
+
price: number;
|
|
16
|
+
isoTime?: string;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/** デバッグ候補エントリ */
|
|
20
|
+
interface CandidateDebug {
|
|
21
|
+
type: string;
|
|
22
|
+
accepted: boolean;
|
|
23
|
+
reason?: string;
|
|
24
|
+
indices?: number[];
|
|
25
|
+
points?: Array<{ role: string; idx: number; price: number }>;
|
|
26
|
+
details?: Record<string, unknown>;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/** パターン検出メタデータ */
|
|
30
|
+
interface PatternMeta {
|
|
31
|
+
debug?: {
|
|
32
|
+
swings?: SwingDebug[];
|
|
33
|
+
candidates?: CandidateDebug[];
|
|
34
|
+
};
|
|
35
|
+
effective_params?: { tolerancePct?: number };
|
|
36
|
+
[key: string]: unknown;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** パターン検出結果(res パラメータ用) */
|
|
40
|
+
interface PatternResult {
|
|
41
|
+
ok?: boolean;
|
|
42
|
+
data?: { patterns?: PatternEntry[]; overlays?: unknown };
|
|
43
|
+
meta?: Record<string, unknown>;
|
|
44
|
+
summary?: string;
|
|
45
|
+
[key: string]: unknown;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/** fmtPointList 用のポイント */
|
|
49
|
+
interface IndexedPoint {
|
|
50
|
+
index: number;
|
|
51
|
+
price: number;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// ── helpers ──
|
|
55
|
+
|
|
56
|
+
const toTs = (s?: string): number => {
|
|
57
|
+
try {
|
|
58
|
+
return s ? Date.parse(s) : NaN;
|
|
59
|
+
} catch {
|
|
60
|
+
return NaN;
|
|
61
|
+
}
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
const fmtPointList = (arr: unknown): string =>
|
|
65
|
+
Array.isArray(arr) ? arr.map((p: IndexedPoint) => `[${p.index}:${formatRounded(p.price)}]`).join(', ') : 'n/a';
|
|
66
|
+
|
|
67
|
+
// ── shared ──
|
|
68
|
+
|
|
69
|
+
/** 検出対象期間の1行テキスト */
|
|
70
|
+
export function buildPeriodLine(pats: PatternEntry[]): string {
|
|
71
|
+
try {
|
|
72
|
+
const ends = pats.map((p) => toTs(p?.range?.end)).filter(Number.isFinite);
|
|
73
|
+
const starts = pats.map((p) => toTs(p?.range?.start)).filter(Number.isFinite);
|
|
74
|
+
if (starts.length && ends.length) {
|
|
75
|
+
const startIso = (toIsoTime(Math.min(...starts)) ?? '').slice(0, 10);
|
|
76
|
+
const endIso = (toIsoTime(Math.max(...ends)) ?? '').slice(0, 10);
|
|
77
|
+
const days = Math.max(1, Math.round((Math.max(...ends) - Math.min(...starts)) / 86400000));
|
|
78
|
+
return `検出対象期間: ${startIso} ~ ${endIso}(${days}日間)`;
|
|
79
|
+
}
|
|
80
|
+
} catch {
|
|
81
|
+
/* noop */
|
|
82
|
+
}
|
|
83
|
+
return '';
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/** 種別別件数集計 */
|
|
87
|
+
export function buildTypeSummary(pats: PatternEntry[]): string {
|
|
88
|
+
const byType = pats.reduce(
|
|
89
|
+
(m: Record<string, number>, p: PatternEntry) => {
|
|
90
|
+
const k = String(p?.type || 'unknown');
|
|
91
|
+
m[k] = (m[k] || 0) + 1;
|
|
92
|
+
return m;
|
|
93
|
+
},
|
|
94
|
+
{} as Record<string, number>,
|
|
95
|
+
);
|
|
96
|
+
return Object.entries(byType)
|
|
97
|
+
.map(([k, v]) => `${k}×${v}`)
|
|
98
|
+
.join(', ');
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// ── debug view: candidate details ──
|
|
102
|
+
|
|
103
|
+
function formatCandidateDetails(c: CandidateDebug): string {
|
|
104
|
+
if (!c.details) return '\n details: none';
|
|
105
|
+
const d = c.details;
|
|
106
|
+
const reason = String(c?.reason ?? '');
|
|
107
|
+
|
|
108
|
+
if (reason === 'type_classification_failed') {
|
|
109
|
+
return (
|
|
110
|
+
`\n failureReason: ${d?.failureReason || 'n/a'}` +
|
|
111
|
+
`\n slopes: hi=${formatFixed(d?.slopeHigh)} lo=${formatFixed(d?.slopeLow)}` +
|
|
112
|
+
`\n slopeRatio: ${Number.isFinite(Number(d?.slopeRatio)) ? Number(d.slopeRatio).toFixed(3) : 'n/a'}`
|
|
113
|
+
);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
if (reason === 'probe_window') {
|
|
117
|
+
return (
|
|
118
|
+
`\n upper.slope: ${formatFixed(d?.slopeHigh)}` +
|
|
119
|
+
`\n lower.slope: ${formatFixed(d?.slopeLow)}` +
|
|
120
|
+
`\n priceRange: ${formatRounded(d?.priceRange)}` +
|
|
121
|
+
`\n barsSpan: ${formatInt(d?.barsSpan)}` +
|
|
122
|
+
`\n minMeaningfulSlope: ${formatFixed(d?.minMeaningfulSlope)}` +
|
|
123
|
+
`\n highsIn: ${fmtPointList(d?.highsIn)}` +
|
|
124
|
+
`\n lowsIn: ${fmtPointList(d?.lowsIn)}`
|
|
125
|
+
);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
if (reason === 'declining_highs' || reason === 'declining_highs_probe') {
|
|
129
|
+
return (
|
|
130
|
+
`\n ${reason === 'declining_highs' ? 'declining_highs: true' : 'declining_highs_probe: metrics'}` +
|
|
131
|
+
`\n highsIn.count: ${formatInt(d?.highsCount)}` +
|
|
132
|
+
`\n 1st half avg: ${formatRounded(d?.firstAvg)}` +
|
|
133
|
+
`\n 2nd half avg: ${formatRounded(d?.secondAvg)}` +
|
|
134
|
+
`\n ratio: ${formatPctFromRatio(d?.ratio)}`
|
|
135
|
+
);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
if (reason === 'rising_probe') {
|
|
139
|
+
return (
|
|
140
|
+
`\n r2: hi=${Number.isFinite(Number(d?.r2High)) ? Number(d.r2High).toFixed(3) : 'n/a'}, lo=${Number.isFinite(Number(d?.r2Low)) ? Number(d.r2Low).toFixed(3) : 'n/a'}` +
|
|
141
|
+
`\n slopes: hi=${Number.isFinite(Number(d?.slopeHigh)) ? Number(d.slopeHigh).toFixed(6) : 'n/a'} lo=${Number.isFinite(Number(d?.slopeLow)) ? Number(d.slopeLow).toFixed(6) : 'n/a'}` +
|
|
142
|
+
`\n slopeRatioLH: ${Number.isFinite(Number(d?.slopeRatioLH)) ? Number(d.slopeRatioLH).toFixed(3) : 'n/a'}` +
|
|
143
|
+
`\n priceRange: ${formatRounded(d?.priceRange)}, barsSpan: ${formatInt(d?.barsSpan)}` +
|
|
144
|
+
`\n minMeaningfulSlope: ${Number.isFinite(Number(d?.minMeaningfulSlope)) ? Number(d.minMeaningfulSlope).toFixed(6) : 'n/a'}` +
|
|
145
|
+
`\n highsIn: ${fmtPointList(d?.highsIn)}` +
|
|
146
|
+
`\n lowsIn: ${fmtPointList(d?.lowsIn)}` +
|
|
147
|
+
`\n declining_highs metrics: firstAvg=${formatRounded(d?.firstAvg)}, secondAvg=${formatRounded(d?.secondAvg)}, ratio=${formatPctFromRatio(d?.ratio)}`
|
|
148
|
+
);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
if (reason === 'post_filter_rising_highs_not_declining') {
|
|
152
|
+
return (
|
|
153
|
+
`\n post_filter: rising highs not declining` +
|
|
154
|
+
`\n highsIn.count: ${formatInt(d?.highsCount)}` +
|
|
155
|
+
`\n 1st half avg: ${formatRounded(d?.firstAvg)}` +
|
|
156
|
+
`\n 2nd half avg: ${formatRounded(d?.secondAvg)}` +
|
|
157
|
+
`\n ratio: ${formatPctFromRatio(d?.ratio)}`
|
|
158
|
+
);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
if (reason === 'post_filter_falling_lows_not_rising') {
|
|
162
|
+
return (
|
|
163
|
+
`\n post_filter: falling lows not rising` +
|
|
164
|
+
`\n lowsIn.count: ${formatInt(d?.lowsCount)}` +
|
|
165
|
+
`\n 1st half avg: ${formatRounded(d?.firstAvg)}` +
|
|
166
|
+
`\n 2nd half avg: ${formatRounded(d?.secondAvg)}` +
|
|
167
|
+
`\n ratio: ${formatPctFromRatio(d?.ratio)}`
|
|
168
|
+
);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
// default
|
|
172
|
+
const s1 = Number(d.spreadStart);
|
|
173
|
+
const s2 = Number(d.spreadEnd);
|
|
174
|
+
const hi = Number(d.hiSlope);
|
|
175
|
+
const lo = Number(d.loSlope);
|
|
176
|
+
const spreadPart =
|
|
177
|
+
Number.isFinite(s1) && Number.isFinite(s2)
|
|
178
|
+
? `${Math.round(s1).toLocaleString('ja-JP')} → ${Math.round(s2).toLocaleString('ja-JP')}`
|
|
179
|
+
: 'n/a';
|
|
180
|
+
return `\n spread: ${spreadPart}${Number.isFinite(hi) || Number.isFinite(lo) ? `, slopes: hi=${formatFixed(hi)} lo=${formatFixed(lo)}` : ''}`;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
export function formatDebugView(
|
|
184
|
+
hdr: string,
|
|
185
|
+
meta: PatternMeta,
|
|
186
|
+
_pats: PatternEntry[],
|
|
187
|
+
res: PatternResult,
|
|
188
|
+
): McpResponse {
|
|
189
|
+
const swings: SwingDebug[] = Array.isArray(meta?.debug?.swings) ? meta.debug.swings : [];
|
|
190
|
+
const cands: CandidateDebug[] = Array.isArray(meta?.debug?.candidates) ? meta.debug.candidates : [];
|
|
191
|
+
|
|
192
|
+
const swingLines = swings.map(
|
|
193
|
+
(s) =>
|
|
194
|
+
`- ${s.kind} idx=${s.idx} price=${Math.round(Number(s.price)).toLocaleString('ja-JP')} (${s.isoTime || 'n/a'})`,
|
|
195
|
+
);
|
|
196
|
+
|
|
197
|
+
const candLines = cands.map((c, i: number) => {
|
|
198
|
+
const tag = c.accepted ? '✅' : '❌';
|
|
199
|
+
const reason = c.accepted ? (c.reason ? ` (${c.reason})` : '') : c.reason ? ` [${c.reason}]` : '';
|
|
200
|
+
const pts = Array.isArray(c.points)
|
|
201
|
+
? c.points.map((p) => `${p.role}@${p.idx}:${Math.round(Number(p.price)).toLocaleString('ja-JP')}`).join(', ')
|
|
202
|
+
: '';
|
|
203
|
+
const indices = Array.isArray(c.indices) ? ` indices=[${c.indices.join(',')}]` : '';
|
|
204
|
+
const detailsStr = formatCandidateDetails(c);
|
|
205
|
+
return `${i + 1}. ${tag} ${c.type}${reason}${indices}${pts ? `\n ${pts}` : ''}${detailsStr}`;
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
const text = [
|
|
209
|
+
hdr,
|
|
210
|
+
'',
|
|
211
|
+
'【Swings】',
|
|
212
|
+
swingLines.length ? swingLines.join('\n') : 'なし',
|
|
213
|
+
'',
|
|
214
|
+
'【Candidates】',
|
|
215
|
+
candLines.length ? candLines.join('\n') : 'なし',
|
|
216
|
+
].join('\n');
|
|
217
|
+
|
|
218
|
+
try {
|
|
219
|
+
return {
|
|
220
|
+
content: [{ type: 'text', text }],
|
|
221
|
+
structuredContent: {
|
|
222
|
+
data: { ...res?.data, candidates: cands },
|
|
223
|
+
meta: res?.meta ?? {},
|
|
224
|
+
ok: res?.ok ?? true,
|
|
225
|
+
summary: res?.summary ?? hdr,
|
|
226
|
+
} as Record<string, unknown>,
|
|
227
|
+
};
|
|
228
|
+
} catch {
|
|
229
|
+
return { content: [{ type: 'text', text }], structuredContent: toStructured(res) };
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
// ── pattern line formatter (shared by full / detailed) ──
|
|
234
|
+
|
|
235
|
+
function buildIdxToIso(meta: PatternMeta): Record<number, string> {
|
|
236
|
+
const map: Record<number, string> = {};
|
|
237
|
+
try {
|
|
238
|
+
const swings = meta?.debug?.swings;
|
|
239
|
+
if (Array.isArray(swings)) {
|
|
240
|
+
for (const s of swings) {
|
|
241
|
+
const i = Number(s?.idx);
|
|
242
|
+
const t = String(s?.isoTime || '');
|
|
243
|
+
if (Number.isFinite(i) && t) map[i] = t;
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
} catch {
|
|
247
|
+
/* noop */
|
|
248
|
+
}
|
|
249
|
+
return map;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
export function formatPatternLine(p: PatternEntry, idx: number, view: string, meta: PatternMeta): string {
|
|
253
|
+
const name = String(p?.type || 'unknown');
|
|
254
|
+
const conf = p?.confidence != null ? Number(p.confidence).toFixed(2) : 'n/a';
|
|
255
|
+
const range = p?.range ? `${p.range.start} ~ ${p.range.end}` : 'n/a';
|
|
256
|
+
|
|
257
|
+
// price range
|
|
258
|
+
let priceRange: string | null = null;
|
|
259
|
+
if (Array.isArray(p?.pivots) && p.pivots.length) {
|
|
260
|
+
const prices = p.pivots.map((v) => Number(v?.price)).filter(Number.isFinite);
|
|
261
|
+
if (prices.length)
|
|
262
|
+
priceRange = `${Math.min(...prices).toLocaleString('ja-JP')}円 - ${Math.max(...prices).toLocaleString('ja-JP')}円`;
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
// neckline
|
|
266
|
+
let neckline: string | null = null;
|
|
267
|
+
if (Array.isArray(p?.neckline) && p.neckline.length === 2) {
|
|
268
|
+
const [a, b] = p.neckline;
|
|
269
|
+
const y1 = Number(a?.y),
|
|
270
|
+
y2 = Number(b?.y);
|
|
271
|
+
if (Number.isFinite(y1) && Number.isFinite(y2)) {
|
|
272
|
+
neckline =
|
|
273
|
+
y1 === y2
|
|
274
|
+
? `${y1.toLocaleString('ja-JP')}円(水平)`
|
|
275
|
+
: `${y1.toLocaleString('ja-JP')}円 → ${y2.toLocaleString('ja-JP')}円`;
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
const idxToIso = buildIdxToIso(meta);
|
|
280
|
+
|
|
281
|
+
// pivot detail lines (full/debug + double_top/double_bottom)
|
|
282
|
+
const pivotLines: string[] = [];
|
|
283
|
+
if ((view === 'full' || view === 'debug') && Array.isArray(p?.pivots) && p.pivots.length >= 3) {
|
|
284
|
+
const pivs = p.pivots as Array<{ idx: number; price: number }>;
|
|
285
|
+
const roleLabels =
|
|
286
|
+
p.type === 'double_top' ? ['山1', '谷', '山2'] : p.type === 'double_bottom' ? ['谷1', '山', '谷2'] : null;
|
|
287
|
+
if (roleLabels) {
|
|
288
|
+
for (let i = 0; i < 3; i++) {
|
|
289
|
+
const pv = pivs[i];
|
|
290
|
+
if (!pv) continue;
|
|
291
|
+
const d = idxToIso[Number(pv.idx)] || '';
|
|
292
|
+
const date = d ? d.slice(0, 10) : 'n/a';
|
|
293
|
+
pivotLines.push(` - ${roleLabels[i]}: ${date} (${Math.round(Number(pv.price)).toLocaleString('ja-JP')}円)`);
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
// breakout
|
|
299
|
+
let breakoutLine: string | null = null;
|
|
300
|
+
try {
|
|
301
|
+
if ((view === 'full' || view === 'debug') && p?.breakout?.idx != null) {
|
|
302
|
+
const bidx = Number(p.breakout.idx);
|
|
303
|
+
const bpx = Number(p.breakout.price);
|
|
304
|
+
const bdate = idxToIso[bidx] ? String(idxToIso[bidx]).slice(0, 10) : 'n/a';
|
|
305
|
+
const bprice = Number.isFinite(bpx) ? Math.round(bpx).toLocaleString('ja-JP') : 'n/a';
|
|
306
|
+
breakoutLine = ` - ブレイク: ${bdate} (${bprice}円)`;
|
|
307
|
+
}
|
|
308
|
+
} catch {
|
|
309
|
+
/* ignore */
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
// status
|
|
313
|
+
let statusLine: string | null = null;
|
|
314
|
+
if (p?.status) {
|
|
315
|
+
const statusJa: Record<string, string> = {
|
|
316
|
+
completed: '完成(ブレイクアウト確認済み)',
|
|
317
|
+
invalid: '無効(期待と逆方向にブレイク)',
|
|
318
|
+
forming: '形成中',
|
|
319
|
+
near_completion: 'ほぼ完成(apex接近)',
|
|
320
|
+
};
|
|
321
|
+
statusLine = ` - 状態: ${statusJa[p.status] || p.status}`;
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
// breakout direction & outcome
|
|
325
|
+
let outcomeLine: string | null = null;
|
|
326
|
+
try {
|
|
327
|
+
if (p?.breakoutDirection && p?.outcome) {
|
|
328
|
+
const directionJa = p.breakoutDirection === 'up' ? '上方' : '下方';
|
|
329
|
+
const outcomeJa = p.outcome === 'success' ? '成功' : '失敗';
|
|
330
|
+
const expectedDirMap: Record<string, string | undefined> = {
|
|
331
|
+
falling_wedge: '上方',
|
|
332
|
+
rising_wedge: '下方',
|
|
333
|
+
triangle_ascending: '上方',
|
|
334
|
+
triangle_descending: '下方',
|
|
335
|
+
pennant: p.poleDirection === 'up' ? '上方' : p.poleDirection === 'down' ? '下方' : undefined,
|
|
336
|
+
};
|
|
337
|
+
const expectedDir = p.type ? expectedDirMap[p.type] : undefined;
|
|
338
|
+
const meaningMap: Record<string, Record<string, string>> = {
|
|
339
|
+
falling_wedge: { success: '強気転換', failure: '弱気継続' },
|
|
340
|
+
rising_wedge: { success: '弱気転換', failure: '強気継続' },
|
|
341
|
+
triangle_ascending: { success: '上方ブレイク(強気)', failure: '下方ブレイク(弱気転換)' },
|
|
342
|
+
triangle_descending: { success: '下方ブレイク(弱気)', failure: '上方ブレイク(強気転換)' },
|
|
343
|
+
pennant: {
|
|
344
|
+
success: `トレンド継続(${p.poleDirection === 'up' ? '強気' : '弱気'})`,
|
|
345
|
+
failure: `ダマシ(${p.poleDirection === 'up' ? '弱気転換' : '強気転換'})`,
|
|
346
|
+
},
|
|
347
|
+
};
|
|
348
|
+
const meaning = (p.type && p.outcome ? meaningMap[p.type]?.[p.outcome] : undefined) || `${directionJa}ブレイク`;
|
|
349
|
+
let dirLine = ` - ブレイク方向: ${directionJa}ブレイク`;
|
|
350
|
+
if (expectedDir) dirLine += `(本来は${expectedDir}ブレイクが期待されるパターン)`;
|
|
351
|
+
outcomeLine = `${dirLine}\n - パターン結果: ${outcomeJa}(${meaning})`;
|
|
352
|
+
}
|
|
353
|
+
} catch {
|
|
354
|
+
/* ignore */
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
// pennant fields
|
|
358
|
+
let pennantLine: string | null = null;
|
|
359
|
+
try {
|
|
360
|
+
if (p?.type === 'pennant') {
|
|
361
|
+
const parts: string[] = [];
|
|
362
|
+
if (p.poleDirection) parts.push(`フラッグポール方向: ${p.poleDirection === 'up' ? '上昇' : '下降'}`);
|
|
363
|
+
if (p.priorTrendDirection)
|
|
364
|
+
parts.push(
|
|
365
|
+
`先行トレンド: ${p.priorTrendDirection === 'bullish' ? '強気(上昇トレンド)' : '弱気(下降トレンド)'}`,
|
|
366
|
+
);
|
|
367
|
+
if (p.flagpoleHeight != null)
|
|
368
|
+
parts.push(`フラッグポール値幅: ${Math.round(Number(p.flagpoleHeight)).toLocaleString('ja-JP')}円`);
|
|
369
|
+
if (p.retracementRatio != null) {
|
|
370
|
+
const pctStr = (Number(p.retracementRatio) * 100).toFixed(0);
|
|
371
|
+
parts.push(
|
|
372
|
+
`戻し比率: ${pctStr}%${Number(p.retracementRatio) > 0.38 ? '(高め — トライアングル寄り)' : '(正常範囲)'}`,
|
|
373
|
+
);
|
|
374
|
+
}
|
|
375
|
+
if (p.isTrendContinuation !== undefined)
|
|
376
|
+
parts.push(`トレンド継続: ${p.isTrendContinuation ? 'はい(成功)' : 'いいえ(ダマシ)'}`);
|
|
377
|
+
if (parts.length) pennantLine = parts.map((s) => ` - ${s}`).join('\n');
|
|
378
|
+
}
|
|
379
|
+
} catch {
|
|
380
|
+
/* ignore */
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
// structure diagram
|
|
384
|
+
let diagramBlock: string | null = null;
|
|
385
|
+
try {
|
|
386
|
+
if ((view === 'full' || view === 'detailed') && p?.structureDiagram?.svg) {
|
|
387
|
+
const diagram = p.structureDiagram;
|
|
388
|
+
const id = String(diagram?.artifact?.identifier || 'pattern-diagram');
|
|
389
|
+
const title = String(diagram?.artifact?.title || 'パターン構造図');
|
|
390
|
+
diagramBlock = [
|
|
391
|
+
'--- Structure Diagram (SVG) ---',
|
|
392
|
+
`identifier: ${id}`,
|
|
393
|
+
`title: ${title}`,
|
|
394
|
+
'type: image/svg+xml',
|
|
395
|
+
'',
|
|
396
|
+
String(diagram.svg),
|
|
397
|
+
].join('\n');
|
|
398
|
+
}
|
|
399
|
+
} catch {
|
|
400
|
+
/* noop */
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
// target price
|
|
404
|
+
let targetLine: string | null = null;
|
|
405
|
+
if (p?.breakoutTarget != null) {
|
|
406
|
+
const methodJa: Record<string, string> = {
|
|
407
|
+
flagpole_projection: 'フラッグポール値幅投影',
|
|
408
|
+
pattern_height: 'パターン高さ投影',
|
|
409
|
+
neckline_projection: 'ネックライン投影',
|
|
410
|
+
};
|
|
411
|
+
targetLine = ` - ターゲット価格: ${Math.round(Number(p.breakoutTarget)).toLocaleString('ja-JP')}円(${(p.targetMethod && methodJa[p.targetMethod]) || p.targetMethod})`;
|
|
412
|
+
if (p?.targetReachedPct != null) {
|
|
413
|
+
targetLine += `\n - ターゲット進捗: ${p.targetReachedPct}%${Number(p.targetReachedPct) >= 100 ? '(到達済み)' : ''}`;
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
const lines = [
|
|
418
|
+
`${idx + 1}. ${name} (パターン整合度: ${conf})`,
|
|
419
|
+
` - 期間: ${range}`,
|
|
420
|
+
statusLine,
|
|
421
|
+
priceRange ? ` - 価格範囲: ${priceRange}` : null,
|
|
422
|
+
...(pivotLines.length ? pivotLines : []),
|
|
423
|
+
neckline ? ` - ${p?.trendlineLabel || 'ネックライン'}: ${neckline}` : null,
|
|
424
|
+
breakoutLine,
|
|
425
|
+
outcomeLine,
|
|
426
|
+
targetLine,
|
|
427
|
+
pennantLine,
|
|
428
|
+
diagramBlock,
|
|
429
|
+
].filter(Boolean);
|
|
430
|
+
return lines.join('\n');
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
// ── summary view ──
|
|
434
|
+
|
|
435
|
+
export function formatSummaryView(
|
|
436
|
+
hdr: string,
|
|
437
|
+
pats: PatternEntry[],
|
|
438
|
+
periodLine: string,
|
|
439
|
+
typeSummary: string,
|
|
440
|
+
patterns: string[] | undefined,
|
|
441
|
+
includeForming: boolean | undefined,
|
|
442
|
+
res: PatternResult,
|
|
443
|
+
): McpResponse {
|
|
444
|
+
const now = Date.now();
|
|
445
|
+
const within = (ms: number) =>
|
|
446
|
+
pats.filter((p) => Number.isFinite(toTs(p?.range?.end)) && now - toTs(p.range!.end) <= ms).length;
|
|
447
|
+
const in30 = within(30 * 86400000);
|
|
448
|
+
const in90 = within(90 * 86400000);
|
|
449
|
+
const formingHint = includeForming ? '' : '\n※形成中は includeForming=true を指定してください。';
|
|
450
|
+
const text = `${hdr}(${typeSummary || '分類なし'}、直近30日: ${in30}件、直近90日: ${in90}件)\n${periodLine ? `${periodLine}\n` : ''}検討パターン: ${patterns?.length ? patterns.join(', ') : '既定セット'}${formingHint}\n詳細は structuredContent.data.patterns を参照。`;
|
|
451
|
+
return { content: [{ type: 'text', text }], structuredContent: toStructured(res) };
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
// ── full view ──
|
|
455
|
+
|
|
456
|
+
export function formatFullView(
|
|
457
|
+
hdr: string,
|
|
458
|
+
pats: PatternEntry[],
|
|
459
|
+
periodLine: string,
|
|
460
|
+
typeSummary: string,
|
|
461
|
+
meta: PatternMeta,
|
|
462
|
+
res: PatternResult,
|
|
463
|
+
): McpResponse {
|
|
464
|
+
const body = pats.map((p, i) => formatPatternLine(p, i, 'full', meta)).join('\n\n');
|
|
465
|
+
const overlayNote = res?.data?.overlays
|
|
466
|
+
? '\n\nチャート連携: structuredContent.data.overlays を render_chart_svg.overlays に渡すと注釈/範囲を描画できます。'
|
|
467
|
+
: '';
|
|
468
|
+
const trustNote =
|
|
469
|
+
'\n\nパターン整合度について(形状一致度・対称性・期間から算出):\n 0.8以上 = 理想的な形状(教科書的パターン)\n 0.7-0.8 = 標準的な形状(他指標と併用推奨)\n 0.6-0.7 = やや不明瞭(慎重に判断)\n 0.6未満 = 形状不十分';
|
|
470
|
+
const text = `${hdr}(${typeSummary || '分類なし'})\n${periodLine ? `${periodLine}\n` : ''}\n【検出パターン(全件)】\n${body}${overlayNote}${trustNote}`;
|
|
471
|
+
return { content: [{ type: 'text', text }], structuredContent: toStructured(res) };
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
// ── detailed view (default) ──
|
|
475
|
+
|
|
476
|
+
export function formatDetailedView(
|
|
477
|
+
hdr: string,
|
|
478
|
+
pats: PatternEntry[],
|
|
479
|
+
periodLine: string,
|
|
480
|
+
typeSummary: string,
|
|
481
|
+
meta: PatternMeta,
|
|
482
|
+
tolerancePct: number | undefined,
|
|
483
|
+
patterns: string[] | undefined,
|
|
484
|
+
res: PatternResult,
|
|
485
|
+
): McpResponse {
|
|
486
|
+
const top = pats.slice(0, 5);
|
|
487
|
+
const body = top.length ? top.map((p, i) => formatPatternLine(p, i, 'detailed', meta)).join('\n\n') : '';
|
|
488
|
+
|
|
489
|
+
let none = '';
|
|
490
|
+
if (!top.length) {
|
|
491
|
+
const resSummary = String(res?.summary ?? '');
|
|
492
|
+
if (resSummary === 'insufficient data') {
|
|
493
|
+
none = `\n${resSummary}`;
|
|
494
|
+
} else {
|
|
495
|
+
const effTol = meta?.effective_params?.tolerancePct ?? tolerancePct ?? 'default';
|
|
496
|
+
none = `\nパターンは検出されませんでした(tolerancePct=${effTol})。\n・検討パターン: ${patterns?.length ? patterns.join(', ') : '既定セット'}\n・必要に応じて tolerance を 0.03-0.06 に緩和してください`;
|
|
497
|
+
}
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
const overlayNote = res?.data?.overlays
|
|
501
|
+
? '\n\nチャート連携: structuredContent.data.overlays を render_chart_svg.overlays に渡すと注釈/範囲を描画できます。'
|
|
502
|
+
: '';
|
|
503
|
+
const trustNote =
|
|
504
|
+
'\n\nパターン整合度について(形状一致度・対称性・期間から算出):\n 0.8以上 = 理想的な形状(教科書的パターン)\n 0.7-0.8 = 標準的な形状(他指標と併用推奨)\n 0.6-0.7 = やや不明瞭(慎重に判断)\n 0.6未満 = 形状不十分';
|
|
505
|
+
const usage = `\n\nusage_example:\n step1: detect_patterns を実行\n step2: structuredContent.data.overlays を取得\n step3: render_chart_svg の overlays に渡す`;
|
|
506
|
+
const text = `${hdr}(${typeSummary || '分類なし'})\n${periodLine ? `${periodLine}\n` : ''}\n${top.length ? `【検出パターン】\n${body}` : ''}${none}${overlayNote}${trustNote}${usage}`;
|
|
507
|
+
return {
|
|
508
|
+
content: [{ type: 'text', text }],
|
|
509
|
+
structuredContent: {
|
|
510
|
+
...res,
|
|
511
|
+
usage_example: {
|
|
512
|
+
step1: 'detect_patterns を実行',
|
|
513
|
+
step2: 'data.overlays を取得',
|
|
514
|
+
step3: 'render_chart_svg の overlays に渡す',
|
|
515
|
+
},
|
|
516
|
+
},
|
|
517
|
+
};
|
|
518
|
+
}
|