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,665 @@
|
|
|
1
|
+
import type { z } from 'zod';
|
|
2
|
+
import { formatPercent, formatPriceJPY, formatSummary } from '../lib/formatter.js';
|
|
3
|
+
import { slidingMean } from '../lib/math.js';
|
|
4
|
+
import { fail, failFromError, failFromValidation, ok } from '../lib/result.js';
|
|
5
|
+
import { createMeta, ensurePair } from '../lib/validate.js';
|
|
6
|
+
import {
|
|
7
|
+
type AnalyzeMarketSignalDataSchemaOut,
|
|
8
|
+
type AnalyzeMarketSignalMetaSchemaOut,
|
|
9
|
+
AnalyzeMarketSignalOutputSchema,
|
|
10
|
+
} from '../src/schemas.js';
|
|
11
|
+
import analyzeIndicators from './analyze_indicators.js';
|
|
12
|
+
import getFlowMetrics from './get_flow_metrics.js';
|
|
13
|
+
import getVolatilityMetrics from './get_volatility_metrics.js';
|
|
14
|
+
|
|
15
|
+
// ── buildMarketSignalText ──────────────────────────────────────
|
|
16
|
+
|
|
17
|
+
export type BuildMarketSignalTextInput = {
|
|
18
|
+
pair: string;
|
|
19
|
+
type: string;
|
|
20
|
+
score: number;
|
|
21
|
+
recommendation: string;
|
|
22
|
+
confidence: { level: string; reason: string };
|
|
23
|
+
latestClose: number | null | undefined;
|
|
24
|
+
sma: {
|
|
25
|
+
sma25: number | null | undefined;
|
|
26
|
+
sma75: number | null | undefined;
|
|
27
|
+
sma200: number | null | undefined;
|
|
28
|
+
};
|
|
29
|
+
smaArrangement: 'bullish' | 'bearish' | 'mixed';
|
|
30
|
+
smaPosition: 'above_all' | 'below_all' | 'mixed';
|
|
31
|
+
smaDeviations: { vs25?: number; vs75?: number; vs200?: number };
|
|
32
|
+
recentCross: { type: 'golden_cross' | 'death_cross'; pair: string; barsAgo: number } | null;
|
|
33
|
+
factors: {
|
|
34
|
+
smaTrendFactor: number;
|
|
35
|
+
momentumFactor: number;
|
|
36
|
+
cvdTrend: number;
|
|
37
|
+
volatilityFactor: number;
|
|
38
|
+
buyPressure: number;
|
|
39
|
+
};
|
|
40
|
+
contributions: {
|
|
41
|
+
sma: number;
|
|
42
|
+
mom: number;
|
|
43
|
+
cvd: number;
|
|
44
|
+
vol: number;
|
|
45
|
+
buy: number;
|
|
46
|
+
};
|
|
47
|
+
rsi: number | null;
|
|
48
|
+
rvNum: number;
|
|
49
|
+
buyRatio: number;
|
|
50
|
+
nextActions: Array<{ priority: string; tool: string; reason: string; suggestedParams?: Record<string, unknown> }>;
|
|
51
|
+
alerts: Array<{ level: string; message: string }>;
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
export function buildMarketSignalText(input: BuildMarketSignalTextInput): string {
|
|
55
|
+
const {
|
|
56
|
+
pair,
|
|
57
|
+
type,
|
|
58
|
+
score,
|
|
59
|
+
recommendation,
|
|
60
|
+
confidence,
|
|
61
|
+
latestClose,
|
|
62
|
+
sma,
|
|
63
|
+
smaArrangement,
|
|
64
|
+
smaPosition,
|
|
65
|
+
smaDeviations,
|
|
66
|
+
recentCross,
|
|
67
|
+
factors,
|
|
68
|
+
contributions,
|
|
69
|
+
rsi,
|
|
70
|
+
rvNum,
|
|
71
|
+
buyRatio,
|
|
72
|
+
nextActions,
|
|
73
|
+
alerts,
|
|
74
|
+
} = input;
|
|
75
|
+
const { smaTrendFactor, momentumFactor, cvdTrend, volatilityFactor, buyPressure } = factors;
|
|
76
|
+
const { sma25, sma75, sma200 } = sma;
|
|
77
|
+
|
|
78
|
+
const score100 = Math.round(score * 100);
|
|
79
|
+
const priceNowStr = formatPriceJPY(latestClose);
|
|
80
|
+
const relToNow = (smaVal?: number | null) => {
|
|
81
|
+
if (smaVal == null || latestClose == null || latestClose === 0) return 'n/a';
|
|
82
|
+
const rel = ((smaVal - latestClose) / latestClose) * 100;
|
|
83
|
+
return `${formatPercent(rel, { sign: true, digits: 2 })}${rel >= 0 ? '上' : '下'}`;
|
|
84
|
+
};
|
|
85
|
+
const sma25Line = sma25 != null ? `${formatPriceJPY(sma25)}(現在より${relToNow(sma25)})` : 'n/a';
|
|
86
|
+
const sma75Line = sma75 != null ? `${formatPriceJPY(sma75)}(現在より${relToNow(sma75)})` : 'n/a';
|
|
87
|
+
const sma200Line = sma200 != null ? `${formatPriceJPY(sma200)}(現在より${relToNow(sma200)})` : 'n/a';
|
|
88
|
+
const arrangementStr =
|
|
89
|
+
smaArrangement === 'bullish'
|
|
90
|
+
? '上向き(短期 > 長期)'
|
|
91
|
+
: smaArrangement === 'bearish'
|
|
92
|
+
? '下向き(短期 < 長期)'
|
|
93
|
+
: '混在';
|
|
94
|
+
|
|
95
|
+
const toState = (v: number) => (v > 0.1 ? 'up' : v < -0.1 ? 'down' : 'flat');
|
|
96
|
+
const momentumState = toState(momentumFactor);
|
|
97
|
+
const cvdState = toState(cvdTrend);
|
|
98
|
+
|
|
99
|
+
const buyLabel =
|
|
100
|
+
buyPressure > 0.2
|
|
101
|
+
? '買い優勢'
|
|
102
|
+
: buyPressure > 0.05
|
|
103
|
+
? 'やや買い優勢'
|
|
104
|
+
: buyPressure < -0.2
|
|
105
|
+
? '売り優勢'
|
|
106
|
+
: buyPressure < -0.05
|
|
107
|
+
? 'やや売り優勢'
|
|
108
|
+
: '拮抗';
|
|
109
|
+
const cvdLabel = cvdState === 'up' ? '上昇中' : cvdState === 'down' ? '下降中' : '横ばい';
|
|
110
|
+
const momLabel = momentumState === 'up' ? '上昇中' : momentumState === 'down' ? '下降中' : '横ばい';
|
|
111
|
+
const volLabel = volatilityFactor > 0.2 ? '落ち着いている' : volatilityFactor < -0.2 ? '荒い' : '中庸';
|
|
112
|
+
|
|
113
|
+
const nextLines = nextActions.slice(0, 2).map((a, i) => {
|
|
114
|
+
const num = `${i + 1}.`;
|
|
115
|
+
const params = a.suggestedParams ? ` ${JSON.stringify(a.suggestedParams)}` : '';
|
|
116
|
+
return `${num} ${a.tool}${params}`;
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
const orderStr = (() => {
|
|
120
|
+
if (latestClose == null || sma25 == null || sma75 == null || sma200 == null) return '';
|
|
121
|
+
if (smaArrangement === 'bearish') return '200 > 75 > 25 > 現在価格';
|
|
122
|
+
if (smaArrangement === 'bullish') return '現在価格 > 25 > 75 > 200';
|
|
123
|
+
return '';
|
|
124
|
+
})();
|
|
125
|
+
const trendLabel = smaArrangement === 'bearish' ? '弱気' : smaArrangement === 'bullish' ? '強気' : '不明瞭';
|
|
126
|
+
const positionLabel = (() => {
|
|
127
|
+
if (smaPosition === 'above_all') return '全平均の上';
|
|
128
|
+
if (smaPosition === 'below_all') return '全平均の下';
|
|
129
|
+
return '一部の平均と交差';
|
|
130
|
+
})();
|
|
131
|
+
const crossLine = (() => {
|
|
132
|
+
if (!recentCross) return '';
|
|
133
|
+
const jpType = recentCross.type === 'golden_cross' ? 'ゴールデンクロス' : 'デッドクロス';
|
|
134
|
+
const action = recentCross.type === 'golden_cross' ? '上抜け' : '下抜け';
|
|
135
|
+
const ago = recentCross.barsAgo || 0;
|
|
136
|
+
return `直近クロス: ${ago}日前に${jpType}(25日が75日を${action})`;
|
|
137
|
+
})();
|
|
138
|
+
|
|
139
|
+
return [
|
|
140
|
+
`${String(pair).toUpperCase()} [${String(type)}]`,
|
|
141
|
+
`総合スコア: ${score100}(${recommendation}、信頼度: ${confidence.level})`,
|
|
142
|
+
`※ トレンド重視型(中長期35%+30% / 短期20% / 瞬間5%)`,
|
|
143
|
+
'',
|
|
144
|
+
'【価格情報】',
|
|
145
|
+
`現在価格: ${priceNowStr}`,
|
|
146
|
+
'',
|
|
147
|
+
'【SMA詳細】',
|
|
148
|
+
`- 短期(25日平均): ${sma25Line}`,
|
|
149
|
+
`- 中期(75日平均): ${sma75Line}`,
|
|
150
|
+
`- 長期(200日平均): ${sma200Line}`,
|
|
151
|
+
`配置: ${smaArrangement === 'bearish' ? '下降順' : smaArrangement === 'bullish' ? '上昇順' : '混在'}${orderStr ? `(${orderStr})` : ''} → トレンド: ${trendLabel}`,
|
|
152
|
+
`位置: ${positionLabel}`,
|
|
153
|
+
...(crossLine ? [crossLine] : []),
|
|
154
|
+
'',
|
|
155
|
+
'【各要素の詳細】',
|
|
156
|
+
`- 平均価格の配置(重み35%): ${smaTrendFactor.toFixed(2)}(${arrangementStr})`,
|
|
157
|
+
`- 勢いの変化(重み30%): ${momentumFactor.toFixed(2)}(${momLabel}${rsi != null ? `、RSI=${Math.round(rsi)}` : ''})`,
|
|
158
|
+
`- 出来高の流れ(重み20%): ${cvdTrend.toFixed(2)}(${cvdLabel})`,
|
|
159
|
+
`- 値動きの荒さ(重み10%): ${volatilityFactor.toFixed(2)}(${volLabel})`,
|
|
160
|
+
`- 板の買い圧力(重み5%): ${buyPressure.toFixed(2)}(${buyLabel})`,
|
|
161
|
+
'',
|
|
162
|
+
'【次の確認推奨】',
|
|
163
|
+
...(nextLines.length ? nextLines : ['- 該当なし']),
|
|
164
|
+
'',
|
|
165
|
+
'【数値詳細】',
|
|
166
|
+
`contributions: sma=${contributions.sma.toFixed(3)} mom=${contributions.mom.toFixed(3)} cvd=${contributions.cvd.toFixed(3)} vol=${contributions.vol.toFixed(3)} buy=${contributions.buy.toFixed(3)}`,
|
|
167
|
+
`rawValues: smaTrend=${smaTrendFactor.toFixed(3)} momentum=${momentumFactor.toFixed(3)} cvdTrend=${cvdTrend.toFixed(3)} volatility=${volatilityFactor.toFixed(3)} buyPressure=${buyPressure.toFixed(3)}`,
|
|
168
|
+
`confidence: ${confidence.level} (${confidence.reason})`,
|
|
169
|
+
`RSI: ${rsi ?? 'n/a'} | rv_ann: ${rvNum.toFixed(4)} | aggRatio: ${buyRatio.toFixed(3)}`,
|
|
170
|
+
...(smaDeviations.vs25 != null
|
|
171
|
+
? [
|
|
172
|
+
`SMA乖離: vs25=${(smaDeviations.vs25 * 100).toFixed(2)}% vs75=${smaDeviations.vs75 != null ? (smaDeviations.vs75 * 100).toFixed(2) : 'n/a'}% vs200=${smaDeviations.vs200 != null ? (smaDeviations.vs200 * 100).toFixed(2) : 'n/a'}%`,
|
|
173
|
+
]
|
|
174
|
+
: []),
|
|
175
|
+
...(recentCross ? [`SMAクロス: ${recentCross.type} ${recentCross.pair} ${recentCross.barsAgo}bars前`] : []),
|
|
176
|
+
...(alerts.length ? [`alerts: ${alerts.map((a) => `[${a.level}] ${a.message}`).join('; ')}`] : []),
|
|
177
|
+
'',
|
|
178
|
+
'---',
|
|
179
|
+
'📌 含まれるもの: 総合スコア・各要素の寄与度と生値・SMA配置・信頼度・推奨アクション',
|
|
180
|
+
'📌 含まれないもの: 指標の時系列詳細、個別約定データ、チャートパターン検出、板の層別分析',
|
|
181
|
+
'📌 補完ツール: get_flow_metrics(フロー詳細), get_volatility_metrics(ボラ詳細), analyze_indicators(指標詳細), detect_patterns(パターン), get_orderbook(板情報)',
|
|
182
|
+
].join('\n');
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
// ── main function ──────────────────────────────────────────────
|
|
186
|
+
|
|
187
|
+
type AnalyzeOpts = {
|
|
188
|
+
type?: string;
|
|
189
|
+
flowLimit?: number;
|
|
190
|
+
bucketMs?: number;
|
|
191
|
+
windows?: number[];
|
|
192
|
+
horizonBuckets?: number;
|
|
193
|
+
};
|
|
194
|
+
|
|
195
|
+
function clamp(x: number, min: number, max: number) {
|
|
196
|
+
return Math.max(min, Math.min(max, x));
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
export default async function analyzeMarketSignal(pair: string = 'btc_jpy', opts: AnalyzeOpts = {}) {
|
|
200
|
+
const chk = ensurePair(pair);
|
|
201
|
+
if (!chk.ok) return failFromValidation(chk, AnalyzeMarketSignalOutputSchema);
|
|
202
|
+
|
|
203
|
+
const type = opts.type || '1day';
|
|
204
|
+
const flowLimit = Math.max(50, Math.min(opts.flowLimit ?? 300, 2000));
|
|
205
|
+
const bucketMs = Math.max(1_000, Math.min(opts.bucketMs ?? 60_000, 3_600_000));
|
|
206
|
+
const windows = (opts.windows?.length ? opts.windows : [14, 20, 30]).slice(0, 3);
|
|
207
|
+
const horizon = Math.max(5, Math.min(opts.horizonBuckets ?? 10, 100));
|
|
208
|
+
|
|
209
|
+
try {
|
|
210
|
+
const [flowRes, volRes, indRes] = await Promise.all([
|
|
211
|
+
getFlowMetrics(chk.pair, flowLimit, undefined, bucketMs),
|
|
212
|
+
getVolatilityMetrics(chk.pair, type, 200, windows, { annualize: true }),
|
|
213
|
+
// SMA25/75/200 を扱うため十分な本数を取得(最低200+バッファ)
|
|
214
|
+
analyzeIndicators(chk.pair, type, 220),
|
|
215
|
+
]);
|
|
216
|
+
|
|
217
|
+
if (!flowRes?.ok)
|
|
218
|
+
return AnalyzeMarketSignalOutputSchema.parse(
|
|
219
|
+
fail(
|
|
220
|
+
flowRes?.summary || 'flow failed',
|
|
221
|
+
(!flowRes.ok && 'errorType' in flowRes.meta ? flowRes.meta.errorType : undefined) || 'internal',
|
|
222
|
+
),
|
|
223
|
+
);
|
|
224
|
+
if (!volRes?.ok)
|
|
225
|
+
return AnalyzeMarketSignalOutputSchema.parse(
|
|
226
|
+
fail(
|
|
227
|
+
volRes?.summary || 'vol failed',
|
|
228
|
+
(!volRes.ok && 'errorType' in volRes.meta ? volRes.meta.errorType : undefined) || 'internal',
|
|
229
|
+
),
|
|
230
|
+
);
|
|
231
|
+
if (!indRes?.ok)
|
|
232
|
+
return AnalyzeMarketSignalOutputSchema.parse(
|
|
233
|
+
fail(
|
|
234
|
+
indRes?.summary || 'indicators failed',
|
|
235
|
+
(!indRes.ok && 'errorType' in indRes.meta ? indRes.meta.errorType : undefined) || 'internal',
|
|
236
|
+
),
|
|
237
|
+
);
|
|
238
|
+
|
|
239
|
+
// Flow metrics
|
|
240
|
+
const agg = flowRes.data.aggregates || {};
|
|
241
|
+
const buckets = (flowRes.data.series?.buckets || []) as Array<{ cvd: number }>;
|
|
242
|
+
const cvdSeries = buckets.map((b) => b.cvd);
|
|
243
|
+
const cvdSlice = cvdSeries.slice(-horizon);
|
|
244
|
+
const cvdSlope = cvdSlice.length >= 2 ? cvdSlice[cvdSlice.length - 1] - cvdSlice[0] : 0;
|
|
245
|
+
const cvdNormBase = Math.max(1, Math.max(...cvdSlice.map((v) => Math.abs(v))) || 1);
|
|
246
|
+
const cvdTrend = clamp(cvdSlope / cvdNormBase, -1, 1);
|
|
247
|
+
const buyRatio = typeof agg.aggressorRatio === 'number' ? agg.aggressorRatio : 0.5;
|
|
248
|
+
const buyPressure = clamp((buyRatio - 0.5) * 2, -1, 1);
|
|
249
|
+
|
|
250
|
+
// Volatility
|
|
251
|
+
const rv = volRes?.data?.aggregates?.rv_std_ann ?? volRes?.data?.aggregates?.rv_std;
|
|
252
|
+
const rvNum = typeof rv === 'number' ? rv : 0.5; // typical range ~0.2-0.8
|
|
253
|
+
const volatilityFactor = clamp((0.5 - rvNum) / 0.5, -1, 1); // 低ボラほど +
|
|
254
|
+
|
|
255
|
+
// Indicators
|
|
256
|
+
const rsi = indRes?.data?.indicators?.RSI_14 as number | null;
|
|
257
|
+
const momentumFactor = rsi == null ? 0 : clamp((rsi - 50) / 50, -1, 1);
|
|
258
|
+
// SMA trend factor: price vs SMA25/75 alignment and distance to SMA200
|
|
259
|
+
const latestClose = indRes?.data?.normalized?.at(-1)?.close as number | undefined;
|
|
260
|
+
const sma25 = indRes?.data?.indicators?.SMA_25 as number | null | undefined;
|
|
261
|
+
const sma75 = indRes?.data?.indicators?.SMA_75 as number | null | undefined;
|
|
262
|
+
const sma200 = indRes?.data?.indicators?.SMA_200 as number | null | undefined;
|
|
263
|
+
let smaTrendFactor = 0;
|
|
264
|
+
let smaArrangement: 'bullish' | 'bearish' | 'mixed' = 'mixed';
|
|
265
|
+
let smaDeviations: { vs25?: number; vs75?: number; vs200?: number } = {};
|
|
266
|
+
if (latestClose != null && sma25 != null && sma75 != null) {
|
|
267
|
+
// alignment bonus
|
|
268
|
+
const alignedUp = latestClose > sma25 && (sma25 as number) > (sma75 as number);
|
|
269
|
+
const alignedDown = latestClose < sma25 && (sma25 as number) < (sma75 as number);
|
|
270
|
+
if (alignedUp) smaTrendFactor += 0.6;
|
|
271
|
+
else if (alignedDown) smaTrendFactor -= 0.6;
|
|
272
|
+
smaArrangement = alignedUp ? 'bullish' : alignedDown ? 'bearish' : 'mixed';
|
|
273
|
+
// distance to SMA200 (above -> positive, below -> negative), normalized by 5% band
|
|
274
|
+
if (sma200 != null) {
|
|
275
|
+
const dist = (latestClose - (sma200 as number)) / (sma200 as number);
|
|
276
|
+
smaTrendFactor += clamp(dist / 0.05, -0.4, 0.4);
|
|
277
|
+
}
|
|
278
|
+
smaTrendFactor = clamp(smaTrendFactor, -1, 1);
|
|
279
|
+
// deviations (percent) vs SMA
|
|
280
|
+
const pct = (val: number | null | undefined) =>
|
|
281
|
+
val != null && latestClose != null && val !== 0 ? (latestClose - val) / val : undefined;
|
|
282
|
+
smaDeviations = {
|
|
283
|
+
vs25: pct(sma25 ?? null),
|
|
284
|
+
vs75: pct(sma75 ?? null),
|
|
285
|
+
vs200: pct(sma200 ?? null),
|
|
286
|
+
};
|
|
287
|
+
}
|
|
288
|
+
// SMA position classification relative to all SMAs
|
|
289
|
+
let smaPosition: 'above_all' | 'below_all' | 'mixed' = 'mixed';
|
|
290
|
+
if (latestClose != null && sma25 != null && sma75 != null && sma200 != null) {
|
|
291
|
+
if (latestClose > sma25 && latestClose > sma75 && latestClose > sma200) smaPosition = 'above_all';
|
|
292
|
+
else if (latestClose < sma25 && latestClose < sma75 && latestClose < sma200) smaPosition = 'below_all';
|
|
293
|
+
else smaPosition = 'mixed';
|
|
294
|
+
}
|
|
295
|
+
// Recent cross detection for 25/75 using normalized closes (fallback if indicator series not available)
|
|
296
|
+
let recentCross: { type: 'golden_cross' | 'death_cross'; pair: '25/75'; barsAgo: number } | null = null;
|
|
297
|
+
try {
|
|
298
|
+
const normalized = indRes.data.normalized;
|
|
299
|
+
const closes: number[] = Array.isArray(normalized)
|
|
300
|
+
? normalized.map((c) => Number(c?.close)).filter((v) => Number.isFinite(v))
|
|
301
|
+
: [];
|
|
302
|
+
if (closes.length >= 80) {
|
|
303
|
+
const sma25Series = slidingMean(closes, 25);
|
|
304
|
+
const sma75Series = slidingMean(closes, 75);
|
|
305
|
+
const m = Math.min(sma25Series.length, sma75Series.length);
|
|
306
|
+
const off = closes.length - m; // alignment offset to original closes indices
|
|
307
|
+
for (let j = m - 1; j >= 1; j--) {
|
|
308
|
+
const prevDiff = sma25Series[j - 1] - sma75Series[j - 1];
|
|
309
|
+
const currDiff = sma25Series[j] - sma75Series[j];
|
|
310
|
+
if ((prevDiff <= 0 && currDiff > 0) || (prevDiff >= 0 && currDiff < 0)) {
|
|
311
|
+
const typeCross = prevDiff <= 0 && currDiff > 0 ? 'golden_cross' : 'death_cross';
|
|
312
|
+
const barsAgo = Math.max(0, closes.length - 1 - (off + j));
|
|
313
|
+
recentCross = { type: typeCross, pair: '25/75', barsAgo };
|
|
314
|
+
break;
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
} catch {
|
|
319
|
+
/* ignore cross calc errors */
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
// Composite score
|
|
323
|
+
// トレンド重視型(初心者向け): 中長期トレンドを重視し、瞬間的な板の変動を抑制
|
|
324
|
+
const weights = { smaTrend: 0.35, momentum: 0.3, cvdTrend: 0.2, volatility: 0.1, buyPressure: 0.05 } as const;
|
|
325
|
+
const contribution_buy = buyPressure * weights.buyPressure;
|
|
326
|
+
const contribution_cvd = cvdTrend * weights.cvdTrend;
|
|
327
|
+
const contribution_mom = momentumFactor * weights.momentum;
|
|
328
|
+
const contribution_vol = volatilityFactor * weights.volatility;
|
|
329
|
+
const contribution_sma = smaTrendFactor * weights.smaTrend;
|
|
330
|
+
const score = Number(
|
|
331
|
+
(contribution_buy + contribution_cvd + contribution_mom + contribution_vol + contribution_sma).toFixed(3),
|
|
332
|
+
);
|
|
333
|
+
|
|
334
|
+
const recommendation = score >= 0.25 ? 'bullish' : score <= -0.25 ? 'bearish' : 'neutral';
|
|
335
|
+
const tags: string[] = [];
|
|
336
|
+
if (buyPressure > 0.2) tags.push('buy_pressure');
|
|
337
|
+
if (cvdTrend > 0.2) tags.push('positive_cvd');
|
|
338
|
+
if (volatilityFactor > 0.2) tags.push('low_vol');
|
|
339
|
+
if (rsi != null && rsi < 35) tags.push('oversold_bias');
|
|
340
|
+
if (rsi != null && rsi > 65) tags.push('overbought_risk');
|
|
341
|
+
|
|
342
|
+
// compact contributions summary (top 2 by absolute value)
|
|
343
|
+
const contribPairs: Array<[string, number]> = [
|
|
344
|
+
['buy', contribution_buy],
|
|
345
|
+
['cvd', contribution_cvd],
|
|
346
|
+
['sma', contribution_sma],
|
|
347
|
+
['mom', contribution_mom],
|
|
348
|
+
['vol', contribution_vol],
|
|
349
|
+
];
|
|
350
|
+
const top2 = contribPairs
|
|
351
|
+
.sort((a, b) => Math.abs(b[1]) - Math.abs(a[1]))
|
|
352
|
+
.slice(0, 2)
|
|
353
|
+
.map(([k, v]) => `${k}${v >= 0 ? '+' : ''}${Number(v.toFixed(2))}`)
|
|
354
|
+
.join(', ');
|
|
355
|
+
|
|
356
|
+
// summary will be finalized after confidence/nextActions are computed
|
|
357
|
+
let _summary = '';
|
|
358
|
+
|
|
359
|
+
function calculateConfidence(
|
|
360
|
+
contributions: { buyPressure: number; cvdTrend: number; momentum: number; volatility: number; smaTrend: number },
|
|
361
|
+
score: number,
|
|
362
|
+
) {
|
|
363
|
+
const contribValues = Object.values(contributions);
|
|
364
|
+
const sorted = contribValues
|
|
365
|
+
.map((val, idx) => ({ value: val, index: idx }))
|
|
366
|
+
.sort((a, b) => Math.abs(b.value) - Math.abs(a.value));
|
|
367
|
+
const top3Signs = sorted.slice(0, 3).map((x) => Math.sign(x.value));
|
|
368
|
+
const allPositive = top3Signs.every((s) => s > 0);
|
|
369
|
+
const allNegative = top3Signs.every((s) => s < 0);
|
|
370
|
+
const top2Match = top3Signs[0] === top3Signs[1];
|
|
371
|
+
const maxContribution = Math.abs(sorted[0].value);
|
|
372
|
+
if ((allPositive || allNegative) && maxContribution >= 0.15) {
|
|
373
|
+
return { level: 'high' as const, reason: '主要3要素が同方向で一致。シグナルの信頼性高' };
|
|
374
|
+
} else if (top2Match || Math.abs(score) < 0.3) {
|
|
375
|
+
const reasons: string[] = [];
|
|
376
|
+
if (top2Match) reasons.push('上位2要素が一致');
|
|
377
|
+
if (Math.abs(score) < 0.3) reasons.push('スコアが中立圏');
|
|
378
|
+
return { level: 'medium' as const, reason: `${reasons.join('、')}。追加確認推奨` };
|
|
379
|
+
} else {
|
|
380
|
+
return { level: 'low' as const, reason: '主要要素間で矛盾あり。詳細分析必須' };
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
// Precompute contributions/breakdown, confidence and next actions
|
|
385
|
+
type BreakdownEntry = { rawValue: number; weight: number; contribution: number; interpretation: string };
|
|
386
|
+
type Breakdown = {
|
|
387
|
+
buyPressure: BreakdownEntry;
|
|
388
|
+
cvdTrend: BreakdownEntry;
|
|
389
|
+
momentum: BreakdownEntry;
|
|
390
|
+
volatility: BreakdownEntry;
|
|
391
|
+
smaTrend: BreakdownEntry;
|
|
392
|
+
};
|
|
393
|
+
|
|
394
|
+
const contributionsData = {
|
|
395
|
+
buyPressure: Number(contribution_buy.toFixed(3)),
|
|
396
|
+
cvdTrend: Number(contribution_cvd.toFixed(3)),
|
|
397
|
+
momentum: Number(contribution_mom.toFixed(3)),
|
|
398
|
+
volatility: Number(contribution_vol.toFixed(3)),
|
|
399
|
+
smaTrend: Number(contribution_sma.toFixed(3)),
|
|
400
|
+
};
|
|
401
|
+
|
|
402
|
+
const breakdownData: Breakdown = {
|
|
403
|
+
buyPressure: {
|
|
404
|
+
rawValue: Number(buyPressure.toFixed(3)),
|
|
405
|
+
weight: 0.05,
|
|
406
|
+
contribution: Number(contribution_buy.toFixed(3)),
|
|
407
|
+
interpretation:
|
|
408
|
+
buyPressure >= 0.4 ? 'strong' : buyPressure >= 0.15 ? 'moderate' : buyPressure <= -0.15 ? 'weak' : 'neutral',
|
|
409
|
+
},
|
|
410
|
+
cvdTrend: {
|
|
411
|
+
rawValue: Number(cvdTrend.toFixed(3)),
|
|
412
|
+
weight: 0.2,
|
|
413
|
+
contribution: Number(contribution_cvd.toFixed(3)),
|
|
414
|
+
interpretation:
|
|
415
|
+
cvdTrend >= 0.4 ? 'strong' : cvdTrend >= 0.15 ? 'moderate' : cvdTrend <= -0.15 ? 'weak' : 'neutral',
|
|
416
|
+
},
|
|
417
|
+
momentum: {
|
|
418
|
+
rawValue: Number(momentumFactor.toFixed(3)),
|
|
419
|
+
weight: 0.3,
|
|
420
|
+
contribution: Number(contribution_mom.toFixed(3)),
|
|
421
|
+
interpretation:
|
|
422
|
+
momentumFactor >= 0.35
|
|
423
|
+
? 'strong'
|
|
424
|
+
: momentumFactor >= 0.1
|
|
425
|
+
? 'moderate'
|
|
426
|
+
: momentumFactor <= -0.1
|
|
427
|
+
? 'weak'
|
|
428
|
+
: 'neutral',
|
|
429
|
+
},
|
|
430
|
+
volatility: {
|
|
431
|
+
rawValue: Number(volatilityFactor.toFixed(3)),
|
|
432
|
+
weight: 0.1,
|
|
433
|
+
contribution: Number(contribution_vol.toFixed(3)),
|
|
434
|
+
interpretation:
|
|
435
|
+
volatilityFactor >= 0.35
|
|
436
|
+
? 'strong'
|
|
437
|
+
: volatilityFactor >= 0.1
|
|
438
|
+
? 'moderate'
|
|
439
|
+
: volatilityFactor <= -0.1
|
|
440
|
+
? 'weak'
|
|
441
|
+
: 'neutral',
|
|
442
|
+
},
|
|
443
|
+
smaTrend: {
|
|
444
|
+
rawValue: Number(smaTrendFactor.toFixed(3)),
|
|
445
|
+
weight: 0.35,
|
|
446
|
+
contribution: Number(contribution_sma.toFixed(3)),
|
|
447
|
+
interpretation:
|
|
448
|
+
smaTrendFactor >= 0.35
|
|
449
|
+
? 'strong'
|
|
450
|
+
: smaTrendFactor >= 0.1
|
|
451
|
+
? 'moderate'
|
|
452
|
+
: smaTrendFactor <= -0.1
|
|
453
|
+
? 'weak'
|
|
454
|
+
: 'neutral',
|
|
455
|
+
},
|
|
456
|
+
};
|
|
457
|
+
|
|
458
|
+
const confidence = calculateConfidence(contributionsData, score);
|
|
459
|
+
|
|
460
|
+
function generateNextActions(
|
|
461
|
+
breakdown: Breakdown,
|
|
462
|
+
scoreVal: number,
|
|
463
|
+
conf: { level: 'high' | 'medium' | 'low'; reason: string },
|
|
464
|
+
) {
|
|
465
|
+
const actions: Array<{
|
|
466
|
+
priority: 'high' | 'medium' | 'low';
|
|
467
|
+
tool: string;
|
|
468
|
+
reason: string;
|
|
469
|
+
suggestedParams?: Record<string, unknown>;
|
|
470
|
+
}> = [];
|
|
471
|
+
const cvdContribAbs = Math.abs(breakdown.cvdTrend.contribution);
|
|
472
|
+
if (cvdContribAbs < 0.1) {
|
|
473
|
+
actions.push({
|
|
474
|
+
priority: 'high',
|
|
475
|
+
tool: 'get_flow_metrics',
|
|
476
|
+
reason: `CVD寄与が弱い(${breakdown.cvdTrend.contribution.toFixed(2)})。実際のフロー・スパイク確認推奨`,
|
|
477
|
+
suggestedParams: { bucketMs: 60000, limit: 300 },
|
|
478
|
+
});
|
|
479
|
+
}
|
|
480
|
+
const volContribAbs = Math.abs(breakdown.volatility.contribution);
|
|
481
|
+
if (volContribAbs > 0.08 || breakdown.volatility.interpretation === 'strong') {
|
|
482
|
+
actions.push({
|
|
483
|
+
priority: volContribAbs > 0.12 ? 'high' : 'medium',
|
|
484
|
+
tool: 'get_volatility_metrics',
|
|
485
|
+
reason: `ボラティリティ寄与が${volContribAbs > 0.12 ? '大' : '中程度'}(${breakdown.volatility.contribution.toFixed(2)})。詳細確認推奨`,
|
|
486
|
+
suggestedParams: { windows: [14, 20, 30], type: '1day' },
|
|
487
|
+
});
|
|
488
|
+
}
|
|
489
|
+
const momContribAbs = Math.abs(breakdown.momentum.contribution);
|
|
490
|
+
if (momContribAbs > 0.1) {
|
|
491
|
+
actions.push({
|
|
492
|
+
priority: momContribAbs > 0.15 ? 'high' : 'medium',
|
|
493
|
+
tool: 'get_indicators',
|
|
494
|
+
reason: `モメンタム寄与が${momContribAbs > 0.15 ? '大' : '中程度'}(${breakdown.momentum.contribution.toFixed(2)})。指標詳細確認推奨`,
|
|
495
|
+
suggestedParams: { limit: 200 },
|
|
496
|
+
});
|
|
497
|
+
}
|
|
498
|
+
const buyPressureAbs = Math.abs(breakdown.buyPressure.rawValue);
|
|
499
|
+
if (buyPressureAbs > 0.5) {
|
|
500
|
+
actions.push({
|
|
501
|
+
priority: 'medium',
|
|
502
|
+
tool: 'get_orderbook',
|
|
503
|
+
reason: `板圧力が極端(${breakdown.buyPressure.rawValue.toFixed(2)})。帯域別分析推奨`,
|
|
504
|
+
suggestedParams: { mode: 'pressure', bandsPct: [0.001, 0.005, 0.01] },
|
|
505
|
+
});
|
|
506
|
+
}
|
|
507
|
+
if (Math.abs(scoreVal) < 0.3) {
|
|
508
|
+
actions.push({
|
|
509
|
+
priority: 'medium',
|
|
510
|
+
tool: 'detect_patterns',
|
|
511
|
+
reason: `スコア中立圏(${scoreVal.toFixed(3)})。レンジ・パターン形成可能性`,
|
|
512
|
+
suggestedParams: { view: 'detailed' },
|
|
513
|
+
});
|
|
514
|
+
}
|
|
515
|
+
if (conf.level === 'low') {
|
|
516
|
+
actions.push({
|
|
517
|
+
priority: 'high',
|
|
518
|
+
tool: 'analyze_indicators',
|
|
519
|
+
reason: '要素間で矛盾。複数角度からの検証必須',
|
|
520
|
+
suggestedParams: { limit: 200 },
|
|
521
|
+
});
|
|
522
|
+
}
|
|
523
|
+
const priorityOrder: Record<'high' | 'medium' | 'low', number> = { high: 0, medium: 1, low: 2 };
|
|
524
|
+
return actions.sort((a, b) => priorityOrder[a.priority] - priorityOrder[b.priority]);
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
const nextActions = generateNextActions(breakdownData, score, confidence);
|
|
528
|
+
|
|
529
|
+
const _confidenceEmoji = confidence.level === 'high' ? '✅' : confidence.level === 'medium' ? '⚠️' : '🔴';
|
|
530
|
+
const nextActionsText = nextActions
|
|
531
|
+
.slice(0, 2)
|
|
532
|
+
.map((action) => {
|
|
533
|
+
const priorityEmoji = action.priority === 'high' ? '🔴' : action.priority === 'medium' ? '🟡' : '🟢';
|
|
534
|
+
const params = action.suggestedParams ? ` ${JSON.stringify(action.suggestedParams)}` : '';
|
|
535
|
+
return `${priorityEmoji} ${action.tool}${params}`;
|
|
536
|
+
})
|
|
537
|
+
.join(', ');
|
|
538
|
+
const summaryText = formatSummary({
|
|
539
|
+
pair: chk.pair,
|
|
540
|
+
latest: indRes?.data?.normalized?.at(-1)?.close,
|
|
541
|
+
extra: `score=${score} rec=${recommendation} confidence=${confidence.level} (top: ${top2})${nextActions.length > 0 ? ` next=[${nextActionsText}]` : ''}`,
|
|
542
|
+
});
|
|
543
|
+
_summary = summaryText;
|
|
544
|
+
|
|
545
|
+
const alerts = (() => {
|
|
546
|
+
const a: Array<{ level: 'info' | 'warning' | 'critical'; message: string }> = [];
|
|
547
|
+
if (Math.abs(breakdownData.volatility.contribution) < 0.03) {
|
|
548
|
+
a.push({ level: 'info', message: 'ボラティリティ寄与が低い。急変時に注意' });
|
|
549
|
+
}
|
|
550
|
+
if (confidence.level === 'low') {
|
|
551
|
+
a.push({ level: 'warning', message: '要素間の矛盾あり。詳細分析を強く推奨' });
|
|
552
|
+
}
|
|
553
|
+
return a;
|
|
554
|
+
})();
|
|
555
|
+
|
|
556
|
+
// Direction states helper
|
|
557
|
+
const toState = (v: number) => (v > 0.1 ? 'up' : v < -0.1 ? 'down' : 'flat');
|
|
558
|
+
const momentumState = toState(momentumFactor);
|
|
559
|
+
const cvdState = toState(cvdTrend);
|
|
560
|
+
// Timeframe recommendation (simple): suggest 4hour when annualized RV is high
|
|
561
|
+
const recommendedTimeframes: string[] = ['1day', ...(rvNum > 0.6 ? ['4hour'] : [])];
|
|
562
|
+
|
|
563
|
+
const data = {
|
|
564
|
+
score,
|
|
565
|
+
recommendation,
|
|
566
|
+
tags,
|
|
567
|
+
formula: 'score = 0.35*smaTrend + 0.30*momentum + 0.20*cvdTrend + 0.10*volatility + 0.05*buyPressure',
|
|
568
|
+
weights: { smaTrend: 0.35, momentum: 0.3, cvdTrend: 0.2, volatility: 0.1, buyPressure: 0.05 },
|
|
569
|
+
contributions: contributionsData,
|
|
570
|
+
breakdown: breakdownData,
|
|
571
|
+
topContributors: ['smaTrend', 'momentum', 'cvdTrend', 'volatility', 'buyPressure']
|
|
572
|
+
.map((k) => [
|
|
573
|
+
k,
|
|
574
|
+
{
|
|
575
|
+
buyPressure: contribution_buy,
|
|
576
|
+
cvdTrend: contribution_cvd,
|
|
577
|
+
smaTrend: contribution_sma,
|
|
578
|
+
momentum: contribution_mom,
|
|
579
|
+
volatility: contribution_vol,
|
|
580
|
+
}[k as 'buyPressure'] as number,
|
|
581
|
+
])
|
|
582
|
+
.sort((a, b) => Math.abs(b[1] as number) - Math.abs(a[1] as number))
|
|
583
|
+
.slice(0, 2)
|
|
584
|
+
.map((x) => x[0]) as Array<'buyPressure' | 'cvdTrend' | 'momentum' | 'volatility' | 'smaTrend'>,
|
|
585
|
+
confidence: confidence.level,
|
|
586
|
+
confidenceReason: confidence.reason,
|
|
587
|
+
nextActions,
|
|
588
|
+
alerts,
|
|
589
|
+
thresholds: { bullish: 0.25, bearish: -0.25 },
|
|
590
|
+
metrics: {
|
|
591
|
+
buyPressure,
|
|
592
|
+
cvdTrend,
|
|
593
|
+
momentumFactor,
|
|
594
|
+
volatilityFactor,
|
|
595
|
+
smaTrendFactor,
|
|
596
|
+
rsi: rsi ?? null,
|
|
597
|
+
rv_std_ann: rvNum,
|
|
598
|
+
aggressorRatio: buyRatio,
|
|
599
|
+
cvdSlope,
|
|
600
|
+
horizon,
|
|
601
|
+
},
|
|
602
|
+
states: {
|
|
603
|
+
momentum: momentumState,
|
|
604
|
+
cvdTrend: cvdState,
|
|
605
|
+
},
|
|
606
|
+
sma: {
|
|
607
|
+
current: latestClose ?? null,
|
|
608
|
+
values: { sma25: sma25 ?? null, sma75: sma75 ?? null, sma200: sma200 ?? null },
|
|
609
|
+
deviations: {
|
|
610
|
+
vs25: smaDeviations.vs25 != null ? Number((smaDeviations.vs25 * 100).toFixed(2)) : null,
|
|
611
|
+
vs75: smaDeviations.vs75 != null ? Number((smaDeviations.vs75 * 100).toFixed(2)) : null,
|
|
612
|
+
vs200: smaDeviations.vs200 != null ? Number((smaDeviations.vs200 * 100).toFixed(2)) : null,
|
|
613
|
+
},
|
|
614
|
+
arrangement: smaArrangement,
|
|
615
|
+
position: smaPosition,
|
|
616
|
+
distanceFromSma25Pct: smaDeviations.vs25 != null ? Number((smaDeviations.vs25 * 100).toFixed(2)) : null,
|
|
617
|
+
recentCross,
|
|
618
|
+
},
|
|
619
|
+
recommendedTimeframes,
|
|
620
|
+
refs: {
|
|
621
|
+
flow: { aggregates: flowRes.data.aggregates, lastBuckets: buckets.slice(-Math.min(5, buckets.length)) },
|
|
622
|
+
volatility: { aggregates: volRes.data.aggregates },
|
|
623
|
+
indicators: { latest: indRes.data.indicators, trend: indRes.data.trend },
|
|
624
|
+
},
|
|
625
|
+
};
|
|
626
|
+
|
|
627
|
+
const fullText = buildMarketSignalText({
|
|
628
|
+
pair: chk.pair,
|
|
629
|
+
type,
|
|
630
|
+
score,
|
|
631
|
+
recommendation,
|
|
632
|
+
confidence,
|
|
633
|
+
latestClose,
|
|
634
|
+
sma: { sma25, sma75, sma200 },
|
|
635
|
+
smaArrangement,
|
|
636
|
+
smaPosition,
|
|
637
|
+
smaDeviations,
|
|
638
|
+
recentCross,
|
|
639
|
+
factors: { smaTrendFactor, momentumFactor, cvdTrend, volatilityFactor, buyPressure },
|
|
640
|
+
contributions: {
|
|
641
|
+
sma: contribution_sma,
|
|
642
|
+
mom: contribution_mom,
|
|
643
|
+
cvd: contribution_cvd,
|
|
644
|
+
vol: contribution_vol,
|
|
645
|
+
buy: contribution_buy,
|
|
646
|
+
},
|
|
647
|
+
rsi: rsi ?? null,
|
|
648
|
+
rvNum,
|
|
649
|
+
buyRatio,
|
|
650
|
+
nextActions,
|
|
651
|
+
alerts,
|
|
652
|
+
});
|
|
653
|
+
|
|
654
|
+
const meta = createMeta(chk.pair, { type, windows, bucketMs, flowLimit });
|
|
655
|
+
return AnalyzeMarketSignalOutputSchema.parse(
|
|
656
|
+
ok(
|
|
657
|
+
fullText,
|
|
658
|
+
data as z.infer<typeof AnalyzeMarketSignalDataSchemaOut>,
|
|
659
|
+
meta as z.infer<typeof AnalyzeMarketSignalMetaSchemaOut>,
|
|
660
|
+
),
|
|
661
|
+
);
|
|
662
|
+
} catch (e: unknown) {
|
|
663
|
+
return failFromError(e, { schema: AnalyzeMarketSignalOutputSchema });
|
|
664
|
+
}
|
|
665
|
+
}
|