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.
Files changed (147) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +388 -0
  3. package/assets/lightweight-charts.standalone.js +7 -0
  4. package/bin/bitbank-lab-mcp.js +20 -0
  5. package/lib/cache.ts +70 -0
  6. package/lib/candle-utils.ts +48 -0
  7. package/lib/candle-validate.ts +434 -0
  8. package/lib/conversions.ts +25 -0
  9. package/lib/datetime.ts +157 -0
  10. package/lib/depth-analysis.ts +51 -0
  11. package/lib/error.ts +15 -0
  12. package/lib/formatter.ts +296 -0
  13. package/lib/get-depth.ts +111 -0
  14. package/lib/http.ts +132 -0
  15. package/lib/indicator-config.ts +39 -0
  16. package/lib/indicator_buffer.ts +41 -0
  17. package/lib/indicators.ts +579 -0
  18. package/lib/logger.ts +120 -0
  19. package/lib/ma-snapshot-utils.ts +277 -0
  20. package/lib/math.ts +89 -0
  21. package/lib/pattern-diagrams.ts +562 -0
  22. package/lib/result.ts +104 -0
  23. package/lib/validate.ts +154 -0
  24. package/lib/volatility.ts +132 -0
  25. package/package.json +79 -0
  26. package/src/env.ts +4 -0
  27. package/src/handlers/analyzeCandlePatternsHandler.ts +383 -0
  28. package/src/handlers/analyzeFibonacciHandler.ts +54 -0
  29. package/src/handlers/analyzeIndicatorsHandler.ts +682 -0
  30. package/src/handlers/analyzeMarketSignalHandler.ts +272 -0
  31. package/src/handlers/analyzeMyPortfolioHandler.ts +800 -0
  32. package/src/handlers/detectPatternsHandler.ts +77 -0
  33. package/src/handlers/detectPatternsViewsHandler.ts +518 -0
  34. package/src/handlers/getTickersJpyHandler.ts +145 -0
  35. package/src/handlers/getVolatilityMetricsHandler.ts +234 -0
  36. package/src/handlers/portfolio/calc.ts +549 -0
  37. package/src/handlers/portfolio/fetch.ts +318 -0
  38. package/src/handlers/portfolio/types.ts +170 -0
  39. package/src/handlers/renderChartSvgHandler.ts +69 -0
  40. package/src/handlers/runBacktestHandler.ts +70 -0
  41. package/src/http.ts +107 -0
  42. package/src/private/auth.ts +104 -0
  43. package/src/private/client.ts +298 -0
  44. package/src/private/config.ts +25 -0
  45. package/src/private/confirmation.ts +185 -0
  46. package/src/private/schemas.ts +866 -0
  47. package/src/prompts.ts +2296 -0
  48. package/src/resources/app-resources.ts +79 -0
  49. package/src/schema/analysis.ts +942 -0
  50. package/src/schema/backtest.ts +100 -0
  51. package/src/schema/base.ts +88 -0
  52. package/src/schema/candle-validate.ts +135 -0
  53. package/src/schema/chart.ts +399 -0
  54. package/src/schema/index.ts +11 -0
  55. package/src/schema/indicators.ts +125 -0
  56. package/src/schema/market-data.ts +298 -0
  57. package/src/schema/patterns.ts +382 -0
  58. package/src/schema/types.ts +97 -0
  59. package/src/schemas.d.ts +37 -0
  60. package/src/schemas.ts +7 -0
  61. package/src/server.ts +405 -0
  62. package/src/tool-definition.ts +44 -0
  63. package/src/tool-registry.ts +174 -0
  64. package/src/types/express-shim.d.ts +9 -0
  65. package/src/types/schemas.generated.d.ts +23 -0
  66. package/tools/analyze_bb_snapshot.ts +385 -0
  67. package/tools/analyze_candle_patterns.ts +810 -0
  68. package/tools/analyze_currency_strength.ts +273 -0
  69. package/tools/analyze_ema_snapshot.ts +183 -0
  70. package/tools/analyze_fibonacci.ts +530 -0
  71. package/tools/analyze_ichimoku_snapshot.ts +606 -0
  72. package/tools/analyze_indicators.ts +691 -0
  73. package/tools/analyze_market_signal.ts +665 -0
  74. package/tools/analyze_mtf_fibonacci.ts +273 -0
  75. package/tools/analyze_mtf_sma.ts +175 -0
  76. package/tools/analyze_sma_snapshot.ts +146 -0
  77. package/tools/analyze_stoch_snapshot.ts +276 -0
  78. package/tools/analyze_support_resistance.ts +817 -0
  79. package/tools/analyze_volume_profile.ts +546 -0
  80. package/tools/chart/ichimoku-cloud.ts +113 -0
  81. package/tools/chart/render-depth.ts +139 -0
  82. package/tools/chart/render-sub-panels.ts +208 -0
  83. package/tools/chart/svg-utils.ts +102 -0
  84. package/tools/detect_macd_cross.ts +691 -0
  85. package/tools/detect_patterns.ts +424 -0
  86. package/tools/detect_whale_events.ts +181 -0
  87. package/tools/get_candles.ts +487 -0
  88. package/tools/get_flow_metrics.ts +596 -0
  89. package/tools/get_orderbook.ts +540 -0
  90. package/tools/get_ticker.ts +132 -0
  91. package/tools/get_tickers_jpy.ts +240 -0
  92. package/tools/get_transactions.ts +209 -0
  93. package/tools/get_volatility_metrics.ts +302 -0
  94. package/tools/patterns/aftermath.ts +212 -0
  95. package/tools/patterns/config.ts +151 -0
  96. package/tools/patterns/detect_doubles.ts +650 -0
  97. package/tools/patterns/detect_hs.ts +635 -0
  98. package/tools/patterns/detect_pennants.ts +373 -0
  99. package/tools/patterns/detect_triangles.ts +820 -0
  100. package/tools/patterns/detect_triples.ts +633 -0
  101. package/tools/patterns/detect_wedges.ts +1072 -0
  102. package/tools/patterns/helpers.ts +517 -0
  103. package/tools/patterns/index.ts +40 -0
  104. package/tools/patterns/regression.ts +153 -0
  105. package/tools/patterns/smoothing.ts +168 -0
  106. package/tools/patterns/swing.ts +91 -0
  107. package/tools/patterns/types.ts +193 -0
  108. package/tools/prepare_chart_data.ts +294 -0
  109. package/tools/prepare_depth_data.ts +189 -0
  110. package/tools/private/analyze_my_portfolio.ts +21 -0
  111. package/tools/private/cancel_order.ts +127 -0
  112. package/tools/private/cancel_orders.ts +121 -0
  113. package/tools/private/create_order.ts +236 -0
  114. package/tools/private/get_margin_positions.ts +134 -0
  115. package/tools/private/get_margin_status.ts +155 -0
  116. package/tools/private/get_margin_trade_history.ts +156 -0
  117. package/tools/private/get_my_assets.ts +207 -0
  118. package/tools/private/get_my_deposit_withdrawal.ts +500 -0
  119. package/tools/private/get_my_orders.ts +157 -0
  120. package/tools/private/get_my_trade_history.ts +229 -0
  121. package/tools/private/get_order.ts +95 -0
  122. package/tools/private/get_orders_info.ts +90 -0
  123. package/tools/private/preview_cancel_order.ts +172 -0
  124. package/tools/private/preview_cancel_orders.ts +137 -0
  125. package/tools/private/preview_order.ts +292 -0
  126. package/tools/render_candle_pattern_diagram.ts +389 -0
  127. package/tools/render_chart_svg.ts +799 -0
  128. package/tools/render_depth_svg.ts +274 -0
  129. package/tools/trading_process/index.ts +7 -0
  130. package/tools/trading_process/lib/backtest_engine.ts +252 -0
  131. package/tools/trading_process/lib/equity.ts +131 -0
  132. package/tools/trading_process/lib/fetch_candles.ts +181 -0
  133. package/tools/trading_process/lib/sma.ts +62 -0
  134. package/tools/trading_process/lib/strategies/bb_breakout.ts +141 -0
  135. package/tools/trading_process/lib/strategies/index.ts +52 -0
  136. package/tools/trading_process/lib/strategies/macd_cross.ts +256 -0
  137. package/tools/trading_process/lib/strategies/rsi.ts +133 -0
  138. package/tools/trading_process/lib/strategies/sma_cross.ts +214 -0
  139. package/tools/trading_process/lib/strategies/types.ts +118 -0
  140. package/tools/trading_process/lib/svg_to_png.ts +64 -0
  141. package/tools/trading_process/render_backtest_chart_generic.ts +729 -0
  142. package/tools/trading_process/run_backtest.ts +243 -0
  143. package/tools/trading_process/types.ts +85 -0
  144. package/tools/validate_candle_data.ts +260 -0
  145. package/tsconfig.json +17 -0
  146. package/ui/cancel-confirm/dist/cancel-confirm.html +99 -0
  147. package/ui/order-confirm/dist/order-confirm.html +99 -0
