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,691 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import { dayjs } from '../lib/datetime.js';
|
|
3
|
+
import { formatSummary } from '../lib/formatter.js';
|
|
4
|
+
import { fail, failFromError, failFromValidation, ok, toStructured } from '../lib/result.js';
|
|
5
|
+
import { ALLOWED_PAIRS, ensurePair } from '../lib/validate.js';
|
|
6
|
+
import type { Pair } from '../src/schemas.js';
|
|
7
|
+
import type { ToolDefinition } from '../src/tool-definition.js';
|
|
8
|
+
import analyzeIndicators from './analyze_indicators.js';
|
|
9
|
+
|
|
10
|
+
// ── テキスト組み立て: 純粋エクスポート関数 ──
|
|
11
|
+
|
|
12
|
+
export interface MacdScreenCross {
|
|
13
|
+
pair: string;
|
|
14
|
+
type: 'golden' | 'dead';
|
|
15
|
+
crossDate: string | null;
|
|
16
|
+
barsAgo: number;
|
|
17
|
+
macdAtCross: number | null;
|
|
18
|
+
signalAtCross: number | null;
|
|
19
|
+
histogramDelta: number | null;
|
|
20
|
+
returnSinceCrossPct: number | null;
|
|
21
|
+
prevCross: { type: 'golden' | 'dead'; barsAgo: number } | null;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export interface BuildMacdScreenTextInput {
|
|
25
|
+
baseSummary: string;
|
|
26
|
+
crosses: MacdScreenCross[];
|
|
27
|
+
includeForming: boolean;
|
|
28
|
+
includeStats: boolean;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function buildMacdScreenText(input: BuildMacdScreenTextInput): string {
|
|
32
|
+
const { baseSummary, crosses } = input;
|
|
33
|
+
const crossLines = crosses.map((r, i) => {
|
|
34
|
+
const date = r.crossDate ? String(r.crossDate).slice(0, 10) : '?';
|
|
35
|
+
const ret =
|
|
36
|
+
r.returnSinceCrossPct != null ? ` ret:${r.returnSinceCrossPct >= 0 ? '+' : ''}${r.returnSinceCrossPct}%` : '';
|
|
37
|
+
const hd = r.histogramDelta != null ? ` histDelta:${r.histogramDelta}` : '';
|
|
38
|
+
const prev = r.prevCross ? ` prev:${r.prevCross.type}(${r.prevCross.barsAgo}bars)` : '';
|
|
39
|
+
return `[${i}] ${r.pair} ${r.type} @${date} barsAgo:${r.barsAgo} macd:${r.macdAtCross} sig:${r.signalAtCross}${hd}${ret}${prev}`;
|
|
40
|
+
});
|
|
41
|
+
return (
|
|
42
|
+
baseSummary +
|
|
43
|
+
`\n\n📋 全${crosses.length}件のクロス詳細:\n` +
|
|
44
|
+
crossLines.join('\n') +
|
|
45
|
+
`\n\n---\n📌 含まれるもの: MACDクロス検出(種類・日付・ヒストグラム差分・リターン率・前回クロス)` +
|
|
46
|
+
`\n📌 含まれないもの: 他のテクニカル指標(RSI・BB等)、出来高分析、板情報` +
|
|
47
|
+
`\n📌 補完ツール: analyze_indicators(全指標詳細), analyze_market_signal(総合シグナル), get_flow_metrics(出来高)`
|
|
48
|
+
);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
interface FormingStatus {
|
|
52
|
+
status: string;
|
|
53
|
+
estimatedCrossDays?: number | null;
|
|
54
|
+
completion?: number | null;
|
|
55
|
+
currentHistogram?: number | null;
|
|
56
|
+
histogramTrend?: unknown[];
|
|
57
|
+
currentMACD?: number | null;
|
|
58
|
+
currentSignal?: number | null;
|
|
59
|
+
lastCrossDate?: string | null;
|
|
60
|
+
lastCrossBarsAgo?: number | null;
|
|
61
|
+
lastCrossType?: string | null;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
interface CrossStats {
|
|
65
|
+
totalSamples: number;
|
|
66
|
+
avgDay5Return?: number | null;
|
|
67
|
+
worstCase?: number | null;
|
|
68
|
+
bestCase?: number | null;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
interface CrossHistory {
|
|
72
|
+
goldenCrosses: CrossPerf[];
|
|
73
|
+
deadCrosses: CrossPerf[];
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export interface BuildMacdSingleTextInput {
|
|
77
|
+
pair: string;
|
|
78
|
+
lastClose: number | null;
|
|
79
|
+
forming: FormingStatus | null;
|
|
80
|
+
statistics: { golden: CrossStats; dead: CrossStats } | null;
|
|
81
|
+
history: CrossHistory | null;
|
|
82
|
+
historyDays: number;
|
|
83
|
+
includeForming: boolean;
|
|
84
|
+
includeStats: boolean;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export function buildMacdSingleText(input: BuildMacdSingleTextInput): string {
|
|
88
|
+
const { pair, lastClose, forming, statistics, history, historyDays, includeForming, includeStats } = input;
|
|
89
|
+
const lines: string[] = [];
|
|
90
|
+
const pairStr = String(pair).toUpperCase();
|
|
91
|
+
lines.push(lastClose != null ? `${pairStr} close=${Number(lastClose).toLocaleString('ja-JP')}円` : pairStr);
|
|
92
|
+
|
|
93
|
+
if (forming) {
|
|
94
|
+
const f = forming;
|
|
95
|
+
if (f.status === 'forming_golden' || f.status === 'forming_dead') {
|
|
96
|
+
const days =
|
|
97
|
+
f.estimatedCrossDays != null
|
|
98
|
+
? f.estimatedCrossDays <= 1.5
|
|
99
|
+
? '1-2日以内'
|
|
100
|
+
: `${Math.round(f.estimatedCrossDays)}日程度`
|
|
101
|
+
: '不明';
|
|
102
|
+
const compStr = f.completion != null ? `${Math.round((f.completion || 0) * 100)}%` : 'n/a';
|
|
103
|
+
const crossType = f.status === 'forming_golden' ? 'ゴールデン' : 'デッド';
|
|
104
|
+
const fmt = (v: unknown, d = 2) => (v == null ? 'n/a' : Number(v).toFixed(d));
|
|
105
|
+
const estDate = (() => {
|
|
106
|
+
if (f.estimatedCrossDays == null) return '不明';
|
|
107
|
+
try {
|
|
108
|
+
return dayjs()
|
|
109
|
+
.add(Math.max(0, Math.round(f.estimatedCrossDays)), 'day')
|
|
110
|
+
.format('YYYY-MM-DD');
|
|
111
|
+
} catch {
|
|
112
|
+
return '不明';
|
|
113
|
+
}
|
|
114
|
+
})();
|
|
115
|
+
lines.push(`${crossType}クロス形成中: 完成度${compStr}、推定クロス日 ${estDate}(あと${days})`);
|
|
116
|
+
lines.push(
|
|
117
|
+
`- ヒストグラム: ${fmt(f.currentHistogram, 2)} (直近5本: [${(Array.isArray(f.histogramTrend) ? f.histogramTrend : []).map((v: unknown) => (v == null ? 'n/a' : String(v))).join(', ')}])`,
|
|
118
|
+
);
|
|
119
|
+
lines.push(`- MACD: ${fmt(f.currentMACD, 2)} / Signal: ${fmt(f.currentSignal, 2)}`);
|
|
120
|
+
} else if (f.status === 'crossed_recently') {
|
|
121
|
+
const dateStr = f.lastCrossDate ? String(f.lastCrossDate).slice(0, 10) : '不明';
|
|
122
|
+
const agoStr = f.lastCrossBarsAgo != null ? `${f.lastCrossBarsAgo}日前` : '直近';
|
|
123
|
+
const typ = f.lastCrossType === 'dead' ? 'デッド' : 'ゴールデン';
|
|
124
|
+
lines.push(`${typ}クロス発生: ${dateStr}(${agoStr})`);
|
|
125
|
+
} else {
|
|
126
|
+
lines.push('現在クロス形成の兆候なし');
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
if (statistics && history) {
|
|
131
|
+
const gStats = statistics.golden;
|
|
132
|
+
const dStats = statistics.dead;
|
|
133
|
+
const goldenCrosses = history.goldenCrosses;
|
|
134
|
+
const deadCrosses = history.deadCrosses;
|
|
135
|
+
if (gStats.totalSamples > 0) {
|
|
136
|
+
const avgStr =
|
|
137
|
+
gStats.avgDay5Return != null ? `${gStats.avgDay5Return >= 0 ? '+' : ''}${gStats.avgDay5Return}%` : 'n/a';
|
|
138
|
+
const upCount = goldenCrosses.filter((c) => (c.performance.day5 ?? -Infinity) > 0).length;
|
|
139
|
+
const rangeStr =
|
|
140
|
+
gStats.worstCase != null && gStats.bestCase != null
|
|
141
|
+
? `${gStats.worstCase >= 0 ? '+' : ''}${gStats.worstCase}% 〜 ${gStats.bestCase >= 0 ? '+' : ''}${gStats.bestCase}%`
|
|
142
|
+
: 'n/a';
|
|
143
|
+
lines.push(`過去${historyDays}日: ゴールデンクロス${goldenCrosses.length}回`);
|
|
144
|
+
const upPct = goldenCrosses.length ? Math.round((upCount / goldenCrosses.length) * 100) : 0;
|
|
145
|
+
lines.push(`- クロス後5日間: 平均${avgStr}、上昇した割合 ${upCount}/${goldenCrosses.length}回(${upPct}%)`);
|
|
146
|
+
lines.push(`- 範囲: ${rangeStr}`);
|
|
147
|
+
}
|
|
148
|
+
if (dStats.totalSamples > 0) {
|
|
149
|
+
const avgStr =
|
|
150
|
+
dStats.avgDay5Return != null ? `${dStats.avgDay5Return >= 0 ? '+' : ''}${dStats.avgDay5Return}%` : 'n/a';
|
|
151
|
+
const downCount = deadCrosses.filter((c) => (c.performance.day5 ?? Infinity) < 0).length;
|
|
152
|
+
const rangeStr =
|
|
153
|
+
dStats.worstCase != null && dStats.bestCase != null
|
|
154
|
+
? `${dStats.worstCase >= 0 ? '+' : ''}${dStats.worstCase}% 〜 ${dStats.bestCase >= 0 ? '+' : ''}${dStats.bestCase}%`
|
|
155
|
+
: 'n/a';
|
|
156
|
+
lines.push(`デッドクロス${deadCrosses.length}回`);
|
|
157
|
+
const downPct = deadCrosses.length ? Math.round((downCount / deadCrosses.length) * 100) : 0;
|
|
158
|
+
lines.push(`- クロス後5日間: 平均${avgStr}、下落した割合 ${downCount}/${deadCrosses.length}回(${downPct}%)`);
|
|
159
|
+
lines.push(`- 範囲: ${rangeStr}`);
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
return (
|
|
164
|
+
lines.join('\n') +
|
|
165
|
+
`\n\n---\n📌 含まれるもの: MACD分析(${includeForming ? 'forming検出' : ''}${includeForming && includeStats ? '・' : ''}${includeStats ? '過去統計' : ''})` +
|
|
166
|
+
`\n📌 含まれないもの: 他のテクニカル指標(RSI・BB等)、出来高分析、板情報` +
|
|
167
|
+
`\n📌 補完ツール: analyze_indicators(全指標詳細), analyze_market_signal(総合シグナル), get_flow_metrics(出来高)`
|
|
168
|
+
);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
// ── 共通: クロス検出ヘルパー ──
|
|
172
|
+
|
|
173
|
+
type CrossDetailed = {
|
|
174
|
+
pair: string;
|
|
175
|
+
type: 'golden' | 'dead';
|
|
176
|
+
crossIndex: number;
|
|
177
|
+
crossDate: string | null;
|
|
178
|
+
barsAgo: number;
|
|
179
|
+
macdAtCross: number | null;
|
|
180
|
+
signalAtCross: number | null;
|
|
181
|
+
histogramPrev: number | null;
|
|
182
|
+
histogramCurr: number | null;
|
|
183
|
+
histogramDelta: number | null;
|
|
184
|
+
prevCross: { type: 'golden' | 'dead'; barsAgo: number; date: string | null } | null;
|
|
185
|
+
priceAtCross: number | null;
|
|
186
|
+
currentPrice: number | null;
|
|
187
|
+
returnSinceCrossPct: number | null;
|
|
188
|
+
};
|
|
189
|
+
|
|
190
|
+
function diffAt(line: (number | null)[], signal: (number | null)[], i: number): number | null {
|
|
191
|
+
return (line[i] ?? null) != null && (signal[i] ?? null) != null ? (line[i] as number) - (signal[i] as number) : null;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
function findPrevCross(
|
|
195
|
+
line: number[],
|
|
196
|
+
signal: number[],
|
|
197
|
+
before: number,
|
|
198
|
+
): { idx: number; type: 'golden' | 'dead' } | null {
|
|
199
|
+
for (let j = before - 1; j >= 1; j--) {
|
|
200
|
+
const pd = diffAt(line, signal, j - 1);
|
|
201
|
+
const cd = diffAt(line, signal, j);
|
|
202
|
+
if (pd == null || cd == null) continue;
|
|
203
|
+
if (pd <= 0 && cd > 0) return { idx: j, type: 'golden' };
|
|
204
|
+
if (pd >= 0 && cd < 0) return { idx: j, type: 'dead' };
|
|
205
|
+
}
|
|
206
|
+
return null;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
function detectCrossInRange(
|
|
210
|
+
line: number[],
|
|
211
|
+
signal: number[],
|
|
212
|
+
candles: Array<{ isoTime?: string | null; close?: number | null }>,
|
|
213
|
+
start: number,
|
|
214
|
+
end: number,
|
|
215
|
+
n: number,
|
|
216
|
+
pairName: string,
|
|
217
|
+
): CrossDetailed | null {
|
|
218
|
+
for (let i = end; i >= start; i--) {
|
|
219
|
+
const prevDiff = diffAt(line, signal, i - 1);
|
|
220
|
+
const currDiff = diffAt(line, signal, i);
|
|
221
|
+
if (prevDiff == null || currDiff == null) continue;
|
|
222
|
+
const isGolden = prevDiff <= 0 && currDiff > 0;
|
|
223
|
+
const isDead = prevDiff >= 0 && currDiff < 0;
|
|
224
|
+
if (!isGolden && !isDead) continue;
|
|
225
|
+
|
|
226
|
+
const currentPrice = (candles.at(-1)?.close ?? null) as number | null;
|
|
227
|
+
const priceAtCross = (candles[i]?.close ?? null) as number | null;
|
|
228
|
+
const retPct =
|
|
229
|
+
priceAtCross && currentPrice != null
|
|
230
|
+
? Number((((currentPrice - priceAtCross) / priceAtCross) * 100).toFixed(2))
|
|
231
|
+
: null;
|
|
232
|
+
const prev = findPrevCross(line, signal, i);
|
|
233
|
+
|
|
234
|
+
return {
|
|
235
|
+
pair: pairName,
|
|
236
|
+
type: isGolden ? 'golden' : 'dead',
|
|
237
|
+
crossIndex: i,
|
|
238
|
+
crossDate: candles[i]?.isoTime ?? null,
|
|
239
|
+
barsAgo: n - 1 - i,
|
|
240
|
+
macdAtCross: (line[i] ?? null) as number | null,
|
|
241
|
+
signalAtCross: (signal[i] ?? null) as number | null,
|
|
242
|
+
histogramPrev: prevDiff,
|
|
243
|
+
histogramCurr: currDiff,
|
|
244
|
+
histogramDelta: Number((currDiff - prevDiff).toFixed(4)),
|
|
245
|
+
prevCross: prev ? { type: prev.type, barsAgo: i - prev.idx, date: candles[prev.idx]?.isoTime ?? null } : null,
|
|
246
|
+
priceAtCross,
|
|
247
|
+
currentPrice,
|
|
248
|
+
returnSinceCrossPct: retPct,
|
|
249
|
+
};
|
|
250
|
+
}
|
|
251
|
+
return null;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
// ── モード A: 複数ペアスクリーニング ──
|
|
255
|
+
|
|
256
|
+
type ScreenOpts = {
|
|
257
|
+
minHistogramDelta?: number;
|
|
258
|
+
maxBarsAgo?: number;
|
|
259
|
+
minReturnPct?: number;
|
|
260
|
+
maxReturnPct?: number;
|
|
261
|
+
crossType?: 'golden' | 'dead' | 'both';
|
|
262
|
+
sortBy?: 'date' | 'histogram' | 'return' | 'barsAgo';
|
|
263
|
+
sortOrder?: 'asc' | 'desc';
|
|
264
|
+
limit?: number;
|
|
265
|
+
withPrice?: boolean;
|
|
266
|
+
};
|
|
267
|
+
|
|
268
|
+
async function screenMode(
|
|
269
|
+
market: 'all' | 'jpy',
|
|
270
|
+
lookback: number,
|
|
271
|
+
pairs: string[] | undefined,
|
|
272
|
+
view: 'summary' | 'detailed',
|
|
273
|
+
screen: ScreenOpts | undefined,
|
|
274
|
+
) {
|
|
275
|
+
const universe = pairs?.length
|
|
276
|
+
? pairs.filter((p) => ALLOWED_PAIRS.has(p as Pair))
|
|
277
|
+
: Array.from(ALLOWED_PAIRS.values()).filter((p) => (market === 'jpy' ? p.endsWith('_jpy') : true));
|
|
278
|
+
|
|
279
|
+
const allDetailed: CrossDetailed[] = [];
|
|
280
|
+
const failedPairs: string[] = [];
|
|
281
|
+
await Promise.all(
|
|
282
|
+
universe.map(async (pair) => {
|
|
283
|
+
try {
|
|
284
|
+
const ind = await analyzeIndicators(pair, '1day', 120);
|
|
285
|
+
if (!ind?.ok) {
|
|
286
|
+
failedPairs.push(pair);
|
|
287
|
+
return;
|
|
288
|
+
}
|
|
289
|
+
const macdSeries = (ind.data?.indicators as { macd_series?: { line: number[]; signal: number[] } })
|
|
290
|
+
?.macd_series;
|
|
291
|
+
const line = macdSeries?.line || [];
|
|
292
|
+
const signal = macdSeries?.signal || [];
|
|
293
|
+
const candles = (ind.data?.normalized || []) as Array<{ isoTime?: string | null; close?: number | null }>;
|
|
294
|
+
const n = line.length;
|
|
295
|
+
if (n < 2) return;
|
|
296
|
+
const start = Math.max(1, n - lookback);
|
|
297
|
+
const cross = detectCrossInRange(line, signal, candles, start, n - 1, n, pair as string);
|
|
298
|
+
if (cross) allDetailed.push(cross);
|
|
299
|
+
} catch {
|
|
300
|
+
failedPairs.push(pair);
|
|
301
|
+
}
|
|
302
|
+
}),
|
|
303
|
+
);
|
|
304
|
+
|
|
305
|
+
const opts = screen || {};
|
|
306
|
+
const crossType = opts.crossType || 'both';
|
|
307
|
+
const totalFound = allDetailed.length;
|
|
308
|
+
let filtered = allDetailed.filter((r) => {
|
|
309
|
+
if (crossType !== 'both' && r.type !== crossType) return false;
|
|
310
|
+
if (
|
|
311
|
+
opts.minHistogramDelta != null &&
|
|
312
|
+
r.histogramDelta != null &&
|
|
313
|
+
Math.abs(r.histogramDelta) < opts.minHistogramDelta
|
|
314
|
+
)
|
|
315
|
+
return false;
|
|
316
|
+
if (opts.maxBarsAgo != null && r.barsAgo > opts.maxBarsAgo) return false;
|
|
317
|
+
if (opts.minReturnPct != null && !(r.returnSinceCrossPct != null && r.returnSinceCrossPct >= opts.minReturnPct))
|
|
318
|
+
return false;
|
|
319
|
+
if (opts.maxReturnPct != null && !(r.returnSinceCrossPct != null && r.returnSinceCrossPct <= opts.maxReturnPct))
|
|
320
|
+
return false;
|
|
321
|
+
return true;
|
|
322
|
+
});
|
|
323
|
+
|
|
324
|
+
const sortBy = opts.sortBy || 'date';
|
|
325
|
+
const order = (opts.sortOrder || 'desc') === 'desc' ? -1 : 1;
|
|
326
|
+
const safeNum = (v: unknown, def = 0) => (v == null || Number.isNaN(Number(v)) ? def : Number(v));
|
|
327
|
+
const projReturn = (v: unknown) => (v == null ? Number.NEGATIVE_INFINITY : Number(v));
|
|
328
|
+
filtered.sort((a, b) => {
|
|
329
|
+
if (sortBy === 'histogram')
|
|
330
|
+
return (Math.abs(safeNum(b.histogramDelta)) - Math.abs(safeNum(a.histogramDelta))) * (order === -1 ? 1 : -1);
|
|
331
|
+
if (sortBy === 'return')
|
|
332
|
+
return (projReturn(b.returnSinceCrossPct) - projReturn(a.returnSinceCrossPct)) * (order === -1 ? 1 : -1);
|
|
333
|
+
if (sortBy === 'barsAgo') return (safeNum(a.barsAgo) - safeNum(b.barsAgo)) * (order === -1 ? 1 : -1);
|
|
334
|
+
// sortBy === 'date': compare crossDate strings directly
|
|
335
|
+
const dateA = a.crossDate || '';
|
|
336
|
+
const dateB = b.crossDate || '';
|
|
337
|
+
return dateA < dateB ? -1 * order : dateA > dateB ? 1 * order : 0;
|
|
338
|
+
});
|
|
339
|
+
if (opts.limit != null && opts.limit > 0) filtered = filtered.slice(0, opts.limit);
|
|
340
|
+
|
|
341
|
+
const resultsScreened = filtered.map((r) => ({
|
|
342
|
+
pair: r.pair,
|
|
343
|
+
type: r.type,
|
|
344
|
+
macd: r.macdAtCross as number,
|
|
345
|
+
signal: r.signalAtCross as number,
|
|
346
|
+
isoTime: r.crossDate,
|
|
347
|
+
}));
|
|
348
|
+
const brief = resultsScreened
|
|
349
|
+
.slice(0, 6)
|
|
350
|
+
.map((r) => `${r.pair}:${r.type}${r.isoTime ? `@${String(r.isoTime).slice(0, 10)}` : ''}`)
|
|
351
|
+
.join(', ');
|
|
352
|
+
const conds: string[] = [];
|
|
353
|
+
if (crossType !== 'both') conds.push(crossType);
|
|
354
|
+
if (opts.minHistogramDelta != null) conds.push(`ヒストグラム≥${opts.minHistogramDelta}`);
|
|
355
|
+
if (opts.maxBarsAgo != null) conds.push(`bars≤${opts.maxBarsAgo}`);
|
|
356
|
+
if (opts.minReturnPct != null) conds.push(`return≥${opts.minReturnPct}%`);
|
|
357
|
+
if (opts.maxReturnPct != null) conds.push(`return≤${opts.maxReturnPct}%`);
|
|
358
|
+
if (opts.limit != null) conds.push(`top${opts.limit}`);
|
|
359
|
+
const failedInfo = failedPairs.length > 0 ? ` | ⚠️${failedPairs.length}/${universe.length}ペア取得失敗` : '';
|
|
360
|
+
const condStr = conds.length ? ` (全${totalFound}件中, 条件: ${conds.join(', ')})` : '';
|
|
361
|
+
const baseSummary = formatSummary({
|
|
362
|
+
pair: 'multi',
|
|
363
|
+
latest: undefined,
|
|
364
|
+
extra: `crosses=${resultsScreened.length}${condStr}${failedInfo}${brief ? ` [${brief}]` : ''}`,
|
|
365
|
+
});
|
|
366
|
+
const screenCrosses: MacdScreenCross[] = filtered.map((r) => ({
|
|
367
|
+
pair: r.pair,
|
|
368
|
+
type: r.type,
|
|
369
|
+
crossDate: r.crossDate,
|
|
370
|
+
barsAgo: r.barsAgo,
|
|
371
|
+
macdAtCross: r.macdAtCross,
|
|
372
|
+
signalAtCross: r.signalAtCross,
|
|
373
|
+
histogramDelta: r.histogramDelta,
|
|
374
|
+
returnSinceCrossPct: r.returnSinceCrossPct,
|
|
375
|
+
prevCross: r.prevCross ? { type: r.prevCross.type, barsAgo: r.prevCross.barsAgo } : null,
|
|
376
|
+
}));
|
|
377
|
+
const summary = buildMacdScreenText({
|
|
378
|
+
baseSummary,
|
|
379
|
+
crosses: screenCrosses,
|
|
380
|
+
includeForming: false,
|
|
381
|
+
includeStats: false,
|
|
382
|
+
});
|
|
383
|
+
const data: Record<string, unknown> = { results: resultsScreened };
|
|
384
|
+
if (view === 'detailed') {
|
|
385
|
+
data.resultsDetailed = allDetailed;
|
|
386
|
+
data.screenedDetailed = filtered;
|
|
387
|
+
}
|
|
388
|
+
const meta: Record<string, unknown> = {
|
|
389
|
+
market,
|
|
390
|
+
lookback,
|
|
391
|
+
pairs: universe,
|
|
392
|
+
view,
|
|
393
|
+
screen: { ...opts, crossType, sortBy, sortOrder: opts.sortOrder || 'desc' },
|
|
394
|
+
};
|
|
395
|
+
if (failedPairs.length > 0) {
|
|
396
|
+
meta.warning = `⚠️ ${universe.length}ペア中${failedPairs.length}ペアの指標取得に失敗しました: ${failedPairs.join(', ')}`;
|
|
397
|
+
meta.failedPairs = failedPairs;
|
|
398
|
+
}
|
|
399
|
+
return ok(summary, data, meta);
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
// ── モード B: 単一ペア深掘り(forming + 過去統計) ──
|
|
403
|
+
|
|
404
|
+
type CrossPerf = { date: string | null; histogram: number | null; performance: Record<string, number | null> };
|
|
405
|
+
|
|
406
|
+
async function singlePairMode(
|
|
407
|
+
pair: string,
|
|
408
|
+
includeForming: boolean,
|
|
409
|
+
includeStats: boolean,
|
|
410
|
+
historyDays: number,
|
|
411
|
+
performanceWindows: number[],
|
|
412
|
+
minHistogramForForming: number,
|
|
413
|
+
) {
|
|
414
|
+
const limit = Math.max(120, historyDays + 40);
|
|
415
|
+
const ind = await analyzeIndicators(pair, '1day', limit);
|
|
416
|
+
if (!ind?.ok) return fail(ind?.summary || 'indicators failed', ind?.meta?.errorType || 'internal');
|
|
417
|
+
|
|
418
|
+
const macd = ind?.data?.indicators?.macd_series?.line || [];
|
|
419
|
+
const signal = ind?.data?.indicators?.macd_series?.signal || [];
|
|
420
|
+
const hist = ind?.data?.indicators?.macd_series?.hist || [];
|
|
421
|
+
const candles: Array<{ isoTime?: string | null; close?: number }> = Array.isArray(ind?.data?.normalized)
|
|
422
|
+
? ind.data.normalized
|
|
423
|
+
: [];
|
|
424
|
+
const n = Math.min(macd.length, signal.length, hist.length, candles.length);
|
|
425
|
+
if (n < 20) return fail('insufficient data', 'user');
|
|
426
|
+
|
|
427
|
+
const nowIdx = n - 1;
|
|
428
|
+
const lastClose = candles[nowIdx]?.close ?? null;
|
|
429
|
+
|
|
430
|
+
// forming detection
|
|
431
|
+
let forming: FormingStatus | null = null;
|
|
432
|
+
if (includeForming) {
|
|
433
|
+
const win = Math.min(5, n - 1);
|
|
434
|
+
const hNow = hist[nowIdx] as number | null;
|
|
435
|
+
const hPrev = hist[nowIdx - win] as number | null;
|
|
436
|
+
let completion: number | null = null;
|
|
437
|
+
let estimatedCrossDays: number | null = null;
|
|
438
|
+
let status: 'forming_golden' | 'forming_dead' | 'neutral' | 'crossed_recently' = 'neutral';
|
|
439
|
+
const histogramTrend: Array<number | null> = [];
|
|
440
|
+
for (let i = nowIdx - win + 1; i <= nowIdx; i++)
|
|
441
|
+
histogramTrend.push(hist[i] == null ? null : Number((hist[i] as number).toFixed(4)));
|
|
442
|
+
|
|
443
|
+
if (hNow != null && hPrev != null && Math.abs(hPrev) > 0) {
|
|
444
|
+
const slopePerBar = (hNow - hPrev) / win;
|
|
445
|
+
const movingTowardZero = (hPrev > 0 && slopePerBar < 0) || (hPrev < 0 && slopePerBar > 0);
|
|
446
|
+
const notCrossedYet = (hPrev < 0 && hNow < 0) || (hPrev > 0 && hNow > 0);
|
|
447
|
+
if (movingTowardZero && notCrossedYet && Math.abs(hNow) <= minHistogramForForming * 5) {
|
|
448
|
+
completion = Number((1 - Math.min(1, Math.abs(hNow) / Math.abs(hPrev))).toFixed(2));
|
|
449
|
+
const speed = Math.abs(slopePerBar);
|
|
450
|
+
estimatedCrossDays = speed > 0 ? Number((Math.abs(hNow) / speed).toFixed(1)) : null;
|
|
451
|
+
status = hPrev < 0 ? 'forming_golden' : 'forming_dead';
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
let lastCrossIdx: number | null = null;
|
|
456
|
+
let lastCrossType: 'golden' | 'dead' | null = null;
|
|
457
|
+
for (let i = nowIdx; i >= Math.max(1, nowIdx - 3); i--) {
|
|
458
|
+
const hp = hist[i - 1];
|
|
459
|
+
const hc = hist[i];
|
|
460
|
+
if (hp != null && hc != null) {
|
|
461
|
+
if (hp <= 0 && hc > 0) {
|
|
462
|
+
lastCrossIdx = i;
|
|
463
|
+
lastCrossType = 'golden';
|
|
464
|
+
break;
|
|
465
|
+
}
|
|
466
|
+
if (hp >= 0 && hc < 0) {
|
|
467
|
+
lastCrossIdx = i;
|
|
468
|
+
lastCrossType = 'dead';
|
|
469
|
+
break;
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
}
|
|
473
|
+
if (status !== 'forming_golden' && status !== 'forming_dead' && lastCrossIdx != null) status = 'crossed_recently';
|
|
474
|
+
|
|
475
|
+
forming = {
|
|
476
|
+
status,
|
|
477
|
+
completion,
|
|
478
|
+
estimatedCrossDays,
|
|
479
|
+
currentMACD: macd[nowIdx] ?? null,
|
|
480
|
+
currentSignal: signal[nowIdx] ?? null,
|
|
481
|
+
currentHistogram: hNow ?? null,
|
|
482
|
+
histogramTrend,
|
|
483
|
+
...(status === 'crossed_recently' && lastCrossIdx != null
|
|
484
|
+
? {
|
|
485
|
+
lastCrossType,
|
|
486
|
+
lastCrossDate: candles[lastCrossIdx]?.isoTime ?? null,
|
|
487
|
+
lastCrossBarsAgo: nowIdx - lastCrossIdx,
|
|
488
|
+
}
|
|
489
|
+
: {}),
|
|
490
|
+
};
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
// history + statistics
|
|
494
|
+
let history: CrossHistory | null = null;
|
|
495
|
+
let statistics: { golden: CrossStats; dead: CrossStats } | null = null;
|
|
496
|
+
if (includeStats) {
|
|
497
|
+
const msCut = dayjs().subtract(historyDays, 'day').valueOf();
|
|
498
|
+
const crosses: Array<{
|
|
499
|
+
idx: number;
|
|
500
|
+
type: 'golden' | 'dead';
|
|
501
|
+
date: string | null;
|
|
502
|
+
histogram: number | null;
|
|
503
|
+
price: number | null;
|
|
504
|
+
}> = [];
|
|
505
|
+
for (let i = 1; i < n; i++) {
|
|
506
|
+
const prevDiff = diffAt(macd, signal, i - 1);
|
|
507
|
+
const currDiff = diffAt(macd, signal, i);
|
|
508
|
+
if (prevDiff == null || currDiff == null) continue;
|
|
509
|
+
const isGolden = prevDiff <= 0 && currDiff > 0;
|
|
510
|
+
const isDead = prevDiff >= 0 && currDiff < 0;
|
|
511
|
+
if (!isGolden && !isDead) continue;
|
|
512
|
+
const dateStr = candles[i]?.isoTime || null;
|
|
513
|
+
const ts = dateStr ? dayjs(dateStr).valueOf() : NaN;
|
|
514
|
+
if (!Number.isFinite(ts) || ts < msCut) continue;
|
|
515
|
+
crosses.push({
|
|
516
|
+
idx: i,
|
|
517
|
+
type: isGolden ? 'golden' : 'dead',
|
|
518
|
+
date: dateStr,
|
|
519
|
+
histogram: hist[i] ?? null,
|
|
520
|
+
price: candles[i]?.close ?? null,
|
|
521
|
+
});
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
const performanceFor = (idx: number, basePrice: number | null): Record<string, number | null> => {
|
|
525
|
+
const perf: Record<string, number | null> = {};
|
|
526
|
+
for (const w of performanceWindows) {
|
|
527
|
+
const j = Math.min(n - 1, idx + w);
|
|
528
|
+
const priceW = candles[j]?.close ?? null;
|
|
529
|
+
perf[`day${String(w)}`] =
|
|
530
|
+
basePrice != null && priceW != null ? Number((((priceW - basePrice) / basePrice) * 100).toFixed(2)) : null;
|
|
531
|
+
}
|
|
532
|
+
return perf;
|
|
533
|
+
};
|
|
534
|
+
|
|
535
|
+
const goldenCrosses: CrossPerf[] = [];
|
|
536
|
+
const deadCrosses: CrossPerf[] = [];
|
|
537
|
+
for (const c of crosses) {
|
|
538
|
+
const perf = performanceFor(c.idx, c.price ?? null);
|
|
539
|
+
const item: CrossPerf = {
|
|
540
|
+
date: c.date,
|
|
541
|
+
histogram: c.histogram == null ? null : Number((c.histogram as number).toFixed(4)),
|
|
542
|
+
performance: perf,
|
|
543
|
+
};
|
|
544
|
+
if (c.type === 'golden') goldenCrosses.push(item);
|
|
545
|
+
else deadCrosses.push(item);
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
const statsOf = (list: CrossPerf[]) => {
|
|
549
|
+
const w = performanceWindows.includes(5) ? 5 : performanceWindows[performanceWindows.length - 1];
|
|
550
|
+
const pick = (it: CrossPerf) => it.performance[`day${String(w)}`];
|
|
551
|
+
const vals = list.map(pick).filter((v): v is number => v != null);
|
|
552
|
+
const avg = vals.length ? Number((vals.reduce((a, b) => a + b, 0) / vals.length).toFixed(2)) : null;
|
|
553
|
+
const successRate = list.length
|
|
554
|
+
? Number(((list.filter((p) => (pick(p) ?? -Infinity) > 0).length / list.length) * 100).toFixed(0))
|
|
555
|
+
: 0;
|
|
556
|
+
const bestCase = vals.length ? Math.max(...vals) : null;
|
|
557
|
+
const worstCase = vals.length ? Math.min(...vals) : null;
|
|
558
|
+
return { avgDay5Return: avg, successRate, totalSamples: list.length, bestCase, worstCase };
|
|
559
|
+
};
|
|
560
|
+
|
|
561
|
+
history = { goldenCrosses, deadCrosses };
|
|
562
|
+
statistics = { golden: statsOf(goldenCrosses), dead: statsOf(deadCrosses) };
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
// Build summary text
|
|
566
|
+
const summaryText = buildMacdSingleText({
|
|
567
|
+
pair,
|
|
568
|
+
lastClose,
|
|
569
|
+
forming,
|
|
570
|
+
statistics,
|
|
571
|
+
history,
|
|
572
|
+
historyDays,
|
|
573
|
+
includeForming,
|
|
574
|
+
includeStats,
|
|
575
|
+
});
|
|
576
|
+
|
|
577
|
+
return ok(
|
|
578
|
+
summaryText,
|
|
579
|
+
{ forming, history, statistics },
|
|
580
|
+
{ pair, historyDays, performanceWindows, minHistogramForForming, includeForming, includeStats },
|
|
581
|
+
);
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
// ── MCP ツール定義(tool-registry から自動収集) ──
|
|
585
|
+
|
|
586
|
+
export const toolDef: ToolDefinition = {
|
|
587
|
+
name: 'detect_macd_cross',
|
|
588
|
+
description: `[MACD Cross / Crossover / Screening] MACDクロス検出(MACD cross / crossover / golden cross / dead cross / screening)。
|
|
589
|
+
|
|
590
|
+
pair省略: 複数銘柄スクリーニング / pair指定: 単一ペア深掘り分析(forming検出・過去統計)。
|
|
591
|
+
|
|
592
|
+
screen(スクリーニング用):
|
|
593
|
+
- crossType: golden|dead|both
|
|
594
|
+
- minHistogramDelta / maxBarsAgo / minReturnPct / maxReturnPct
|
|
595
|
+
- sortBy: date|histogram|return|barsAgo
|
|
596
|
+
- limit: 上位N件`,
|
|
597
|
+
inputSchema: z.object({
|
|
598
|
+
pair: z.string().optional().describe('指定時は単一ペア深掘りモード'),
|
|
599
|
+
market: z.enum(['all', 'jpy']).default('all').describe('スクリーニング時の対象市場'),
|
|
600
|
+
pairs: z.array(z.string()).optional().describe('スクリーニング時の対象ペア限定'),
|
|
601
|
+
lookback: z.number().int().min(1).max(10).default(3),
|
|
602
|
+
view: z.enum(['summary', 'detailed']).optional().default('summary'),
|
|
603
|
+
includeForming: z.boolean().optional().default(true).describe('単一ペア: forming検出'),
|
|
604
|
+
includeStats: z.boolean().optional().default(true).describe('単一ペア: 過去統計'),
|
|
605
|
+
historyDays: z.number().int().min(10).max(365).optional().default(90).describe('単一ペア: 統計対象期間'),
|
|
606
|
+
performanceWindows: z.array(z.number().int().min(1).max(30)).optional().default([1, 3, 5, 10]),
|
|
607
|
+
minHistogramForForming: z.number().min(0).optional().default(0.3),
|
|
608
|
+
screen: z
|
|
609
|
+
.object({
|
|
610
|
+
minHistogramDelta: z.number().optional(),
|
|
611
|
+
maxBarsAgo: z.number().int().min(0).optional(),
|
|
612
|
+
minReturnPct: z.number().optional(),
|
|
613
|
+
maxReturnPct: z.number().optional(),
|
|
614
|
+
crossType: z.enum(['golden', 'dead', 'both']).optional().default('both'),
|
|
615
|
+
sortBy: z.enum(['date', 'histogram', 'return', 'barsAgo']).optional().default('date'),
|
|
616
|
+
sortOrder: z.enum(['asc', 'desc']).optional().default('desc'),
|
|
617
|
+
limit: z.number().int().min(1).max(100).optional(),
|
|
618
|
+
withPrice: z.boolean().optional(),
|
|
619
|
+
})
|
|
620
|
+
.optional(),
|
|
621
|
+
}),
|
|
622
|
+
handler: async (args: {
|
|
623
|
+
pair?: string;
|
|
624
|
+
market?: 'all' | 'jpy';
|
|
625
|
+
pairs?: string[];
|
|
626
|
+
lookback?: number;
|
|
627
|
+
view?: 'summary' | 'detailed';
|
|
628
|
+
includeForming?: boolean;
|
|
629
|
+
includeStats?: boolean;
|
|
630
|
+
historyDays?: number;
|
|
631
|
+
performanceWindows?: number[];
|
|
632
|
+
minHistogramForForming?: number;
|
|
633
|
+
screen?: {
|
|
634
|
+
minHistogramDelta?: number;
|
|
635
|
+
maxBarsAgo?: number;
|
|
636
|
+
minReturnPct?: number;
|
|
637
|
+
maxReturnPct?: number;
|
|
638
|
+
crossType?: 'golden' | 'dead' | 'both';
|
|
639
|
+
sortBy?: 'date' | 'histogram' | 'return' | 'barsAgo';
|
|
640
|
+
sortOrder?: 'asc' | 'desc';
|
|
641
|
+
limit?: number;
|
|
642
|
+
withPrice?: boolean;
|
|
643
|
+
};
|
|
644
|
+
}) => {
|
|
645
|
+
try {
|
|
646
|
+
if (args.pair) {
|
|
647
|
+
const chk = ensurePair(args.pair);
|
|
648
|
+
if (!chk.ok) return failFromValidation(chk);
|
|
649
|
+
return singlePairMode(
|
|
650
|
+
chk.pair,
|
|
651
|
+
args.includeForming ?? true,
|
|
652
|
+
args.includeStats ?? true,
|
|
653
|
+
args.historyDays ?? 90,
|
|
654
|
+
args.performanceWindows ?? [1, 3, 5, 10],
|
|
655
|
+
args.minHistogramForForming ?? 0.3,
|
|
656
|
+
);
|
|
657
|
+
}
|
|
658
|
+
const res: Awaited<ReturnType<typeof screenMode>> = await screenMode(
|
|
659
|
+
args.market ?? 'all',
|
|
660
|
+
args.lookback ?? 3,
|
|
661
|
+
args.pairs,
|
|
662
|
+
args.view ?? 'summary',
|
|
663
|
+
args.screen,
|
|
664
|
+
);
|
|
665
|
+
if (!res?.ok || args.view !== 'detailed') return res;
|
|
666
|
+
try {
|
|
667
|
+
const detRaw: CrossDetailed[] = Array.isArray(res?.data?.screenedDetailed)
|
|
668
|
+
? (res.data.screenedDetailed as CrossDetailed[])
|
|
669
|
+
: Array.isArray(res?.data?.resultsDetailed)
|
|
670
|
+
? (res.data.resultsDetailed as CrossDetailed[])
|
|
671
|
+
: [];
|
|
672
|
+
if (!detRaw.length) return res;
|
|
673
|
+
const fmtDelta = (v: number | null | undefined) =>
|
|
674
|
+
v == null ? 'n/a' : `${v >= 0 ? '+' : ''}${Number(v).toFixed(2)}`;
|
|
675
|
+
const fmtRet = (v: number | null | undefined) =>
|
|
676
|
+
v == null ? 'n/a' : `${v >= 0 ? '+' : ''}${Number(v).toFixed(2)}%`;
|
|
677
|
+
const lines = detRaw.map((r) => {
|
|
678
|
+
const date = (r?.crossDate || '').slice(0, 10);
|
|
679
|
+
const prevDays = r?.prevCross?.barsAgo != null ? `${r.prevCross.barsAgo}日` : 'n/a';
|
|
680
|
+
return `${String(r.pair)}: ${String(r.type)}@${date} (ヒストグラム${fmtDelta(r?.histogramDelta)}, 前回クロスから${prevDays}${r?.returnSinceCrossPct != null ? `, ${fmtRet(r.returnSinceCrossPct)}` : ''})`;
|
|
681
|
+
});
|
|
682
|
+
const text = `${String(res?.summary || '')}\n${lines.join('\n')}`.trim();
|
|
683
|
+
return { content: [{ type: 'text', text }], structuredContent: toStructured(res) };
|
|
684
|
+
} catch {
|
|
685
|
+
return res;
|
|
686
|
+
}
|
|
687
|
+
} catch (e: unknown) {
|
|
688
|
+
return failFromError(e);
|
|
689
|
+
}
|
|
690
|
+
},
|
|
691
|
+
};
|