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,302 @@
|
|
|
1
|
+
import type { z } from 'zod';
|
|
2
|
+
import { toNum } from '../lib/conversions.js';
|
|
3
|
+
import { nowIso } from '../lib/datetime.js';
|
|
4
|
+
import { formatSummary } from '../lib/formatter.js';
|
|
5
|
+
import { trueRange } from '../lib/indicators.js';
|
|
6
|
+
import { slidingMean, slidingStddev, stddev } from '../lib/math.js';
|
|
7
|
+
import { fail, failFromError, failFromValidation, ok } from '../lib/result.js';
|
|
8
|
+
import { createMeta, ensurePair, validateLimit } from '../lib/validate.js';
|
|
9
|
+
import {
|
|
10
|
+
componentMeanToVol,
|
|
11
|
+
garmanKlassComponents,
|
|
12
|
+
logReturns,
|
|
13
|
+
parkinsonComponents,
|
|
14
|
+
rogersSatchellComponents,
|
|
15
|
+
} from '../lib/volatility.js';
|
|
16
|
+
import {
|
|
17
|
+
type GetVolMetricsDataSchemaOut,
|
|
18
|
+
type GetVolMetricsMetaSchemaOut,
|
|
19
|
+
GetVolMetricsOutputSchema,
|
|
20
|
+
} from '../src/schemas.js';
|
|
21
|
+
import getCandles from './get_candles.js';
|
|
22
|
+
|
|
23
|
+
type Candle = { open: number; high: number; low: number; close: number; isoTime?: string | null };
|
|
24
|
+
|
|
25
|
+
export interface RollingEntry {
|
|
26
|
+
window: number;
|
|
27
|
+
rv_std: number;
|
|
28
|
+
rv_std_ann?: number;
|
|
29
|
+
atr?: number;
|
|
30
|
+
parkinson?: number;
|
|
31
|
+
garmanKlass?: number;
|
|
32
|
+
rogersSatchell?: number;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export interface BuildVolatilityMetricsTextInput {
|
|
36
|
+
baseSummary: string;
|
|
37
|
+
aggregates: {
|
|
38
|
+
rv_std: number;
|
|
39
|
+
rv_std_ann?: number;
|
|
40
|
+
parkinson: number;
|
|
41
|
+
garmanKlass: number;
|
|
42
|
+
rogersSatchell: number;
|
|
43
|
+
atr: number;
|
|
44
|
+
};
|
|
45
|
+
rolling: RollingEntry[];
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/** テキスト組み立て(ボラティリティ詳細)— テスト可能な純粋関数 */
|
|
49
|
+
export function buildVolatilityMetricsText(input: BuildVolatilityMetricsTextInput): string {
|
|
50
|
+
const { baseSummary, aggregates: a, rolling } = input;
|
|
51
|
+
const aggLines = [
|
|
52
|
+
`rv_std:${a.rv_std}`,
|
|
53
|
+
a.rv_std_ann != null ? `rv_std_ann:${a.rv_std_ann}` : '',
|
|
54
|
+
`parkinson:${a.parkinson}`,
|
|
55
|
+
`garmanKlass:${a.garmanKlass}`,
|
|
56
|
+
`rogersSatchell:${a.rogersSatchell}`,
|
|
57
|
+
`atr:${a.atr}`,
|
|
58
|
+
]
|
|
59
|
+
.filter(Boolean)
|
|
60
|
+
.join(' ');
|
|
61
|
+
const rollLines = rolling.map((r) => {
|
|
62
|
+
const parts = [`w=${r.window} rv:${r.rv_std.toFixed(6)}`];
|
|
63
|
+
if (r.rv_std_ann != null) parts.push(`ann:${r.rv_std_ann.toFixed(6)}`);
|
|
64
|
+
if (r.atr != null) parts.push(`atr:${r.atr.toFixed(2)}`);
|
|
65
|
+
if (r.parkinson != null) parts.push(`pk:${r.parkinson.toFixed(6)}`);
|
|
66
|
+
return parts.join(' ');
|
|
67
|
+
});
|
|
68
|
+
return (
|
|
69
|
+
baseSummary +
|
|
70
|
+
`\n\naggregates: ${aggLines}` +
|
|
71
|
+
`\n\n📊 ローリング分析:\n` +
|
|
72
|
+
rollLines.join('\n') +
|
|
73
|
+
`\n\n---\n📌 含まれるもの: ボラティリティ指標(RV・Parkinson・GK・RS・ATR)、ローリング分析` +
|
|
74
|
+
`\n📌 含まれないもの: 価格の方向性・トレンド、出来高フロー、板情報、テクニカル指標` +
|
|
75
|
+
`\n📌 補完ツール: get_candles(価格OHLCV), analyze_indicators(方向性指標), get_flow_metrics(出来高フロー)`
|
|
76
|
+
);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function baseIntervalMsOf(type: string): number {
|
|
80
|
+
switch (type) {
|
|
81
|
+
case '1min':
|
|
82
|
+
return 60_000;
|
|
83
|
+
case '5min':
|
|
84
|
+
return 5 * 60_000;
|
|
85
|
+
case '15min':
|
|
86
|
+
return 15 * 60_000;
|
|
87
|
+
case '30min':
|
|
88
|
+
return 30 * 60_000;
|
|
89
|
+
case '1hour':
|
|
90
|
+
return 60 * 60_000;
|
|
91
|
+
case '4hour':
|
|
92
|
+
return 4 * 60 * 60_000;
|
|
93
|
+
case '8hour':
|
|
94
|
+
return 8 * 60 * 60_000;
|
|
95
|
+
case '12hour':
|
|
96
|
+
return 12 * 60 * 60_000;
|
|
97
|
+
case '1day':
|
|
98
|
+
return 24 * 60 * 60_000;
|
|
99
|
+
case '1week':
|
|
100
|
+
return 7 * 24 * 60 * 60_000;
|
|
101
|
+
case '1month':
|
|
102
|
+
return 30 * 24 * 60 * 60_000; // approx
|
|
103
|
+
default:
|
|
104
|
+
return 24 * 60 * 60_000;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function periodsPerYear(type: string): number {
|
|
109
|
+
const secondsPerYear = 365 * 24 * 60 * 60;
|
|
110
|
+
const intervalSec = baseIntervalMsOf(type) / 1000;
|
|
111
|
+
return Math.max(1, Math.floor(secondsPerYear / intervalSec));
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function toMs(iso: string | null | undefined): number | null {
|
|
115
|
+
if (!iso) return null;
|
|
116
|
+
return toNum(Date.parse(iso));
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
export default async function getVolatilityMetrics(
|
|
120
|
+
pair: string,
|
|
121
|
+
type: string = '1day',
|
|
122
|
+
limit: number = 200,
|
|
123
|
+
windows: number[] = [14, 20, 30],
|
|
124
|
+
opts?: { useLogReturns?: boolean; annualize?: boolean; tz?: string; cacheTtlMs?: number },
|
|
125
|
+
) {
|
|
126
|
+
const chk = ensurePair(pair);
|
|
127
|
+
if (!chk.ok) return failFromValidation(chk, GetVolMetricsOutputSchema);
|
|
128
|
+
const lim = validateLimit(limit, 20, 500);
|
|
129
|
+
if (!lim.ok) return failFromValidation(lim, GetVolMetricsOutputSchema);
|
|
130
|
+
|
|
131
|
+
try {
|
|
132
|
+
const cRes = await getCandles(chk.pair, type, undefined, lim.value);
|
|
133
|
+
if (!cRes.ok)
|
|
134
|
+
return GetVolMetricsOutputSchema.parse(fail(cRes.summary || 'failed', cRes.meta.errorType || 'internal'));
|
|
135
|
+
const candles: Candle[] = cRes.data.normalized;
|
|
136
|
+
if (!Array.isArray(candles) || candles.length < 20) {
|
|
137
|
+
return GetVolMetricsOutputSchema.parse(fail('データ不足(最低20本必要)', 'user'));
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
const useLog = opts?.useLogReturns ?? true;
|
|
141
|
+
const withAnn = opts?.annualize ?? true;
|
|
142
|
+
|
|
143
|
+
const ts: number[] = [];
|
|
144
|
+
const close: number[] = [];
|
|
145
|
+
const open: number[] = [];
|
|
146
|
+
const high: number[] = [];
|
|
147
|
+
const low: number[] = [];
|
|
148
|
+
for (const c of candles) {
|
|
149
|
+
const o = toNum(c.open);
|
|
150
|
+
const h = toNum(c.high);
|
|
151
|
+
const l = toNum(c.low);
|
|
152
|
+
const cl = toNum(c.close);
|
|
153
|
+
if (o == null || h == null || l == null || cl == null) continue;
|
|
154
|
+
|
|
155
|
+
const t = toMs(c.isoTime ?? null);
|
|
156
|
+
if (t != null) ts.push(t);
|
|
157
|
+
else ts.push(ts.length > 0 ? ts[ts.length - 1] + baseIntervalMsOf(type) : Date.now());
|
|
158
|
+
open.push(o);
|
|
159
|
+
high.push(h);
|
|
160
|
+
low.push(l);
|
|
161
|
+
close.push(cl);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
if (close.length < 20) {
|
|
165
|
+
return GetVolMetricsOutputSchema.parse(fail('有効なOHLCデータ不足(最低20本必要)', 'user'));
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
const ret = logReturns(close, useLog);
|
|
169
|
+
const rvInst = ret.map((r) => Math.abs(r));
|
|
170
|
+
|
|
171
|
+
// Per-candle components for OHLC-based estimators(lib/volatility.ts に委譲)
|
|
172
|
+
const pkSeries = parkinsonComponents(high, low);
|
|
173
|
+
const gkSeries = garmanKlassComponents(open, high, low, close);
|
|
174
|
+
const rsSeries = rogersSatchellComponents(open, high, low, close);
|
|
175
|
+
|
|
176
|
+
// True Range(lib/indicators.ts に委譲)
|
|
177
|
+
const trRaw = trueRange(high, low, close);
|
|
178
|
+
// trRaw[0] は NaN なので、互換性のため先頭を h-l で埋める
|
|
179
|
+
const trSeries = trRaw.map((v, i) => (Number.isFinite(v) ? v : Math.max(0, high[i] - low[i])));
|
|
180
|
+
|
|
181
|
+
// Aggregates over whole sample (use returns length for rv)
|
|
182
|
+
const rvStd = stddev(ret);
|
|
183
|
+
// Parkinson/GK/RS are per-candle estimators (not return-based), so use full series
|
|
184
|
+
const pkMean = pkSeries.reduce((s, v) => s + v, 0) / Math.max(1, pkSeries.length);
|
|
185
|
+
const gkMean = gkSeries.reduce((s, v) => s + v, 0) / Math.max(1, gkSeries.length);
|
|
186
|
+
const rsMean = rsSeries.reduce((s, v) => s + v, 0) / Math.max(1, rsSeries.length);
|
|
187
|
+
const parkinson = componentMeanToVol(pkMean, 'parkinson');
|
|
188
|
+
const garmanKlass = componentMeanToVol(gkMean, 'garmanKlass');
|
|
189
|
+
const rogersSatchell = componentMeanToVol(rsMean, 'rogersSatchell');
|
|
190
|
+
|
|
191
|
+
// ATR aggregate: use first window (default 14) SMA on TR, take last
|
|
192
|
+
const primaryWindow = Math.max(2, windows?.[0] || 14);
|
|
193
|
+
const atrSeries = slidingMean(trSeries, primaryWindow);
|
|
194
|
+
const atrAgg = atrSeries.length > 0 ? atrSeries[atrSeries.length - 1] : 0;
|
|
195
|
+
|
|
196
|
+
const annFactor = withAnn ? Math.sqrt(periodsPerYear(type)) : 1;
|
|
197
|
+
const rvStdAnn = withAnn ? rvStd * annFactor : undefined;
|
|
198
|
+
|
|
199
|
+
// Rolling per requested windows
|
|
200
|
+
const rollingOut: Array<{
|
|
201
|
+
window: number;
|
|
202
|
+
rv_std: number;
|
|
203
|
+
rv_std_ann?: number;
|
|
204
|
+
atr?: number;
|
|
205
|
+
parkinson?: number;
|
|
206
|
+
garmanKlass?: number;
|
|
207
|
+
rogersSatchell?: number;
|
|
208
|
+
}> = [];
|
|
209
|
+
for (const wRaw of windows) {
|
|
210
|
+
const w = Math.max(2, Math.min(wRaw | 0, ret.length));
|
|
211
|
+
if (w > ret.length) continue;
|
|
212
|
+
const rvStdRoll = slidingStddev(ret, w);
|
|
213
|
+
const rvStdLatest = rvStdRoll.at(-1) ?? 0;
|
|
214
|
+
const rvStdAnnLatest = withAnn ? rvStdLatest * annFactor : undefined;
|
|
215
|
+
const pkRoll = slidingMean(pkSeries, w);
|
|
216
|
+
const gkRoll = slidingMean(gkSeries, w);
|
|
217
|
+
const rsRoll = slidingMean(rsSeries, w);
|
|
218
|
+
const atrRoll = slidingMean(trSeries, w);
|
|
219
|
+
const p = pkRoll.length ? componentMeanToVol(pkRoll.at(-1) as number, 'parkinson') : undefined;
|
|
220
|
+
const gk = gkRoll.length ? componentMeanToVol(gkRoll.at(-1) as number, 'garmanKlass') : undefined;
|
|
221
|
+
const rs = rsRoll.length ? componentMeanToVol(rsRoll.at(-1) as number, 'rogersSatchell') : undefined;
|
|
222
|
+
const atr = atrRoll.length ? (atrRoll.at(-1) as number) : undefined;
|
|
223
|
+
rollingOut.push({
|
|
224
|
+
window: w,
|
|
225
|
+
rv_std: rvStdLatest,
|
|
226
|
+
rv_std_ann: rvStdAnnLatest,
|
|
227
|
+
atr,
|
|
228
|
+
parkinson: p,
|
|
229
|
+
garmanKlass: gk,
|
|
230
|
+
rogersSatchell: rs,
|
|
231
|
+
});
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
// Tags: always use annualized RV for consistent thresholds regardless of annualize flag
|
|
235
|
+
const tags: string[] = [];
|
|
236
|
+
const rvRefAnn = rvStdAnn ?? rvStd * Math.sqrt(periodsPerYear(type));
|
|
237
|
+
if (rvRefAnn >= 0.8) tags.push('volatile');
|
|
238
|
+
else if (rvRefAnn <= 0.3) tags.push('calm');
|
|
239
|
+
|
|
240
|
+
const data = {
|
|
241
|
+
meta: {
|
|
242
|
+
pair: chk.pair,
|
|
243
|
+
type: String(type),
|
|
244
|
+
fetchedAt: nowIso(),
|
|
245
|
+
baseIntervalMs: baseIntervalMsOf(type),
|
|
246
|
+
sampleSize: candles.length,
|
|
247
|
+
windows: [...windows],
|
|
248
|
+
annualize: withAnn,
|
|
249
|
+
useLogReturns: useLog,
|
|
250
|
+
source: 'bitbank:candlestick' as const,
|
|
251
|
+
},
|
|
252
|
+
aggregates: {
|
|
253
|
+
rv_std: Number(rvStd.toFixed(8)),
|
|
254
|
+
rv_std_ann: withAnn ? Number((rvStdAnn as number).toFixed(8)) : undefined,
|
|
255
|
+
parkinson: Number(parkinson.toFixed(8)),
|
|
256
|
+
garmanKlass: Number(garmanKlass.toFixed(8)),
|
|
257
|
+
rogersSatchell: Number(rogersSatchell.toFixed(8)),
|
|
258
|
+
atr: Number(atrAgg.toFixed(8)),
|
|
259
|
+
},
|
|
260
|
+
rolling: rollingOut.map((r) => ({
|
|
261
|
+
window: r.window,
|
|
262
|
+
rv_std: Number(r.rv_std.toFixed(8)),
|
|
263
|
+
rv_std_ann: r.rv_std_ann != null ? Number(r.rv_std_ann.toFixed(8)) : undefined,
|
|
264
|
+
atr: r.atr != null ? Number(r.atr.toFixed(8)) : undefined,
|
|
265
|
+
parkinson: r.parkinson != null ? Number(r.parkinson.toFixed(8)) : undefined,
|
|
266
|
+
garmanKlass: r.garmanKlass != null ? Number(r.garmanKlass.toFixed(8)) : undefined,
|
|
267
|
+
rogersSatchell: r.rogersSatchell != null ? Number(r.rogersSatchell.toFixed(8)) : undefined,
|
|
268
|
+
})),
|
|
269
|
+
series: {
|
|
270
|
+
ts,
|
|
271
|
+
close,
|
|
272
|
+
ret: ret.map((v) => Number(v.toFixed(8))),
|
|
273
|
+
rv_inst: rvInst.map((v) => Number(v.toFixed(8))),
|
|
274
|
+
},
|
|
275
|
+
tags,
|
|
276
|
+
};
|
|
277
|
+
|
|
278
|
+
const baseSummaryVol = formatSummary({
|
|
279
|
+
pair: chk.pair,
|
|
280
|
+
timeframe: String(type),
|
|
281
|
+
latest: close.at(-1),
|
|
282
|
+
extra: `rv=${(rvRefAnn).toFixed(3)}(ann)${tags.length ? ` ${tags.join(',')}` : ''}`,
|
|
283
|
+
});
|
|
284
|
+
// テキスト summary にボラティリティ詳細を含める(LLM が structuredContent.data を読めない対策)
|
|
285
|
+
const summary = buildVolatilityMetricsText({
|
|
286
|
+
baseSummary: baseSummaryVol,
|
|
287
|
+
aggregates: data.aggregates,
|
|
288
|
+
rolling: rollingOut,
|
|
289
|
+
});
|
|
290
|
+
|
|
291
|
+
const meta = createMeta(chk.pair, { type, count: candles.length });
|
|
292
|
+
return GetVolMetricsOutputSchema.parse(
|
|
293
|
+
ok<z.infer<typeof GetVolMetricsDataSchemaOut>, z.infer<typeof GetVolMetricsMetaSchemaOut>>(
|
|
294
|
+
summary,
|
|
295
|
+
data,
|
|
296
|
+
meta as z.infer<typeof GetVolMetricsMetaSchemaOut>,
|
|
297
|
+
),
|
|
298
|
+
);
|
|
299
|
+
} catch (e: unknown) {
|
|
300
|
+
return failFromError(e, { schema: GetVolMetricsOutputSchema });
|
|
301
|
+
}
|
|
302
|
+
}
|
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* パターン事後分析 (aftermath)
|
|
3
|
+
*
|
|
4
|
+
* パターン完成後のブレイクアウト確認・理論目標価格・リターン計算を行う。
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { avg as avgRaw, median as medianRaw } from '../../lib/math.js';
|
|
8
|
+
import type { AftermathResult, CandleData, PatternEntry } from './types.js';
|
|
9
|
+
|
|
10
|
+
// ---------------------------------------------------------------------------
|
|
11
|
+
// ネックライン補間
|
|
12
|
+
// ---------------------------------------------------------------------------
|
|
13
|
+
export function necklineValue(p: PatternEntry, idx: number): number | null {
|
|
14
|
+
const nl = Array.isArray(p?.neckline) && p.neckline.length === 2 ? p.neckline : null;
|
|
15
|
+
if (!nl) return null;
|
|
16
|
+
const [a, b] = nl;
|
|
17
|
+
if (Number.isFinite(a?.x) && Number.isFinite(b?.x) && Number.isFinite(a?.y) && Number.isFinite(b?.y)) {
|
|
18
|
+
const x1 = Number(a.x),
|
|
19
|
+
y1 = Number(a.y),
|
|
20
|
+
x2 = Number(b.x),
|
|
21
|
+
y2 = Number(b.y);
|
|
22
|
+
if (x2 !== x1) {
|
|
23
|
+
const t = (idx - x1) / (x2 - x1);
|
|
24
|
+
return y1 + (y2 - y1) * Math.max(0, Math.min(1, t));
|
|
25
|
+
}
|
|
26
|
+
return y1;
|
|
27
|
+
}
|
|
28
|
+
return Number.isFinite(a?.y) ? Number(a.y) : Number.isFinite(b?.y) ? Number(b.y) : null;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// ---------------------------------------------------------------------------
|
|
32
|
+
// 事後分析
|
|
33
|
+
// ---------------------------------------------------------------------------
|
|
34
|
+
const BULLISH_TYPES = [
|
|
35
|
+
'double_bottom',
|
|
36
|
+
'inverse_head_and_shoulders',
|
|
37
|
+
'triangle_ascending',
|
|
38
|
+
'triangle_symmetrical',
|
|
39
|
+
'flag',
|
|
40
|
+
];
|
|
41
|
+
const BEARISH_TYPES = ['double_top', 'head_and_shoulders', 'triangle_descending'];
|
|
42
|
+
// Note: 'pennant' is intentionally excluded from both lists — its direction is determined by poleDirection field.
|
|
43
|
+
|
|
44
|
+
export function analyzeAftermath(
|
|
45
|
+
p: PatternEntry,
|
|
46
|
+
candles: CandleData[],
|
|
47
|
+
isoToIndex: Map<string, number>,
|
|
48
|
+
): AftermathResult | null {
|
|
49
|
+
try {
|
|
50
|
+
const endIso = p?.range?.end;
|
|
51
|
+
const endIdx = isoToIndex.has(String(endIso)) ? (isoToIndex.get(String(endIso)) as number) : -1;
|
|
52
|
+
if (endIdx < 0) return null;
|
|
53
|
+
const baseClose = Number(candles[endIdx]?.close ?? NaN);
|
|
54
|
+
if (!Number.isFinite(baseClose)) return null;
|
|
55
|
+
const nlAtEnd = necklineValue(p, endIdx);
|
|
56
|
+
const pType = String(p?.type);
|
|
57
|
+
const isPennant = pType === 'pennant';
|
|
58
|
+
const bullish = isPennant ? p?.poleDirection === 'up' : BULLISH_TYPES.includes(pType);
|
|
59
|
+
const bearish = isPennant ? p?.poleDirection === 'down' : BEARISH_TYPES.includes(pType);
|
|
60
|
+
if (!Number.isFinite(nlAtEnd as number)) return null;
|
|
61
|
+
let breakoutConfirmed = false;
|
|
62
|
+
let breakoutDate: string | undefined;
|
|
63
|
+
let daysToTarget: number | null = null;
|
|
64
|
+
const breakoutBuffer = 0.015;
|
|
65
|
+
for (let i = endIdx + 1; i < Math.min(candles.length, endIdx + 30); i++) {
|
|
66
|
+
const nl = necklineValue(p, i) ?? (nlAtEnd as number);
|
|
67
|
+
const c = Number(candles[i]?.close ?? NaN);
|
|
68
|
+
if (!Number.isFinite(c) || !Number.isFinite(nl)) continue;
|
|
69
|
+
if ((bullish && c > nl * (1 + breakoutBuffer)) || (bearish && c < nl * (1 - breakoutBuffer))) {
|
|
70
|
+
breakoutConfirmed = true;
|
|
71
|
+
breakoutDate = candles[i]?.isoTime;
|
|
72
|
+
break;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
const horizon = [3, 7, 14];
|
|
76
|
+
const priceMove: Record<string, { return: number; high: number; low: number }> = {};
|
|
77
|
+
for (const h of horizon) {
|
|
78
|
+
const to = Math.min(candles.length - 1, endIdx + h);
|
|
79
|
+
if (to <= endIdx) continue;
|
|
80
|
+
let hi = -Infinity,
|
|
81
|
+
lo = Infinity;
|
|
82
|
+
for (let i = endIdx + 1; i <= to; i++) {
|
|
83
|
+
hi = Math.max(hi, Number(candles[i]?.high ?? -Infinity));
|
|
84
|
+
lo = Math.min(lo, Number(candles[i]?.low ?? Infinity));
|
|
85
|
+
}
|
|
86
|
+
const closeTo = Number(candles[to]?.close ?? NaN);
|
|
87
|
+
if (!Number.isFinite(closeTo)) continue;
|
|
88
|
+
const ret = ((closeTo - baseClose) / baseClose) * 100;
|
|
89
|
+
priceMove[`days${h}`] = {
|
|
90
|
+
return: Number(ret.toFixed(2)),
|
|
91
|
+
high: Number(hi.toFixed(0)),
|
|
92
|
+
low: Number(lo.toFixed(0)),
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
// theoretical target
|
|
96
|
+
let theoreticalTarget = NaN;
|
|
97
|
+
const nl = nlAtEnd as number;
|
|
98
|
+
const pivotPrices = Array.isArray(p?.pivots)
|
|
99
|
+
? p.pivots.map((x: { price?: number }) => Number(x?.price)).filter((x: number) => Number.isFinite(x))
|
|
100
|
+
: [];
|
|
101
|
+
if (bullish && pivotPrices.length) {
|
|
102
|
+
const patternLow = Math.min(...pivotPrices);
|
|
103
|
+
theoreticalTarget = nl + (nl - patternLow);
|
|
104
|
+
} else if (bearish && pivotPrices.length) {
|
|
105
|
+
const patternHigh = Math.max(...pivotPrices);
|
|
106
|
+
theoreticalTarget = nl - (patternHigh - nl);
|
|
107
|
+
}
|
|
108
|
+
let targetReached = false;
|
|
109
|
+
if (Number.isFinite(theoreticalTarget)) {
|
|
110
|
+
for (let i = endIdx + 1; i <= Math.min(candles.length - 1, endIdx + 14); i++) {
|
|
111
|
+
const hiVal = Number(candles[i]?.high ?? NaN);
|
|
112
|
+
const loVal = Number(candles[i]?.low ?? NaN);
|
|
113
|
+
if (bullish && Number.isFinite(hiVal) && hiVal >= theoreticalTarget) {
|
|
114
|
+
targetReached = true;
|
|
115
|
+
daysToTarget = i - endIdx;
|
|
116
|
+
break;
|
|
117
|
+
}
|
|
118
|
+
if (bearish && Number.isFinite(loVal) && loVal <= theoreticalTarget) {
|
|
119
|
+
targetReached = true;
|
|
120
|
+
daysToTarget = i - endIdx;
|
|
121
|
+
break;
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
// outcome message
|
|
126
|
+
function outcomeMessage(): string {
|
|
127
|
+
if (!breakoutConfirmed) return 'ネックライン未突破(パターン不発)';
|
|
128
|
+
if (targetReached) return '成功(理論目標価格到達)';
|
|
129
|
+
const r3 = priceMove?.days3?.return;
|
|
130
|
+
const r7 = priceMove?.days7?.return;
|
|
131
|
+
const r14 = priceMove?.days14?.return;
|
|
132
|
+
const arr = [r3, r7, r14].filter((v: unknown) => typeof v === 'number') as number[];
|
|
133
|
+
if (!arr.length) return '評価不可(事後データ不足)';
|
|
134
|
+
const best = arr.reduce((m, v) => (Math.abs(v) > Math.abs(m) ? v : m), 0);
|
|
135
|
+
const isBullish = isPennant ? p?.poleDirection === 'up' : BULLISH_TYPES.includes(String(p?.type));
|
|
136
|
+
const expected = isBullish ? 1 : -1;
|
|
137
|
+
const actual = best > 0 ? 1 : -1;
|
|
138
|
+
if (expected === actual && Math.abs(best) > 3)
|
|
139
|
+
return `部分成功(ブレイクアウト後${best > 0 ? '+' : ''}${best.toFixed(1)}%、目標未達)`;
|
|
140
|
+
if (expected !== actual && Math.abs(best) > 3)
|
|
141
|
+
return `失敗(ブレイクアウト後、期待と逆方向に${best > 0 ? '+' : ''}${best.toFixed(1)}%)`;
|
|
142
|
+
return `失敗(ブレイクアウト後、値動き僅少: ${best > 0 ? '+' : ''}${best.toFixed(1)}%)`;
|
|
143
|
+
}
|
|
144
|
+
const outcome = outcomeMessage();
|
|
145
|
+
return {
|
|
146
|
+
breakoutDate,
|
|
147
|
+
breakoutConfirmed,
|
|
148
|
+
priceMove,
|
|
149
|
+
targetReached,
|
|
150
|
+
theoreticalTarget: Number.isFinite(theoreticalTarget) ? Math.round(theoreticalTarget) : null,
|
|
151
|
+
outcome,
|
|
152
|
+
daysToTarget,
|
|
153
|
+
};
|
|
154
|
+
} catch {
|
|
155
|
+
return null;
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// ---------------------------------------------------------------------------
|
|
160
|
+
// 統計ビルド
|
|
161
|
+
// ---------------------------------------------------------------------------
|
|
162
|
+
export function buildStatistics(
|
|
163
|
+
patterns: PatternEntry[],
|
|
164
|
+
candles: CandleData[],
|
|
165
|
+
): { statistics: Record<string, unknown>; isoToIndex: Map<string, number> } {
|
|
166
|
+
const isoToIndex = new Map<string, number>();
|
|
167
|
+
for (let i = 0; i < candles.length; i++) {
|
|
168
|
+
const t = candles[i]?.isoTime;
|
|
169
|
+
if (t) isoToIndex.set(String(t), i);
|
|
170
|
+
}
|
|
171
|
+
const stats: Record<
|
|
172
|
+
string,
|
|
173
|
+
{ detected: number; withAftermath: number; success: number; r7: number[]; r14: number[] }
|
|
174
|
+
> = {};
|
|
175
|
+
for (const p of patterns) {
|
|
176
|
+
const a = analyzeAftermath(p, candles, isoToIndex);
|
|
177
|
+
if (a) p.aftermath = a;
|
|
178
|
+
const t = String(p.type);
|
|
179
|
+
if (!stats[t]) stats[t] = { detected: 0, withAftermath: 0, success: 0, r7: [], r14: [] };
|
|
180
|
+
stats[t].detected += 1;
|
|
181
|
+
if (a) {
|
|
182
|
+
stats[t].withAftermath += 1;
|
|
183
|
+
if (a.outcome === 'success') stats[t].success += 1;
|
|
184
|
+
const r7 = a?.priceMove?.days7?.return;
|
|
185
|
+
if (typeof r7 === 'number') stats[t].r7.push(r7);
|
|
186
|
+
const r14 = a?.priceMove?.days14?.return;
|
|
187
|
+
if (typeof r14 === 'number') stats[t].r14.push(r14);
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
const avg = (arr: number[]) => {
|
|
192
|
+
const v = avgRaw(arr);
|
|
193
|
+
return v != null ? Number(v.toFixed(2)) : null;
|
|
194
|
+
};
|
|
195
|
+
const med = (arr: number[]) => {
|
|
196
|
+
const v = medianRaw(arr);
|
|
197
|
+
return v != null ? Number(v.toFixed(2)) : null;
|
|
198
|
+
};
|
|
199
|
+
|
|
200
|
+
const statistics: Record<string, unknown> = {};
|
|
201
|
+
for (const [k, v] of Object.entries(stats)) {
|
|
202
|
+
statistics[k] = {
|
|
203
|
+
detected: v.detected,
|
|
204
|
+
withAftermath: v.withAftermath,
|
|
205
|
+
successRate: v.withAftermath ? Number((v.success / v.withAftermath).toFixed(2)) : null,
|
|
206
|
+
avgReturn7d: avg(v.r7),
|
|
207
|
+
avgReturn14d: avg(v.r14),
|
|
208
|
+
medianReturn7d: med(v.r7),
|
|
209
|
+
};
|
|
210
|
+
}
|
|
211
|
+
return { statistics, isoToIndex };
|
|
212
|
+
}
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* patterns/config.ts - 時間軸に応じたパラメータ設定
|
|
3
|
+
*
|
|
4
|
+
* パターン検出で使用するデフォルトパラメータを時間軸ごとに提供する。
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
/** パターンごとの最小整合度(閾値) */
|
|
8
|
+
export const MIN_CONFIDENCE: Record<string, number> = {
|
|
9
|
+
triple_top: 0.7,
|
|
10
|
+
triple_bottom: 0.7,
|
|
11
|
+
double_top: 0.65,
|
|
12
|
+
double_bottom: 0.65,
|
|
13
|
+
head_and_shoulders: 0.7,
|
|
14
|
+
inverse_head_and_shoulders: 0.7,
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
/** スキーマのデフォルト値(サーバ側で埋められる値) */
|
|
18
|
+
export const SCHEMA_DEFAULTS = {
|
|
19
|
+
swingDepth: 7,
|
|
20
|
+
minBarsBetweenSwings: 5,
|
|
21
|
+
tolerancePct: 0.04,
|
|
22
|
+
} as const;
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* 時間軸に応じたスイング検出パラメータを返す
|
|
26
|
+
*/
|
|
27
|
+
export function getDefaultParamsForTf(tf: string): { swingDepth: number; minBarsBetweenSwings: number } {
|
|
28
|
+
const t = String(tf);
|
|
29
|
+
// 期待動作(例)の目安に基づくデフォルト
|
|
30
|
+
if (t === '1hour') return { swingDepth: 3, minBarsBetweenSwings: 2 };
|
|
31
|
+
if (t === '4hour') return { swingDepth: 5, minBarsBetweenSwings: 3 };
|
|
32
|
+
if (t === '8hour') return { swingDepth: 5, minBarsBetweenSwings: 3 };
|
|
33
|
+
if (t === '12hour') return { swingDepth: 5, minBarsBetweenSwings: 3 };
|
|
34
|
+
if (t === '1day') return { swingDepth: 6, minBarsBetweenSwings: 4 };
|
|
35
|
+
if (t === '1week') return { swingDepth: 7, minBarsBetweenSwings: 5 };
|
|
36
|
+
if (t === '1month') return { swingDepth: 8, minBarsBetweenSwings: 6 };
|
|
37
|
+
// 分足はやや緩め(ノイズ多めのため最小幅は確保)
|
|
38
|
+
if (t === '30min') return { swingDepth: 3, minBarsBetweenSwings: 2 };
|
|
39
|
+
if (t === '15min') return { swingDepth: 3, minBarsBetweenSwings: 2 };
|
|
40
|
+
if (t === '5min') return { swingDepth: 2, minBarsBetweenSwings: 1 };
|
|
41
|
+
if (t === '1min') return { swingDepth: 2, minBarsBetweenSwings: 1 };
|
|
42
|
+
// フォールバック(日足相当)
|
|
43
|
+
return { swingDepth: 6, minBarsBetweenSwings: 4 };
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* 時間軸に応じた許容誤差(tolerancePct)を返す
|
|
48
|
+
*/
|
|
49
|
+
export function getDefaultToleranceForTf(tf: string): number {
|
|
50
|
+
const t = String(tf);
|
|
51
|
+
if (t === '1hour' || t === '4hour') return 0.05; // 5%
|
|
52
|
+
if (t === '8hour' || t === '12hour') return 0.045; // 4.5%
|
|
53
|
+
if (t === '15min' || t === '30min') return 0.06; // 6%
|
|
54
|
+
if (t === '1week') return 0.035; // 3.5%
|
|
55
|
+
if (t === '1month') return 0.03; // 3.0%
|
|
56
|
+
return 0.04; // 1day 他
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* 時間軸に応じた収束係数を返す(三角形・ウェッジ用)
|
|
61
|
+
*/
|
|
62
|
+
export function getConvergenceFactorForTf(tf: string): number {
|
|
63
|
+
const t = String(tf);
|
|
64
|
+
if (t === '1hour' || t === '4hour' || t === '15min' || t === '30min') return 0.6;
|
|
65
|
+
return 0.8; // default
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* 時間軸に応じた三角形の係数を返す
|
|
70
|
+
*/
|
|
71
|
+
export function getTriangleCoeffForTf(tf: string): { flat: number; move: number } {
|
|
72
|
+
const t = String(tf);
|
|
73
|
+
if (t === '1hour' || t === '4hour') return { flat: 1.2, move: 0.8 };
|
|
74
|
+
return { flat: 0.8, move: 1.2 };
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* 時間軸に応じた最小フィット値を返す
|
|
79
|
+
*/
|
|
80
|
+
export function getMinFitForTf(tf: string): number {
|
|
81
|
+
const t = String(tf);
|
|
82
|
+
if (t === '1hour' || t === '4hour') return 0.6;
|
|
83
|
+
if (t === '1day') return 0.7;
|
|
84
|
+
return 0.75;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* 時間軸に応じた三角形のウィンドウサイズを返す
|
|
89
|
+
*/
|
|
90
|
+
export function getTriangleWindowSize(tf: string): number {
|
|
91
|
+
const t = String(tf);
|
|
92
|
+
// 長期: 大きなパターン
|
|
93
|
+
if (t === '1month') return 30;
|
|
94
|
+
if (t === '1week') return 40;
|
|
95
|
+
// 中期
|
|
96
|
+
if (t === '1day') return 50;
|
|
97
|
+
// 短期
|
|
98
|
+
if (t === '4hour') return 30;
|
|
99
|
+
if (t === '1hour') return 40;
|
|
100
|
+
if (t === '30min') return 30;
|
|
101
|
+
if (t === '15min') return 30;
|
|
102
|
+
return 20;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* オプションとスキーマデフォルトから実効パラメータを解決する
|
|
107
|
+
*/
|
|
108
|
+
export function resolveParams(
|
|
109
|
+
tf: string,
|
|
110
|
+
opts: Partial<{
|
|
111
|
+
swingDepth: number;
|
|
112
|
+
tolerancePct: number;
|
|
113
|
+
minBarsBetweenSwings: number;
|
|
114
|
+
}>,
|
|
115
|
+
): {
|
|
116
|
+
swingDepth: number;
|
|
117
|
+
tolerancePct: number;
|
|
118
|
+
minBarsBetweenSwings: number;
|
|
119
|
+
autoScaled: boolean;
|
|
120
|
+
} {
|
|
121
|
+
const auto = getDefaultParamsForTf(tf);
|
|
122
|
+
const tolAuto = getDefaultToleranceForTf(tf);
|
|
123
|
+
|
|
124
|
+
// swingDepth: スキーマ既定値(7)が来た場合は時間軸オートに置換
|
|
125
|
+
const swingDepth = Number.isFinite(opts.swingDepth as number)
|
|
126
|
+
? (opts.swingDepth as number) === SCHEMA_DEFAULTS.swingDepth
|
|
127
|
+
? auto.swingDepth
|
|
128
|
+
: (opts.swingDepth as number)
|
|
129
|
+
: auto.swingDepth;
|
|
130
|
+
|
|
131
|
+
// tolerancePct: スキーマ既定値(0.04)が来た場合は時間軸オートを採用
|
|
132
|
+
const tolerancePct =
|
|
133
|
+
typeof opts.tolerancePct === 'number' && !Number.isNaN(opts.tolerancePct)
|
|
134
|
+
? (opts.tolerancePct as number) === SCHEMA_DEFAULTS.tolerancePct
|
|
135
|
+
? tolAuto
|
|
136
|
+
: (opts.tolerancePct as number)
|
|
137
|
+
: tolAuto;
|
|
138
|
+
|
|
139
|
+
// minBarsBetweenSwings: 同様に既定値(5)なら時間軸オートに置換
|
|
140
|
+
const minBarsBetweenSwings = Number.isFinite(opts.minBarsBetweenSwings as number)
|
|
141
|
+
? (opts.minBarsBetweenSwings as number) === SCHEMA_DEFAULTS.minBarsBetweenSwings
|
|
142
|
+
? auto.minBarsBetweenSwings
|
|
143
|
+
: (opts.minBarsBetweenSwings as number)
|
|
144
|
+
: auto.minBarsBetweenSwings;
|
|
145
|
+
|
|
146
|
+
const autoScaled = !(
|
|
147
|
+
Number.isFinite(opts.swingDepth as number) || Number.isFinite(opts.minBarsBetweenSwings as number)
|
|
148
|
+
);
|
|
149
|
+
|
|
150
|
+
return { swingDepth, tolerancePct, minBarsBetweenSwings, autoScaled };
|
|
151
|
+
}
|