@@ -0,0 +1,434 @@
1
+ /**
2
+ * lib/candle-validate.ts — ローソク足データの品質検証ロジック
3
+ *
4
+ * Vol.01(データ取得とクレンジング)のバリデーション手法をベースに、
5
+ * OHLCV データの完全性・整合性・異常値を検出する純粋関数群。
6
+ *
7
+ * 【暗号資産ドメイン考慮】
8
+ * - ファットテール分布: σ閾値は株式より広め(Vol.01 の教え)
9
+ * - 出来高ゼロ: 「取引がなかった」= 正常(Vol.01 の補完戦略: 0埋め)
10
+ * - ペアティア: major/mid/minor で流動性・スプレッドが大きく異なる
11
+ *
12
+ * 【共通仕様】
13
+ * - 入力: CandleRow[](古い順、isoTime 付き)
14
+ * - 出力: 各検証結果オブジェクト
15
+ * - 副作用なし
16
+ */
17
+
18
+ import { dayjs } from './datetime.js';
19
+ import { avg, stddev } from './math.js';
20
+
21
+ /** 検証対象のローソク足行 */
22
+ export interface CandleRow {
23
+ open: number;
24
+ high: number;
25
+ low: number;
26
+ close: number;
27
+ volume?: number;
28
+ isoTime?: string | null;
29
+ }
30
+
31
+ // ── ペアティア分類 ──
32
+ // 参考: bitbank-cli-skills pair-classification.md
33
+ // 24h出来高シェア(JPY建て売買代金割合)に基づく分類
34
+
35
+ export type PairTier = 'major' | 'mid' | 'minor';
36
+
37
+ const MAJOR_PAIRS = new Set(['btc_jpy', 'xrp_jpy', 'eth_jpy']);
38
+
39
+ const MID_PAIRS = new Set([
40
+ 'doge_jpy',
41
+ 'sol_jpy',
42
+ 'ltc_jpy',
43
+ 'ada_jpy',
44
+ 'avax_jpy',
45
+ 'dot_jpy',
46
+ 'bnb_jpy',
47
+ 'sui_jpy',
48
+ 'link_jpy',
49
+ ]);
50
+
51
+ /**
52
+ * ペア名からティアを判定する。
53
+ * major: 出来高シェア10%以上(板が厚く、スプレッド狭い)
54
+ * mid: 出来高シェア1-10%(深夜帯は板が薄くなることがある)
55
+ * minor: 出来高シェア1%未満(出来高ゼロが散発、スプレッド広い、ヒゲが出やすい)
56
+ */
57
+ export function classifyPairTier(pair: string): PairTier {
58
+ const normalized = pair.toLowerCase();
59
+ if (MAJOR_PAIRS.has(normalized)) return 'major';
60
+ if (MID_PAIRS.has(normalized)) return 'mid';
61
+ return 'minor';
62
+ }
63
+
64
+ /**
65
+ * ティアに応じたデフォルト閾値を返す。
66
+ *
67
+ * Vol.01 の教え:
68
+ * - 暗号資産はファットテール → σ閾値は広めが妥当
69
+ * - 出来高ゼロ = 取引がなかった(正常)
70
+ *
71
+ * ペア分類の教え:
72
+ * - minor は出来高ゼロが日常的、スプレッド由来のヒゲも頻出
73
+ * - major は板が厚く、異常値はより信号として意味がある
74
+ */
75
+ export interface TierDefaults {
76
+ priceSigma: number;
77
+ volumeMultiplier: number;
78
+ /** 出来高ゼロを品質スコアで減点する重み (0=減点なし, 1=フル減点) */
79
+ zeroVolumePenaltyWeight: number;
80
+ }
81
+
82
+ export function getTierDefaults(tier: PairTier): TierDefaults {
83
+ switch (tier) {
84
+ case 'major':
85
+ return { priceSigma: 4, volumeMultiplier: 15, zeroVolumePenaltyWeight: 1 };
86
+ case 'mid':
87
+ return { priceSigma: 4, volumeMultiplier: 25, zeroVolumePenaltyWeight: 0.5 };
88
+ case 'minor':
89
+ return { priceSigma: 5, volumeMultiplier: 50, zeroVolumePenaltyWeight: 0 };
90
+ }
91
+ }
92
+
93
+ // ── 時間足ごとのインターバル(ミリ秒) ──
94
+
95
+ const INTERVAL_MS: Record<string, number> = {
96
+ '1min': 60_000,
97
+ '5min': 300_000,
98
+ '15min': 900_000,
99
+ '30min': 1_800_000,
100
+ '1hour': 3_600_000,
101
+ '4hour': 14_400_000,
102
+ '8hour': 28_800_000,
103
+ '12hour': 43_200_000,
104
+ '1day': 86_400_000,
105
+ };
106
+
107
+ // ── 1. 完全性チェック ──
108
+
109
+ export interface CompletenessResult {
110
+ expected: number;
111
+ actual: number;
112
+ missing: number;
113
+ missingTimestamps: string[];
114
+ ratio: number; // 0–1
115
+ }
116
+
117
+ /**
118
+ * タイムスタンプの歯抜けを検出する。
119
+ * Vol.01 の「expected_index との差分」パターン。
120
+ *
121
+ * 1week / 1month は間隔が不規則なのでスキップ(ratio: 1 を返す)。
122
+ */
123
+ export function checkCompleteness(candles: CandleRow[], candleType: string): CompletenessResult {
124
+ const intervalMs = INTERVAL_MS[candleType];
125
+
126
+ // 不規則間隔の時間足はスキップ
127
+ if (!intervalMs || candles.length < 2) {
128
+ return { expected: candles.length, actual: candles.length, missing: 0, missingTimestamps: [], ratio: 1 };
129
+ }
130
+
131
+ const timestamps = candles.map((c) => (c.isoTime ? dayjs(c.isoTime).valueOf() : NaN)).filter((t) => !Number.isNaN(t));
132
+ const uniqueTimestamps = [...new Set(timestamps)];
133
+
134
+ if (uniqueTimestamps.length < 2) {
135
+ return { expected: candles.length, actual: candles.length, missing: 0, missingTimestamps: [], ratio: 1 };
136
+ }
137
+
138
+ const first = uniqueTimestamps[0];
139
+ const last = uniqueTimestamps[uniqueTimestamps.length - 1];
140
+ const expected = Math.floor((last - first) / intervalMs) + 1;
141
+ const tsSet = new Set(uniqueTimestamps);
142
+
143
+ const missingTimestamps: string[] = [];
144
+ for (let t = first; t <= last; t += intervalMs) {
145
+ if (!tsSet.has(t)) {
146
+ missingTimestamps.push(dayjs(t).toISOString());
147
+ }
148
+ }
149
+
150
+ return {
151
+ expected,
152
+ actual: uniqueTimestamps.length,
153
+ missing: missingTimestamps.length,
154
+ missingTimestamps: missingTimestamps.slice(0, 50), // 上限50件
155
+ ratio: expected > 0 ? Math.min(uniqueTimestamps.length / expected, 1) : 1,
156
+ };
157
+ }
158
+
159
+ // ── 2. 重複チェック ──
160
+
161
+ export interface DuplicatesResult {
162
+ count: number;
163
+ timestamps: string[];
164
+ }
165
+
166
+ export function checkDuplicates(candles: CandleRow[]): DuplicatesResult {
167
+ const seen = new Set<string>();
168
+ const duplicates: string[] = [];
169
+
170
+ for (const c of candles) {
171
+ const key = c.isoTime ?? '';
172
+ if (!key) continue;
173
+ if (seen.has(key)) {
174
+ duplicates.push(key);
175
+ } else {
176
+ seen.add(key);
177
+ }
178
+ }
179
+
180
+ return { count: duplicates.length, timestamps: duplicates.slice(0, 50) };
181
+ }
182
+
183
+ // ── 3. OHLCV 整合性チェック ──
184
+
185
+ export interface IntegrityIssue {
186
+ index: number;
187
+ isoTime: string | null;
188
+ issues: string[];
189
+ }
190
+
191
+ export interface IntegrityResult {
192
+ totalChecked: number;
193
+ invalidCount: number;
194
+ issues: IntegrityIssue[];
195
+ }
196
+
197
+ /**
198
+ * OHLCV の論理整合性を検証。
199
+ * - high >= low
200
+ * - high >= max(open, close)
201
+ * - low <= min(open, close)
202
+ * - volume >= 0
203
+ */
204
+ export function checkIntegrity(candles: CandleRow[]): IntegrityResult {
205
+ const issues: IntegrityIssue[] = [];
206
+
207
+ for (let i = 0; i < candles.length; i++) {
208
+ const c = candles[i];
209
+ const rowIssues: string[] = [];
210
+
211
+ if (c.high < c.low) {
212
+ rowIssues.push(`high(${c.high}) < low(${c.low})`);
213
+ }
214
+ if (c.high < Math.max(c.open, c.close)) {
215
+ rowIssues.push(`high(${c.high}) < max(open,close)(${Math.max(c.open, c.close)})`);
216
+ }
217
+ if (c.low > Math.min(c.open, c.close)) {
218
+ rowIssues.push(`low(${c.low}) > min(open,close)(${Math.min(c.open, c.close)})`);
219
+ }
220
+ if (c.volume != null && c.volume < 0) {
221
+ rowIssues.push(`volume(${c.volume}) < 0`);
222
+ }
223
+
224
+ if (rowIssues.length > 0) {
225
+ issues.push({ index: i, isoTime: c.isoTime ?? null, issues: rowIssues });
226
+ }
227
+ }
228
+
229
+ return {
230
+ totalChecked: candles.length,
231
+ invalidCount: issues.length,
232
+ issues: issues.slice(0, 50),
233
+ };
234
+ }
235
+
236
+ // ── 4. 価格異常値検出 ──
237
+
238
+ export interface PriceAnomaly {
239
+ index: number;
240
+ isoTime: string | null;
241
+ returnPct: number;
242
+ sigma: number;
243
+ }
244
+
245
+ export interface PriceAnomalyResult {
246
+ totalBars: number;
247
+ anomalyCount: number;
248
+ anomalies: PriceAnomaly[];
249
+ stats: { mean: number; stddev: number; threshold: number } | null;
250
+ }
251
+
252
+ /**
253
+ * 前足比の変化率(pct_change)が ±N σ を超えるバーを検出。
254
+ * Vol.01 の `ret.describe()` + 外れ値検出パターン。
255
+ */
256
+ export function checkPriceAnomalies(candles: CandleRow[], sigmaThreshold: number): PriceAnomalyResult {
257
+ if (candles.length < 3) {
258
+ return { totalBars: candles.length, anomalyCount: 0, anomalies: [], stats: null };
259
+ }
260
+
261
+ // リターン(変化率)を計算
262
+ const returns: number[] = [];
263
+ for (let i = 1; i < candles.length; i++) {
264
+ const prev = candles[i - 1].close;
265
+ if (prev === 0) {
266
+ returns.push(0);
267
+ } else {
268
+ returns.push((candles[i].close - prev) / prev);
269
+ }
270
+ }
271
+
272
+ const mean = avg(returns) ?? 0;
273
+ const sd = stddev(returns);
274
+
275
+ if (sd === 0) {
276
+ return {
277
+ totalBars: candles.length,
278
+ anomalyCount: 0,
279
+ anomalies: [],
280
+ stats: { mean, stddev: sd, threshold: sigmaThreshold },
281
+ };
282
+ }
283
+
284
+ const anomalies: PriceAnomaly[] = [];
285
+ for (let i = 0; i < returns.length; i++) {
286
+ const sigma = Math.abs((returns[i] - mean) / sd);
287
+ if (sigma >= sigmaThreshold) {
288
+ anomalies.push({
289
+ index: i + 1, // candles のインデックス(returns は1個ずれる)
290
+ isoTime: candles[i + 1].isoTime ?? null,
291
+ returnPct: Number((returns[i] * 100).toFixed(4)),
292
+ sigma: Number(sigma.toFixed(2)),
293
+ });
294
+ }
295
+ }
296
+
297
+ return {
298
+ totalBars: candles.length,
299
+ anomalyCount: anomalies.length,
300
+ anomalies: anomalies.slice(0, 50),
301
+ stats: { mean: Number((mean * 100).toFixed(6)), stddev: Number((sd * 100).toFixed(6)), threshold: sigmaThreshold },
302
+ };
303
+ }
304
+
305
+ // ── 5. 出来高異常値検出 ──
306
+
307
+ export interface VolumeAnomaly {
308
+ index: number;
309
+ isoTime: string | null;
310
+ volume: number;
311
+ reason: 'zero' | 'spike';
312
+ multiplier?: number;
313
+ }
314
+
315
+ export interface VolumeAnomalyResult {
316
+ totalBars: number;
317
+ anomalyCount: number;
318
+ zeroCount: number;
319
+ spikeCount: number;
320
+ anomalies: VolumeAnomaly[];
321
+ stats: { avgVolume: number; threshold: number } | null;
322
+ }
323
+
324
+ /**
325
+ * 出来高ゼロ、または移動平均の N 倍超のスパイクを検出。
326
+ */
327
+ export function checkVolumeAnomalies(candles: CandleRow[], multiplierThreshold: number): VolumeAnomalyResult {
328
+ const volumes = candles.map((c) => c.volume ?? 0);
329
+
330
+ if (volumes.length === 0) {
331
+ return { totalBars: 0, anomalyCount: 0, zeroCount: 0, spikeCount: 0, anomalies: [], stats: null };
332
+ }
333
+
334
+ // スパイク検出のベースラインはゼロ出来高を除外して算出
335
+ // Vol.01: 出来高ゼロ = 「取引がなかった」なのでベースラインに含めるべきではない
336
+ const nonZeroVolumes = volumes.filter((v) => v > 0);
337
+ const avgVol = avg(nonZeroVolumes) ?? 0;
338
+ const anomalies: VolumeAnomaly[] = [];
339
+ let zeroCount = 0;
340
+ let spikeCount = 0;
341
+
342
+ for (let i = 0; i < candles.length; i++) {
343
+ const vol = volumes[i];
344
+
345
+ if (vol === 0) {
346
+ zeroCount++;
347
+ anomalies.push({ index: i, isoTime: candles[i].isoTime ?? null, volume: vol, reason: 'zero' });
348
+ } else if (avgVol > 0 && vol > avgVol * multiplierThreshold) {
349
+ spikeCount++;
350
+ anomalies.push({
351
+ index: i,
352
+ isoTime: candles[i].isoTime ?? null,
353
+ volume: vol,
354
+ reason: 'spike',
355
+ multiplier: Number((vol / avgVol).toFixed(2)),
356
+ });
357
+ }
358
+ }
359
+
360
+ return {
361
+ totalBars: candles.length,
362
+ anomalyCount: anomalies.length,
363
+ zeroCount,
364
+ spikeCount,
365
+ anomalies: anomalies.slice(0, 50),
366
+ stats: { avgVolume: Number(avgVol.toFixed(6)), threshold: multiplierThreshold },
367
+ };
368
+ }
369
+
370
+ // ── 6. 総合品質スコア ──
371
+
372
+ export interface QualityScore {
373
+ score: number; // 0–100
374
+ breakdown: {
375
+ completeness: number; // 0–30
376
+ integrity: number; // 0–25
377
+ priceStability: number; // 0–25
378
+ volumeHealth: number; // 0–20
379
+ };
380
+ grade: 'A' | 'B' | 'C' | 'D' | 'F';
381
+ }
382
+
383
+ /**
384
+ * 各チェック結果を総合して 0–100 の品質スコアを算出。
385
+ *
386
+ * @param zeroVolumePenaltyWeight 出来高ゼロの減点重み (0–1)。
387
+ * Vol.01: 出来高ゼロ = 「取引がなかった」= 正常な解釈。
388
+ * minor ペアでは日常的に発生するため 0(減点なし)、
389
+ * major ペアでは異常の可能性があるため 1(フル減点)。
390
+ */
391
+ export function computeQualityScore(
392
+ completeness: CompletenessResult,
393
+ integrity: IntegrityResult,
394
+ priceAnomalies: PriceAnomalyResult,
395
+ volumeAnomalies: VolumeAnomalyResult,
396
+ zeroVolumePenaltyWeight = 1,
397
+ ): QualityScore {
398
+ // 完全性(30点): 欠損率に応じて減点
399
+ const completenessScore = Math.round(completeness.ratio * 30);
400
+
401
+ // 整合性(25点): 不正レコード率に応じて減点
402
+ const integrityRatio = integrity.totalChecked > 0 ? 1 - integrity.invalidCount / integrity.totalChecked : 1;
403
+ const integrityScore = Math.round(integrityRatio * 25);
404
+
405
+ // 価格安定性(25点): 異常値率に応じて減点
406
+ const priceRatio =
407
+ priceAnomalies.totalBars > 1 ? 1 - Math.min(priceAnomalies.anomalyCount / (priceAnomalies.totalBars - 1), 1) : 1;
408
+ const priceScore = Math.round(priceRatio * 25);
409
+
410
+ // 出来高健全性(20点): ゼロ率(重み付き)とスパイク率で減点
411
+ const zeroRatio = volumeAnomalies.totalBars > 0 ? volumeAnomalies.zeroCount / volumeAnomalies.totalBars : 0;
412
+ const spikeRatio = volumeAnomalies.totalBars > 0 ? volumeAnomalies.spikeCount / volumeAnomalies.totalBars : 0;
413
+ const volumeScore = Math.round((1 - Math.min(zeroRatio * zeroVolumePenaltyWeight + spikeRatio, 1)) * 20);
414
+
415
+ const score = completenessScore + integrityScore + priceScore + volumeScore;
416
+
417
+ let grade: QualityScore['grade'];
418
+ if (score >= 90) grade = 'A';
419
+ else if (score >= 75) grade = 'B';
420
+ else if (score >= 60) grade = 'C';
421
+ else if (score >= 40) grade = 'D';
422
+ else grade = 'F';
423
+
424
+ return {
425
+ score,
426
+ breakdown: {
427
+ completeness: completenessScore,
428
+ integrity: integrityScore,
429
+ priceStability: priceScore,
430
+ volumeHealth: volumeScore,
431
+ },
432
+ grade,
433
+ };
434
+ }
@@ -0,0 +1,25 @@
1
+ /**
2
+ * API レスポンスの文字列→数値変換ユーティリティ
3
+ *
4
+ * bitbank API は価格・数量等を文字列で返す。
5
+ * 各ツールで散在していた `d.x != null ? Number(d.x) : null` を統一する。
6
+ */
7
+
8
+ /**
9
+ * unknown 値を有限な number に変換する。
10
+ * null / undefined / NaN / Infinity は null を返す。
11
+ *
12
+ * @example
13
+ * toNum("12345.67") // 12345.67
14
+ * toNum(null) // null
15
+ * toNum(undefined) // null
16
+ * toNum("") // null (Number("") === 0 だが意図しない変換を防ぐ)
17
+ * toNum("abc") // null
18
+ * toNum(Infinity) // null
19
+ */
20
+ export function toNum(v: unknown): number | null {
21
+ if (v == null) return null;
22
+ if (typeof v === 'string' && v.trim() === '') return null;
23
+ const n = Number(v);
24
+ return Number.isFinite(n) ? n : null;
25
+ }
@@ -0,0 +1,157 @@
1
+ /**
2
+ * 日時変換ユーティリティ
3
+ * 各ツールで重複していた関数を統一
4
+ * dayjs ベースで実装
5
+ */
6
+
7
+ import dayjs from 'dayjs';
8
+ import customParseFormat from 'dayjs/plugin/customParseFormat.js';
9
+ import timezone from 'dayjs/plugin/timezone.js';
10
+ import utc from 'dayjs/plugin/utc.js';
11
+
12
+ // プラグイン有効化
13
+ dayjs.extend(utc);
14
+ dayjs.extend(timezone);
15
+ dayjs.extend(customParseFormat);
16
+
17
+ /**
18
+ * タイムスタンプをISO8601形式に変換
19
+ * @param ts タイムスタンプ(ミリ秒または秒、unknown型対応)
20
+ * @returns ISO8601文字列、無効な場合はnull
21
+ */
22
+ export function toIsoTime(ts: unknown): string | null {
23
+ const d = dayjs(Number(ts));
24
+ return d.isValid() ? d.toISOString() : null;
25
+ }
26
+
27
+ /**
28
+ * ミリ秒タイムスタンプをISO8601形式に変換(null安全版)
29
+ * @param ms ミリ秒タイムスタンプ
30
+ * @returns ISO8601文字列、無効な場合はnull
31
+ */
32
+ export function toIsoMs(ms: number | null): string | null {
33
+ if (ms == null) return null;
34
+ const d = dayjs(ms);
35
+ return d.isValid() ? d.toISOString() : null;
36
+ }
37
+
38
+ /**
39
+ * タイムスタンプをタイムゾーン付きISO風形式に変換
40
+ * @param ts ミリ秒タイムスタンプ
41
+ * @param tz タイムゾーン(例: 'Asia/Tokyo', 'UTC')
42
+ * @returns "2025-01-15T14:30:00" 形式、エラー時はnull
43
+ */
44
+ export function toIsoWithTz(ts: number, tz: string): string | null {
45
+ try {
46
+ const d = dayjs(ts).tz(tz);
47
+ return d.isValid() ? d.format('YYYY-MM-DDTHH:mm:ss') : null;
48
+ } catch {
49
+ return null;
50
+ }
51
+ }
52
+
53
+ /**
54
+ * タイムスタンプを日本語表示形式に変換
55
+ * @param ts ミリ秒タイムスタンプ(未指定時は現在時刻)
56
+ * @param tz タイムゾーン(デフォルト: 'Asia/Tokyo')
57
+ * @returns "2025/01/15 14:30:00 JST" 形式
58
+ */
59
+ export function toDisplayTime(ts: number | undefined, tz: string = 'Asia/Tokyo'): string | null {
60
+ try {
61
+ const d = dayjs(ts).tz(tz);
62
+ if (!d.isValid()) return null;
63
+ const tzShort = tz === 'UTC' ? 'UTC' : 'JST';
64
+ return `${d.format('YYYY/MM/DD HH:mm:ss')} ${tzShort}`;
65
+ } catch {
66
+ return null;
67
+ }
68
+ }
69
+
70
+ /**
71
+ * 現在時刻をISO8601形式で取得
72
+ * @returns ISO8601文字列
73
+ */
74
+ export function nowIso(): string {
75
+ return dayjs().toISOString();
76
+ }
77
+
78
+ /**
79
+ * 現在時刻を指定タイムゾーンで取得
80
+ * @param tz タイムゾーン(デフォルト: 'Asia/Tokyo')
81
+ * @returns dayjs インスタンス
82
+ */
83
+ export function nowTz(tz: string = 'Asia/Tokyo') {
84
+ return dayjs().tz(tz);
85
+ }
86
+
87
+ /**
88
+ * N日前の日付を取得
89
+ * @param daysAgo 何日前か
90
+ * @param format 出力フォーマット(デフォルト: 'YYYYMMDD')
91
+ * @returns フォーマットされた日付文字列
92
+ */
93
+ export function daysAgo(daysAgo: number, format: string = 'YYYYMMDD'): string {
94
+ return dayjs().subtract(daysAgo, 'day').format(format);
95
+ }
96
+
97
+ /**
98
+ * 今日の日付を取得
99
+ * @param format 出力フォーマット(デフォルト: 'YYYYMMDD')
100
+ * @returns フォーマットされた日付文字列
101
+ */
102
+ export function today(format: string = 'YYYYMMDD'): string {
103
+ return dayjs().format(format);
104
+ }
105
+
106
+ /**
107
+ * ISO8601 文字列を strict parse する。
108
+ * `2025-99-99` のような不正値を確実に弾く。
109
+ * @returns dayjs インスタンス(isValid() === true)、不正時は null
110
+ */
111
+ export function parseIso8601(value: string): dayjs.Dayjs | null {
112
+ if (!value) return null;
113
+
114
+ // タイムゾーン部分を分離して日時部分だけ strict parse する
115
+ // ISO8601: YYYY-MM-DD or YYYY-MM-DDTHH:mm:ss[.SSS][Z|±HH:mm|±HHmm]
116
+ const tzPattern = /([Zz]|[+-]\d{2}:\d{2}|[+-]\d{4})$/;
117
+ const tzMatch = value.match(tzPattern);
118
+
119
+ let dateTimePart = value;
120
+ if (tzMatch) {
121
+ dateTimePart = value.slice(0, -tzMatch[0].length);
122
+ }
123
+
124
+ // 日時部分のフォーマット候補
125
+ const formats = ['YYYY-MM-DDTHH:mm:ss.SSS', 'YYYY-MM-DDTHH:mm:ss', 'YYYY-MM-DD'];
126
+
127
+ for (const fmt of formats) {
128
+ const d = dayjs(dateTimePart, fmt, true); // strict = true
129
+ if (d.isValid()) {
130
+ // TZ 付きの場合は元の文字列から utc parse して正確な時刻を返す
131
+ if (tzMatch) {
132
+ const full = dayjs.utc(value);
133
+ return full.isValid() ? full : d;
134
+ }
135
+ return d;
136
+ }
137
+ }
138
+ return null;
139
+ }
140
+
141
+ /**
142
+ * ISO日付文字列を "M/D(曜日)" 形式に変換
143
+ * 例: "2026-04-09T00:00:00Z" → "4/9(木)"
144
+ * @param isoDate ISO8601 日付文字列
145
+ */
146
+ export function formatDateWithDayOfWeek(isoDate: string): string {
147
+ const days = ['日', '月', '火', '水', '木', '金', '土'];
148
+ const d = dayjs(isoDate).utc();
149
+ if (!d.isValid()) return 'n/a';
150
+ const m = d.month() + 1;
151
+ const day = d.date();
152
+ const dow = days[d.day()];
153
+ return `${m}/${day}(${dow})`;
154
+ }
155
+
156
+ // dayjs インスタンスを直接使いたい場合のエクスポート
157
+ export { dayjs };
@@ -0,0 +1,51 @@
1
+ /**
2
+ * 板データ(depth)の分析ユーティリティ
3
+ */
4
+
5
+ import { avg as mathAvg, stddev } from './math.js';
6
+
7
+ export type DepthZone = { low: number; high: number; label: string; color?: string };
8
+
9
+ /** 価格・サイズのペア */
10
+ export type PriceSize = readonly [number, number];
11
+
12
+ /** 累積 volume 階段データ。[price, cumulativeVolume] */
13
+ export type CumulativeStep = [number, number];
14
+
15
+ /**
16
+ * bids / asks の [price, size] 配列から累積 volume 階段データを生成する。
17
+ *
18
+ * - bids: 高価格 → 低価格 の降順(best bid から遠ざかる方向)
19
+ * - asks: 低価格 → 高価格 の昇順(best ask から遠ざかる方向)
20
+ */
21
+ export function buildCumulativeSteps(levels: ReadonlyArray<PriceSize>, side: 'bid' | 'ask'): CumulativeStep[] {
22
+ if (!levels.length) return [];
23
+ const sorted = [...levels].sort((a, b) => (side === 'bid' ? b[0] - a[0] : a[0] - b[0]));
24
+ const out: CumulativeStep[] = [];
25
+ let cum = 0;
26
+ for (const [p, s] of sorted) {
27
+ cum += s;
28
+ out.push([p, cum]);
29
+ }
30
+ return out;
31
+ }
32
+
33
+ /**
34
+ * ゾーン自動推定(簡易):レベル配列から平均+2σ超の価格帯を抽出
35
+ */
36
+ export function estimateZones(levels: ReadonlyArray<[number, number]>, side: 'bid' | 'ask'): DepthZone[] {
37
+ if (!levels.length) return [];
38
+ const qtys = levels.map(([, s]) => s);
39
+ const avg = mathAvg(qtys) ?? 0;
40
+ const stdev = stddev(qtys);
41
+ const thr = avg + stdev * 2;
42
+ const zones: DepthZone[] = [];
43
+ for (const [p, s] of levels) {
44
+ if (s >= thr) {
45
+ const pad = p * 0.001; // 0.1%幅
46
+ if (side === 'bid') zones.push({ low: p - pad, high: p + pad, label: 'bid wall', color: 'rgba(34,197,94,0.08)' });
47
+ else zones.push({ low: p - pad, high: p + pad, label: 'ask wall', color: 'rgba(249,115,22,0.08)' });
48
+ }
49
+ }
50
+ return zones.slice(0, 5); // 多すぎないように上位数本
51
+ }
package/lib/error.ts ADDED
@@ -0,0 +1,15 @@
1
+ /**
2
+ * unknown型のエラーからメッセージを安全に取得する
3
+ */
4
+ export function getErrorMessage(e: unknown): string {
5
+ if (e instanceof Error) return e.message;
6
+ if (typeof e === 'string') return e;
7
+ return String(e);
8
+ }
9
+
10
+ /**
11
+ * AbortErrorかどうかを判定する
12
+ */
13
+ export function isAbortError(e: unknown): boolean {
14
+ return e instanceof Error && e.name === 'AbortError';
15
+ }