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,517 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* パターン検出の共通ヘルパー関数
|
|
3
|
+
*
|
|
4
|
+
* detect_patterns.ts 内にネストされていた関数を、candles 等を明示的に受け取る
|
|
5
|
+
* モジュールレベル関数に変換したもの。
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { dayjs } from '../../lib/datetime.js';
|
|
9
|
+
import { trueRange } from '../../lib/indicators.js';
|
|
10
|
+
import type {
|
|
11
|
+
CandleData,
|
|
12
|
+
DeduplicablePattern,
|
|
13
|
+
PatternScoreComponents,
|
|
14
|
+
PatternScoreWeights,
|
|
15
|
+
TouchPoint,
|
|
16
|
+
TouchResult,
|
|
17
|
+
TrendLine,
|
|
18
|
+
WedgeParams,
|
|
19
|
+
} from './types.js';
|
|
20
|
+
|
|
21
|
+
// ---------------------------------------------------------------------------
|
|
22
|
+
// ATR 計算(lib/indicators.ts の trueRange に委譲)
|
|
23
|
+
// ---------------------------------------------------------------------------
|
|
24
|
+
export function calcATR(candles: readonly CandleData[], from: number, to: number, period: number = 14): number {
|
|
25
|
+
const start = Math.max(1, from);
|
|
26
|
+
const end = Math.max(start + 1, to);
|
|
27
|
+
const slice = candles.slice(start - 1, end + 1); // prevClose 用に 1 つ前を含める
|
|
28
|
+
if (slice.length < 2) return 0;
|
|
29
|
+
|
|
30
|
+
const highs = slice.map((c) => Number(c?.high ?? NaN));
|
|
31
|
+
const lows = slice.map((c) => Number(c?.low ?? NaN));
|
|
32
|
+
const closes = slice.map((c) => Number(c?.close ?? NaN));
|
|
33
|
+
|
|
34
|
+
const tr = trueRange(highs, lows, closes);
|
|
35
|
+
// tr[0] は NaN(prevClose がない)、有効値は tr[1..] — 直近 period 個を取る
|
|
36
|
+
const validTr = tr.filter((v) => Number.isFinite(v));
|
|
37
|
+
if (!validTr.length) return 0;
|
|
38
|
+
const n = Math.min(period, validTr.length);
|
|
39
|
+
const tail = validTr.slice(-n);
|
|
40
|
+
return tail.reduce((s, v) => s + v, 0) / tail.length;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// ---------------------------------------------------------------------------
|
|
44
|
+
// ウェッジのブレイク検出
|
|
45
|
+
// ---------------------------------------------------------------------------
|
|
46
|
+
export interface WedgeBreakResult {
|
|
47
|
+
detected: boolean;
|
|
48
|
+
breakIdx: number;
|
|
49
|
+
breakIsoTime: string | null;
|
|
50
|
+
breakPrice: number | null;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export function detectWedgeBreak(
|
|
54
|
+
candles: CandleData[],
|
|
55
|
+
_wedgeType: 'falling_wedge' | 'rising_wedge',
|
|
56
|
+
upper: { valueAt: (x: number) => number },
|
|
57
|
+
lower: { valueAt: (x: number) => number },
|
|
58
|
+
startIdx: number,
|
|
59
|
+
endIdx: number,
|
|
60
|
+
lastIdx: number,
|
|
61
|
+
atr: number,
|
|
62
|
+
): WedgeBreakResult {
|
|
63
|
+
const patternBars = endIdx - startIdx;
|
|
64
|
+
const scanStart = startIdx + Math.max(20, Math.floor(patternBars * 0.3));
|
|
65
|
+
const scanEnd = Math.max(endIdx, lastIdx);
|
|
66
|
+
|
|
67
|
+
let firstBreakIdx = -1;
|
|
68
|
+
|
|
69
|
+
// 両方向をスキャンし、最初に見つかったブレイクを返す。
|
|
70
|
+
// 方向の判定は呼び出し側(detect_wedges.ts)が breakPrice と
|
|
71
|
+
// トレンドラインの位置関係から行う。
|
|
72
|
+
// - falling_wedge: 上方ブレイク(uLine 超え)が教科書的
|
|
73
|
+
// - rising_wedge: 下方ブレイク(lLine 割れ)が教科書的
|
|
74
|
+
for (let i = scanStart; i <= scanEnd; i++) {
|
|
75
|
+
const close = Number(candles[i]?.close ?? NaN);
|
|
76
|
+
if (!Number.isFinite(close)) continue;
|
|
77
|
+
|
|
78
|
+
const uLine = upper.valueAt(i);
|
|
79
|
+
const lLine = lower.valueAt(i);
|
|
80
|
+
if (!Number.isFinite(uLine) || !Number.isFinite(lLine)) continue;
|
|
81
|
+
|
|
82
|
+
if (close > uLine + atr * 0.5 || close < lLine - atr * 0.5) {
|
|
83
|
+
firstBreakIdx = i;
|
|
84
|
+
break;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
if (firstBreakIdx !== -1) {
|
|
89
|
+
return {
|
|
90
|
+
detected: true,
|
|
91
|
+
breakIdx: firstBreakIdx,
|
|
92
|
+
breakIsoTime: candles[firstBreakIdx]?.isoTime ?? null,
|
|
93
|
+
breakPrice: Number(candles[firstBreakIdx]?.close ?? NaN),
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
return { detected: false, breakIdx: -1, breakIsoTime: null, breakPrice: null };
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// ---------------------------------------------------------------------------
|
|
100
|
+
// ウィンドウ生成
|
|
101
|
+
// ---------------------------------------------------------------------------
|
|
102
|
+
export function generateWindows(
|
|
103
|
+
totalBars: number,
|
|
104
|
+
minSize: number,
|
|
105
|
+
maxSize: number,
|
|
106
|
+
step: number,
|
|
107
|
+
): Array<{ startIdx: number; endIdx: number }> {
|
|
108
|
+
const out: Array<{ startIdx: number; endIdx: number }> = [];
|
|
109
|
+
for (let size = minSize; size <= maxSize; size += step) {
|
|
110
|
+
for (let start = 0; start + size < totalBars; start += step) {
|
|
111
|
+
out.push({ startIdx: start, endIdx: start + size });
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
return out;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// ---------------------------------------------------------------------------
|
|
118
|
+
// ウェッジタイプ判定
|
|
119
|
+
// ---------------------------------------------------------------------------
|
|
120
|
+
export function determineWedgeType(
|
|
121
|
+
slopeHigh: number,
|
|
122
|
+
slopeLow: number,
|
|
123
|
+
params: WedgeParams,
|
|
124
|
+
): 'rising_wedge' | 'falling_wedge' | null {
|
|
125
|
+
const minSlope = params?.minSlope ?? 0.0001;
|
|
126
|
+
const ratioMinRising = params?.slopeRatioMinRising ?? 1.2;
|
|
127
|
+
const ratioMinFalling = params?.slopeRatioMinFalling ?? params?.slopeRatioMin ?? 1.15;
|
|
128
|
+
const minWeakerSlopeRatio = params?.minWeakerSlopeRatio ?? 0.3;
|
|
129
|
+
|
|
130
|
+
// Rising Wedge: 両ライン上向き、下側がより急
|
|
131
|
+
if (slopeHigh > minSlope && slopeLow > minSlope) {
|
|
132
|
+
if (slopeHigh < slopeLow * minWeakerSlopeRatio) return null;
|
|
133
|
+
if (Math.abs(slopeLow) >= Math.abs(slopeHigh) * ratioMinRising) return 'rising_wedge';
|
|
134
|
+
}
|
|
135
|
+
// Falling Wedge: 両ライン下向き、上側がより急
|
|
136
|
+
if (slopeHigh < -minSlope && slopeLow < -minSlope) {
|
|
137
|
+
const absHi = Math.abs(slopeHigh);
|
|
138
|
+
const absLo = Math.abs(slopeLow);
|
|
139
|
+
const weakerRatio = Math.min(absHi, absLo) / Math.max(absHi, absLo);
|
|
140
|
+
if (weakerRatio < minWeakerSlopeRatio) return null;
|
|
141
|
+
if (absHi >= absLo * ratioMinFalling) return 'falling_wedge';
|
|
142
|
+
return null;
|
|
143
|
+
}
|
|
144
|
+
const slopeRatio = Math.abs(slopeLow / (slopeHigh || slopeLow * 1e-6));
|
|
145
|
+
if (slopeRatio > 0.9 && slopeRatio < 1.1) return null;
|
|
146
|
+
return null;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// ---------------------------------------------------------------------------
|
|
150
|
+
// Apex(頂点)計算 — UAlgo 方式
|
|
151
|
+
// ---------------------------------------------------------------------------
|
|
152
|
+
/**
|
|
153
|
+
* 2本のトレンドラインの交差点(Apex)を計算する。
|
|
154
|
+
*
|
|
155
|
+
* UAlgo: apex_x = (y2 - y1 + m1*x1 - m2*x2) / (m1 - m2)
|
|
156
|
+
* 線形回帰の場合: upper = slope_u * x + intercept_u, lower = slope_l * x + intercept_l
|
|
157
|
+
* 交点: slope_u * x + intercept_u = slope_l * x + intercept_l
|
|
158
|
+
* => x = (intercept_l - intercept_u) / (slope_u - slope_l)
|
|
159
|
+
*/
|
|
160
|
+
export interface ApexResult {
|
|
161
|
+
/** Apex のバーインデックス */
|
|
162
|
+
apexIdx: number;
|
|
163
|
+
/** Apex の価格 */
|
|
164
|
+
apexPrice: number;
|
|
165
|
+
/** Apex が有効か(未来にあるか) */
|
|
166
|
+
isValid: boolean;
|
|
167
|
+
/** 現在のバーからApexまでのバー数 */
|
|
168
|
+
barsToApex: number;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
export function calcApex(upper: TrendLine, lower: TrendLine, endIdx: number): ApexResult {
|
|
172
|
+
const slopeDiff = upper.slope - lower.slope;
|
|
173
|
+
if (Math.abs(slopeDiff) < 1e-15) {
|
|
174
|
+
// 平行 — Apex は無限遠
|
|
175
|
+
return { apexIdx: Infinity, apexPrice: NaN, isValid: false, barsToApex: Infinity };
|
|
176
|
+
}
|
|
177
|
+
const apexIdx = Math.round((lower.intercept - upper.intercept) / slopeDiff);
|
|
178
|
+
const apexPrice = upper.valueAt(apexIdx);
|
|
179
|
+
const barsToApex = apexIdx - endIdx;
|
|
180
|
+
// UAlgo: Apex がウィンドウ終端より先(未来)にあること
|
|
181
|
+
const isValid = barsToApex > 0;
|
|
182
|
+
return { apexIdx, apexPrice, isValid, barsToApex };
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
// ---------------------------------------------------------------------------
|
|
186
|
+
// 包含ルール(Containment) — UAlgo 方式
|
|
187
|
+
// ---------------------------------------------------------------------------
|
|
188
|
+
/**
|
|
189
|
+
* ウェッジ形成中に終値が境界外に出ていないかチェックする。
|
|
190
|
+
*
|
|
191
|
+
* UAlgo: "no candle close is allowed outside the upper or lower boundary"
|
|
192
|
+
* 厳格モードではハード棄却。緩和モードでは許容率を返す。
|
|
193
|
+
*
|
|
194
|
+
* @returns closeInsideRatio: 終値が境界内に収まっている割合 (0-1)
|
|
195
|
+
*/
|
|
196
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
197
|
+
export function checkContainment(
|
|
198
|
+
candles: CandleData[],
|
|
199
|
+
upper: { valueAt: (x: number) => number },
|
|
200
|
+
lower: { valueAt: (x: number) => number },
|
|
201
|
+
startIdx: number,
|
|
202
|
+
endIdx: number,
|
|
203
|
+
tolerancePct: number = 0.003,
|
|
204
|
+
): { closeInsideRatio: number; violations: number; total: number } {
|
|
205
|
+
let inside = 0;
|
|
206
|
+
let total = 0;
|
|
207
|
+
let violations = 0;
|
|
208
|
+
for (let i = startIdx; i <= endIdx; i++) {
|
|
209
|
+
const c = candles[i];
|
|
210
|
+
if (!c) continue;
|
|
211
|
+
total++;
|
|
212
|
+
const close = c.close;
|
|
213
|
+
const u = upper.valueAt(i);
|
|
214
|
+
const l = lower.valueAt(i);
|
|
215
|
+
const tol = Math.abs(u - l) * tolerancePct;
|
|
216
|
+
if (close > u + tol || close < l - tol) {
|
|
217
|
+
violations++;
|
|
218
|
+
} else {
|
|
219
|
+
inside++;
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
return { closeInsideRatio: total > 0 ? inside / total : 0, violations, total };
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
// ---------------------------------------------------------------------------
|
|
226
|
+
// ウェッジ品質スコアリング関数群
|
|
227
|
+
// ---------------------------------------------------------------------------
|
|
228
|
+
/**
|
|
229
|
+
* 収束チェック — Apexベースに移行
|
|
230
|
+
*
|
|
231
|
+
* 旧: gapEnd/gapStart < 0.30(非常に厳しい固定閾値)
|
|
232
|
+
* 新: gapEnd > 0 かつ gapEnd < gapStart(基本収束条件)
|
|
233
|
+
* + Apex が未来にある(UAlgo 方式)
|
|
234
|
+
* + ratio を 0.70 以下に緩和(ギャップが30%以上狭まっていればOK)
|
|
235
|
+
*
|
|
236
|
+
* スコアは ratio と加速度で決まる。
|
|
237
|
+
*/
|
|
238
|
+
export function checkConvergenceEx(upper: TrendLine, lower: TrendLine, startIdx: number, endIdx: number) {
|
|
239
|
+
const midIdx = Math.floor((startIdx + endIdx) / 2);
|
|
240
|
+
const gapStart = upper.valueAt(startIdx) - lower.valueAt(startIdx);
|
|
241
|
+
const gapMid = upper.valueAt(midIdx) - lower.valueAt(midIdx);
|
|
242
|
+
const gapEnd = upper.valueAt(endIdx) - lower.valueAt(endIdx);
|
|
243
|
+
const ratio = gapEnd / Math.max(1e-12, gapStart);
|
|
244
|
+
|
|
245
|
+
// 基本条件: ギャップが正で、かつ少なくとも30%収束している
|
|
246
|
+
if (!(gapEnd > 0) || !(ratio < 0.7)) return { isConverging: false, gapStart, gapEnd, ratio };
|
|
247
|
+
|
|
248
|
+
// Apex が未来にあるかチェック(スコアへのボーナス)
|
|
249
|
+
const apex = calcApex(upper, lower, endIdx);
|
|
250
|
+
|
|
251
|
+
const firstHalf = gapStart - gapMid;
|
|
252
|
+
const secondHalf = gapMid - gapEnd;
|
|
253
|
+
const isAccelerating = secondHalf > firstHalf * 1.2;
|
|
254
|
+
|
|
255
|
+
// スコア計算: 収束度 + Apexの位置 + 加速度
|
|
256
|
+
const convergenceComponent = 0.4 * (1 - ratio);
|
|
257
|
+
const apexComponent = 0.35 * (apex.isValid ? 1 : 0.3);
|
|
258
|
+
const accelComponent = 0.25 * (isAccelerating ? 1 : 0.4);
|
|
259
|
+
const score = Math.max(0, Math.min(1, convergenceComponent + apexComponent + accelComponent));
|
|
260
|
+
|
|
261
|
+
return { isConverging: true, gapStart, gapMid, gapEnd, ratio, isAccelerating, apex, score };
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
export function evaluateTouchesEx(
|
|
265
|
+
candles: readonly CandleData[],
|
|
266
|
+
upper: Pick<TrendLine, 'valueAt'>,
|
|
267
|
+
lower: Pick<TrendLine, 'valueAt'>,
|
|
268
|
+
startIdx: number,
|
|
269
|
+
endIdx: number,
|
|
270
|
+
): TouchResult {
|
|
271
|
+
const touchThresholdPct = 0.005;
|
|
272
|
+
const upperTouches: TouchPoint[] = [],
|
|
273
|
+
lowerTouches: TouchPoint[] = [];
|
|
274
|
+
for (let i = startIdx; i <= endIdx; i++) {
|
|
275
|
+
const c = candles[i];
|
|
276
|
+
if (!c) continue;
|
|
277
|
+
const u = upper.valueAt(i),
|
|
278
|
+
l = lower.valueAt(i);
|
|
279
|
+
const thrUp = Math.abs(u) * touchThresholdPct;
|
|
280
|
+
const distU = Math.abs(c.high - u);
|
|
281
|
+
if (distU < thrUp && c.high <= u + thrUp) upperTouches.push({ index: i, distance: distU, isBreak: false });
|
|
282
|
+
else if (c.high > u + thrUp) upperTouches.push({ index: i, distance: distU, isBreak: true });
|
|
283
|
+
const thrLo = Math.abs(l) * touchThresholdPct;
|
|
284
|
+
const distL = Math.abs(c.low - l);
|
|
285
|
+
if (distL < thrLo && c.low >= l - thrLo) lowerTouches.push({ index: i, distance: distL, isBreak: false });
|
|
286
|
+
else if (c.low < l - thrLo) lowerTouches.push({ index: i, distance: distL, isBreak: true });
|
|
287
|
+
}
|
|
288
|
+
const upQ = upperTouches.filter((t) => !t.isBreak).length;
|
|
289
|
+
const loQ = lowerTouches.filter((t) => !t.isBreak).length;
|
|
290
|
+
const score = Math.max(0, Math.min(1, (upQ + loQ) / 8));
|
|
291
|
+
return { upperTouches, lowerTouches, upperQuality: upQ, lowerQuality: loQ, score };
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
export function calcAlternationScoreEx(touches: TouchResult): number {
|
|
295
|
+
const all = [
|
|
296
|
+
...touches.upperTouches.map((t) => ({ ...t, type: 'upper' as const })),
|
|
297
|
+
...touches.lowerTouches.map((t) => ({ ...t, type: 'lower' as const })),
|
|
298
|
+
].sort((a, b) => a.index - b.index);
|
|
299
|
+
if (all.length < 2) return 0;
|
|
300
|
+
let alternations = 0;
|
|
301
|
+
for (let i = 1; i < all.length; i++) {
|
|
302
|
+
if (all[i].type !== all[i - 1].type) alternations++;
|
|
303
|
+
}
|
|
304
|
+
return Math.max(0, Math.min(1, alternations / Math.max(1, all.length - 1)));
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
export function calcInsideRatioEx(
|
|
308
|
+
candles: readonly CandleData[],
|
|
309
|
+
upper: Pick<TrendLine, 'valueAt'>,
|
|
310
|
+
lower: Pick<TrendLine, 'valueAt'>,
|
|
311
|
+
startIdx: number,
|
|
312
|
+
endIdx: number,
|
|
313
|
+
): number {
|
|
314
|
+
let inside = 0,
|
|
315
|
+
total = 0;
|
|
316
|
+
for (let i = startIdx; i <= endIdx; i++) {
|
|
317
|
+
const c = candles[i];
|
|
318
|
+
if (!c) continue;
|
|
319
|
+
total++;
|
|
320
|
+
const u = upper.valueAt(i),
|
|
321
|
+
l = lower.valueAt(i);
|
|
322
|
+
if (c.high <= u && c.low >= l) inside++;
|
|
323
|
+
}
|
|
324
|
+
return total ? inside / total : 0;
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
export function calcDurationScoreEx(
|
|
328
|
+
bars: number,
|
|
329
|
+
params: Pick<WedgeParams, 'windowSizeMin' | 'windowSizeMax'>,
|
|
330
|
+
): number {
|
|
331
|
+
const min = params?.windowSizeMin ?? 25,
|
|
332
|
+
max = params?.windowSizeMax ?? 90;
|
|
333
|
+
if (bars < min) return 0;
|
|
334
|
+
if (bars > max) return 0;
|
|
335
|
+
const mid = (min + max) / 2;
|
|
336
|
+
const dist = Math.abs(bars - mid) / Math.max(1, (max - min) / 2);
|
|
337
|
+
return Math.max(0, Math.min(1, 1 - dist));
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
export function calculatePatternScoreEx(components: PatternScoreComponents, weights?: PatternScoreWeights): number {
|
|
341
|
+
const w = weights || { fit: 0.25, converge: 0.25, touch: 0.35, alternation: 0.07, inside: 0.05, duration: 0.03 };
|
|
342
|
+
return (
|
|
343
|
+
w.fit * components.fitScore +
|
|
344
|
+
w.converge * components.convergeScore +
|
|
345
|
+
w.touch * components.touchScore +
|
|
346
|
+
w.alternation * components.alternationScore +
|
|
347
|
+
w.inside * components.insideScore +
|
|
348
|
+
w.duration * components.durationScore
|
|
349
|
+
);
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
// ---------------------------------------------------------------------------
|
|
353
|
+
// パターン共通スコアリング
|
|
354
|
+
// ---------------------------------------------------------------------------
|
|
355
|
+
export function periodScoreDays(startIso?: string, endIso?: string): number {
|
|
356
|
+
if (!startIso || !endIso) return 0.7;
|
|
357
|
+
const d = Math.abs(dayjs(endIso).diff(dayjs(startIso), 'day', true));
|
|
358
|
+
if (d < 5) return 0.6;
|
|
359
|
+
if (d < 15) return 0.8;
|
|
360
|
+
if (d < 30) return 0.9;
|
|
361
|
+
return 0.7;
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
export function finalizeConf(base: number, type: string): number {
|
|
365
|
+
const adj =
|
|
366
|
+
type === 'head_and_shoulders' || type === 'inverse_head_and_shoulders'
|
|
367
|
+
? 1.1
|
|
368
|
+
: type === 'triple_top' || type === 'triple_bottom'
|
|
369
|
+
? 1.05
|
|
370
|
+
: type.startsWith('triangle') || type === 'pennant' || type === 'flag'
|
|
371
|
+
? 0.95
|
|
372
|
+
: 1.0;
|
|
373
|
+
const v = Math.min(1, Math.max(0, base * adj));
|
|
374
|
+
return Math.round(v * 100) / 100;
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
// ---------------------------------------------------------------------------
|
|
378
|
+
// 重複パターンの排除
|
|
379
|
+
// ---------------------------------------------------------------------------
|
|
380
|
+
export function deduplicatePatterns<T extends DeduplicablePattern>(arr: T[]): T[] {
|
|
381
|
+
const result: T[] = [];
|
|
382
|
+
for (const pattern of arr) {
|
|
383
|
+
if (!pattern?.type || !pattern?.range?.start || !pattern?.range?.end) {
|
|
384
|
+
result.push(pattern);
|
|
385
|
+
continue;
|
|
386
|
+
}
|
|
387
|
+
const overlapping = result.filter((existing) => {
|
|
388
|
+
if (existing?.type !== pattern.type) return false;
|
|
389
|
+
const existingStart = Date.parse(existing.range?.start ?? '');
|
|
390
|
+
const existingEnd = Date.parse(existing.range?.end ?? '');
|
|
391
|
+
const patternStart = Date.parse(pattern.range?.start ?? '');
|
|
392
|
+
const patternEnd = Date.parse(pattern.range?.end ?? '');
|
|
393
|
+
if (
|
|
394
|
+
!Number.isFinite(existingStart) ||
|
|
395
|
+
!Number.isFinite(existingEnd) ||
|
|
396
|
+
!Number.isFinite(patternStart) ||
|
|
397
|
+
!Number.isFinite(patternEnd)
|
|
398
|
+
)
|
|
399
|
+
return false;
|
|
400
|
+
const overlapStart = Math.max(existingStart, patternStart);
|
|
401
|
+
const overlapEnd = Math.min(existingEnd, patternEnd);
|
|
402
|
+
const overlapDuration = Math.max(0, overlapEnd - overlapStart);
|
|
403
|
+
const existingDuration = Math.max(1, existingEnd - existingStart);
|
|
404
|
+
const patternDuration = Math.max(1, patternEnd - patternStart);
|
|
405
|
+
const minDuration = Math.min(existingDuration, patternDuration);
|
|
406
|
+
return overlapDuration / minDuration > 0.5;
|
|
407
|
+
});
|
|
408
|
+
if (overlapping.length === 0) {
|
|
409
|
+
result.push(pattern);
|
|
410
|
+
} else {
|
|
411
|
+
const allCandidates = [...overlapping, pattern];
|
|
412
|
+
const maxEndTime = Math.max(...allCandidates.map((p) => Date.parse(p.range?.end ?? '')));
|
|
413
|
+
let best = allCandidates.filter((p) => Date.parse(p.range?.end ?? '') === maxEndTime);
|
|
414
|
+
if (best.length > 1) {
|
|
415
|
+
const maxConfidence = Math.max(...best.map((p) => Number(p.confidence ?? 0)));
|
|
416
|
+
best = best.filter((p) => Number(p.confidence ?? 0) === maxConfidence);
|
|
417
|
+
}
|
|
418
|
+
if (best.length > 1) {
|
|
419
|
+
const getHeight = (p: DeduplicablePattern) => {
|
|
420
|
+
const piv = Array.isArray(p?.pivots) ? p.pivots : [];
|
|
421
|
+
if (p?.type === 'double_top' && piv.length >= 3) {
|
|
422
|
+
const peak = Math.max(Number(piv[0]?.price ?? 0), Number(piv[2]?.price ?? 0));
|
|
423
|
+
const valley = Number(piv[1]?.price ?? peak);
|
|
424
|
+
return Math.max(0, peak - valley);
|
|
425
|
+
}
|
|
426
|
+
if (p?.type === 'double_bottom' && piv.length >= 3) {
|
|
427
|
+
const valley = Math.min(Number(piv[0]?.price ?? 0), Number(piv[2]?.price ?? 0));
|
|
428
|
+
const peak = Number(piv[1]?.price ?? valley);
|
|
429
|
+
return Math.max(0, peak - valley);
|
|
430
|
+
}
|
|
431
|
+
return 0;
|
|
432
|
+
};
|
|
433
|
+
const maxHeight = Math.max(...best.map(getHeight));
|
|
434
|
+
best = best.filter((p) => getHeight(p) === maxHeight);
|
|
435
|
+
}
|
|
436
|
+
const winner = best[0];
|
|
437
|
+
for (const dup of overlapping) {
|
|
438
|
+
const idx = result.indexOf(dup);
|
|
439
|
+
if (idx >= 0) result.splice(idx, 1);
|
|
440
|
+
}
|
|
441
|
+
result.push(winner);
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
return result;
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
// ---------------------------------------------------------------------------
|
|
448
|
+
// グローバル重複排除(全パターン種別横断)
|
|
449
|
+
// ---------------------------------------------------------------------------
|
|
450
|
+
export function globalDedup(patterns: DeduplicablePattern[]): DeduplicablePattern[] {
|
|
451
|
+
function toMs(iso?: string): number {
|
|
452
|
+
try {
|
|
453
|
+
const t = Date.parse(String(iso));
|
|
454
|
+
return Number.isFinite(t) ? t : NaN;
|
|
455
|
+
} catch {
|
|
456
|
+
return NaN;
|
|
457
|
+
}
|
|
458
|
+
}
|
|
459
|
+
function overlapRatio(aStart: string, aEnd: string, bStart: string, bEnd: string): number {
|
|
460
|
+
const as = toMs(aStart),
|
|
461
|
+
ae = toMs(aEnd),
|
|
462
|
+
bs = toMs(bStart),
|
|
463
|
+
be = toMs(bEnd);
|
|
464
|
+
if (!Number.isFinite(as) || !Number.isFinite(ae) || !Number.isFinite(bs) || !Number.isFinite(be)) return 0;
|
|
465
|
+
const os = Math.max(as, bs);
|
|
466
|
+
const oe = Math.min(ae, be);
|
|
467
|
+
const ov = Math.max(0, oe - os);
|
|
468
|
+
const ad = Math.max(1, ae - as);
|
|
469
|
+
const bd = Math.max(1, be - bs);
|
|
470
|
+
const minD = Math.min(ad, bd);
|
|
471
|
+
return ov / minD;
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
// 同一カテゴリとして扱うパターン群(期間重複する場合は同カテゴリ内でも dedup 対象)
|
|
475
|
+
const categoryMap: Record<string, string> = {
|
|
476
|
+
rising_wedge: 'wedge',
|
|
477
|
+
falling_wedge: 'wedge',
|
|
478
|
+
triangle_ascending: 'triangle',
|
|
479
|
+
triangle_descending: 'triangle',
|
|
480
|
+
triangle_symmetrical: 'triangle',
|
|
481
|
+
};
|
|
482
|
+
function isSameCategory(a: string, b: string): boolean {
|
|
483
|
+
if (a === b) return true;
|
|
484
|
+
const ca = categoryMap[a];
|
|
485
|
+
const cb = categoryMap[b];
|
|
486
|
+
return !!(ca && cb && ca === cb);
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
const dedupThreshold = 0.7;
|
|
490
|
+
const out: DeduplicablePattern[] = [];
|
|
491
|
+
for (const p of patterns) {
|
|
492
|
+
const pRange = { s: String(p?.range?.start), e: String(p?.range?.end ?? p?.range?.current) };
|
|
493
|
+
const overlapIdx = out.findIndex(
|
|
494
|
+
(q) =>
|
|
495
|
+
isSameCategory(String(q?.type), String(p?.type)) &&
|
|
496
|
+
overlapRatio(String(q?.range?.start), String(q?.range?.end ?? q?.range?.current), pRange.s, pRange.e) >=
|
|
497
|
+
dedupThreshold,
|
|
498
|
+
);
|
|
499
|
+
if (overlapIdx < 0) {
|
|
500
|
+
out.push(p);
|
|
501
|
+
} else {
|
|
502
|
+
const existing = out[overlapIdx];
|
|
503
|
+
const eConf = Number(existing?.confidence ?? 0);
|
|
504
|
+
const pConf = Number(p?.confidence ?? 0);
|
|
505
|
+
if (pConf > eConf) {
|
|
506
|
+
out[overlapIdx] = p;
|
|
507
|
+
} else if (pConf === eConf) {
|
|
508
|
+
const eEnd = toMs(existing?.range?.end ?? existing?.range?.current);
|
|
509
|
+
const pEnd = toMs(p?.range?.end ?? p?.range?.current);
|
|
510
|
+
if (Number.isFinite(pEnd) && Number.isFinite(eEnd) && pEnd > eEnd) {
|
|
511
|
+
out[overlapIdx] = p;
|
|
512
|
+
}
|
|
513
|
+
}
|
|
514
|
+
}
|
|
515
|
+
}
|
|
516
|
+
return out;
|
|
517
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* patterns/index.ts - パターン検出モジュールの再エクスポート
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
// config
|
|
6
|
+
export {
|
|
7
|
+
getConvergenceFactorForTf,
|
|
8
|
+
getDefaultParamsForTf,
|
|
9
|
+
getDefaultToleranceForTf,
|
|
10
|
+
getMinFitForTf,
|
|
11
|
+
getTriangleCoeffForTf,
|
|
12
|
+
getTriangleWindowSize,
|
|
13
|
+
MIN_CONFIDENCE,
|
|
14
|
+
resolveParams,
|
|
15
|
+
SCHEMA_DEFAULTS,
|
|
16
|
+
} from './config.js';
|
|
17
|
+
// regression
|
|
18
|
+
export {
|
|
19
|
+
clamp01,
|
|
20
|
+
type LinearRegressionResult,
|
|
21
|
+
type LinearRegressionWithR2Result,
|
|
22
|
+
linearRegression,
|
|
23
|
+
linearRegressionWithR2,
|
|
24
|
+
marginFromRelDev,
|
|
25
|
+
near,
|
|
26
|
+
pct,
|
|
27
|
+
type RegressionPoint,
|
|
28
|
+
relDev,
|
|
29
|
+
trendlineFit,
|
|
30
|
+
type XYPoint,
|
|
31
|
+
} from './regression.js';
|
|
32
|
+
// swing
|
|
33
|
+
export {
|
|
34
|
+
type Candle,
|
|
35
|
+
type DetectSwingePointsOptions,
|
|
36
|
+
detectSwingPoints,
|
|
37
|
+
filterPeaks,
|
|
38
|
+
filterValleys,
|
|
39
|
+
type Pivot,
|
|
40
|
+
} from './swing.js';
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* patterns/regression.ts - 回帰・トレンドライン計算
|
|
3
|
+
*
|
|
4
|
+
* パターン検出で使用する線形回帰およびトレンドラインフィット計算。
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
/** 回帰に使用するポイント(idx/price形式) */
|
|
8
|
+
export interface RegressionPoint {
|
|
9
|
+
idx: number;
|
|
10
|
+
price: number;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/** 回帰に使用するポイント(x/y形式) */
|
|
14
|
+
export interface XYPoint {
|
|
15
|
+
x: number;
|
|
16
|
+
y: number;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/** 線形回帰の結果 */
|
|
20
|
+
export interface LinearRegressionResult {
|
|
21
|
+
slope: number;
|
|
22
|
+
intercept: number;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/** R2付き線形回帰の結果 */
|
|
26
|
+
export interface LinearRegressionWithR2Result extends LinearRegressionResult {
|
|
27
|
+
r2: number;
|
|
28
|
+
valueAt: (x: number) => number;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* 線形回帰(三角保ち合い向け)
|
|
33
|
+
*/
|
|
34
|
+
export function linearRegression(points: RegressionPoint[]): LinearRegressionResult {
|
|
35
|
+
const n = points.length;
|
|
36
|
+
if (!n) return { slope: 0, intercept: 0 };
|
|
37
|
+
|
|
38
|
+
let sumX = 0,
|
|
39
|
+
sumY = 0,
|
|
40
|
+
sumXY = 0,
|
|
41
|
+
sumX2 = 0;
|
|
42
|
+
for (const p of points) {
|
|
43
|
+
sumX += p.idx;
|
|
44
|
+
sumY += p.price;
|
|
45
|
+
sumXY += p.idx * p.price;
|
|
46
|
+
sumX2 += p.idx * p.idx;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const denom = n * sumX2 - sumX * sumX || 1;
|
|
50
|
+
const slope = (n * sumXY - sumX * sumY) / denom;
|
|
51
|
+
const intercept = (sumY - slope * sumX) / n;
|
|
52
|
+
|
|
53
|
+
return { slope, intercept };
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* トレンドラインへのフィット度を計算(0-1)
|
|
58
|
+
*/
|
|
59
|
+
export function trendlineFit(points: RegressionPoint[], line: LinearRegressionResult): number {
|
|
60
|
+
if (!points.length) return 0;
|
|
61
|
+
|
|
62
|
+
let sumDev = 0;
|
|
63
|
+
for (const p of points) {
|
|
64
|
+
const expected = line.slope * p.idx + line.intercept;
|
|
65
|
+
const dev = Math.abs(p.price - expected) / Math.max(1e-12, p.price);
|
|
66
|
+
sumDev += dev;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const avgDev = sumDev / points.length;
|
|
70
|
+
return Math.max(0, Math.min(1, 1 - avgDev));
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* R2(決定係数)付きの線形回帰(ウェッジ用)
|
|
75
|
+
*/
|
|
76
|
+
export function linearRegressionWithR2(points: XYPoint[]): LinearRegressionWithR2Result {
|
|
77
|
+
const n = points.length;
|
|
78
|
+
if (n < 2) {
|
|
79
|
+
return {
|
|
80
|
+
slope: 0,
|
|
81
|
+
intercept: 0,
|
|
82
|
+
r2: 0,
|
|
83
|
+
valueAt: (_x: number) => 0,
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
let sumX = 0,
|
|
88
|
+
sumY = 0,
|
|
89
|
+
sumXY = 0,
|
|
90
|
+
sumX2 = 0;
|
|
91
|
+
for (const p of points) {
|
|
92
|
+
sumX += p.x;
|
|
93
|
+
sumY += p.y;
|
|
94
|
+
sumXY += p.x * p.y;
|
|
95
|
+
sumX2 += p.x * p.x;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
const denom = n * sumX2 - sumX * sumX || 1;
|
|
99
|
+
const slope = (n * sumXY - sumX * sumY) / denom;
|
|
100
|
+
const intercept = (sumY - slope * sumX) / n;
|
|
101
|
+
|
|
102
|
+
// R^2
|
|
103
|
+
const meanY = sumY / n;
|
|
104
|
+
let ssTot = 0,
|
|
105
|
+
ssRes = 0;
|
|
106
|
+
for (const p of points) {
|
|
107
|
+
const yHat = slope * p.x + intercept;
|
|
108
|
+
ssTot += (p.y - meanY) * (p.y - meanY);
|
|
109
|
+
ssRes += (p.y - yHat) * (p.y - yHat);
|
|
110
|
+
}
|
|
111
|
+
const r2 = ssTot <= 0 ? 0 : Math.max(0, Math.min(1, 1 - ssRes / ssTot));
|
|
112
|
+
|
|
113
|
+
const valueAt = (x: number) => slope * x + intercept;
|
|
114
|
+
|
|
115
|
+
return { slope, intercept, r2, valueAt };
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// --- ヘルパー関数 ---
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* 2つの値が許容誤差内で近いかどうかを判定
|
|
122
|
+
*/
|
|
123
|
+
export function near(a: number, b: number, tolerancePct: number): boolean {
|
|
124
|
+
return Math.abs(a - b) <= Math.max(a, b) * tolerancePct;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* パーセント変化を計算
|
|
129
|
+
*/
|
|
130
|
+
export function pct(a: number, b: number): number {
|
|
131
|
+
return (b - a) / (a === 0 ? 1 : a);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* 0-1にクランプ
|
|
136
|
+
*/
|
|
137
|
+
export function clamp01(x: number): number {
|
|
138
|
+
return Math.max(0, Math.min(1, x));
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* 相対偏差を計算
|
|
143
|
+
*/
|
|
144
|
+
export function relDev(a: number, b: number): number {
|
|
145
|
+
return Math.abs(a - b) / Math.max(1, Math.max(a, b));
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* 相対偏差からマージンを計算
|
|
150
|
+
*/
|
|
151
|
+
export function marginFromRelDev(rd: number, tol: number): number {
|
|
152
|
+
return clamp01(1 - rd / Math.max(1e-12, tol));
|
|
153
|
+
}
|