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,579 @@
|
|
|
1
|
+
/** Create a number[] of given length filled with NaN. */
|
|
2
|
+
const nanArray = (len: number): number[] => Array.from<number>({ length: len }).fill(NaN);
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* lib/indicators.ts - テクニカル指標の共通計算モジュール
|
|
6
|
+
*
|
|
7
|
+
* 全指標の純粋な計算関数を提供。
|
|
8
|
+
* リアルタイム分析 (tools/analyze_indicators.ts) と
|
|
9
|
+
* バックテストエンジン (tools/trading_process/) の両方から使用される。
|
|
10
|
+
*
|
|
11
|
+
* 【共通仕様】
|
|
12
|
+
* - 入力: number[](古い順)
|
|
13
|
+
* - 出力: number[](データ不足の位置は NaN)
|
|
14
|
+
* - 丸め処理なし(呼び出し元で必要に応じて丸める)
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* 単純移動平均 (SMA)
|
|
19
|
+
*
|
|
20
|
+
* @param prices 価格配列(古い順)
|
|
21
|
+
* @param period 期間(正の整数)
|
|
22
|
+
* @returns SMA配列(先頭 period-1 個は NaN)
|
|
23
|
+
*/
|
|
24
|
+
export function sma(prices: number[], period: number): number[] {
|
|
25
|
+
if (period <= 0) {
|
|
26
|
+
throw new Error('SMA period must be positive');
|
|
27
|
+
}
|
|
28
|
+
if (prices.length < period) {
|
|
29
|
+
return nanArray(prices.length);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const result: number[] = nanArray(prices.length);
|
|
33
|
+
|
|
34
|
+
let sum = 0;
|
|
35
|
+
for (let i = 0; i < period; i++) {
|
|
36
|
+
sum += prices[i];
|
|
37
|
+
}
|
|
38
|
+
result[period - 1] = sum / period;
|
|
39
|
+
|
|
40
|
+
for (let i = period; i < prices.length; i++) {
|
|
41
|
+
sum = sum - prices[i - period] + prices[i];
|
|
42
|
+
result[i] = sum / period;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
return result;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* 指数移動平均 (EMA)
|
|
50
|
+
*
|
|
51
|
+
* 最初の EMA 値は period 区間の SMA をシードとして使用。
|
|
52
|
+
*
|
|
53
|
+
* @param prices 価格配列(古い順)
|
|
54
|
+
* @param period EMA 期間(2 以上)
|
|
55
|
+
* @returns EMA配列(先頭 period-1 個は NaN)
|
|
56
|
+
*/
|
|
57
|
+
export function ema(prices: number[], period: number): number[] {
|
|
58
|
+
const result: number[] = nanArray(prices.length);
|
|
59
|
+
|
|
60
|
+
if (prices.length < period || period < 1) {
|
|
61
|
+
return result;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// SMA をシードとする
|
|
65
|
+
let sum = 0;
|
|
66
|
+
for (let i = 0; i < period; i++) {
|
|
67
|
+
sum += prices[i];
|
|
68
|
+
}
|
|
69
|
+
result[period - 1] = sum / period;
|
|
70
|
+
|
|
71
|
+
const k = 2 / (period + 1);
|
|
72
|
+
for (let i = period; i < prices.length; i++) {
|
|
73
|
+
result[i] = prices[i] * k + result[i - 1] * (1 - k);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
return result;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* RSI (Relative Strength Index) — Wilder's Smoothing
|
|
81
|
+
*
|
|
82
|
+
* @param closes 終値配列(古い順)
|
|
83
|
+
* @param period RSI 期間(通常 14)
|
|
84
|
+
* @returns RSI配列(0–100、先頭 period 個は NaN)
|
|
85
|
+
*/
|
|
86
|
+
export function rsi(closes: number[], period: number): number[] {
|
|
87
|
+
const result: number[] = nanArray(closes.length);
|
|
88
|
+
|
|
89
|
+
if (closes.length < period + 1) {
|
|
90
|
+
return result;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// 価格変化
|
|
94
|
+
let avgGain = 0;
|
|
95
|
+
let avgLoss = 0;
|
|
96
|
+
for (let i = 1; i <= period; i++) {
|
|
97
|
+
const change = closes[i] - closes[i - 1];
|
|
98
|
+
if (change > 0) avgGain += change;
|
|
99
|
+
else avgLoss += Math.abs(change);
|
|
100
|
+
}
|
|
101
|
+
avgGain /= period;
|
|
102
|
+
avgLoss /= period;
|
|
103
|
+
|
|
104
|
+
// 最初の RSI
|
|
105
|
+
if (avgLoss === 0) {
|
|
106
|
+
result[period] = 100;
|
|
107
|
+
} else {
|
|
108
|
+
result[period] = 100 - 100 / (1 + avgGain / avgLoss);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// Wilder's Smoothing
|
|
112
|
+
for (let i = period + 1; i < closes.length; i++) {
|
|
113
|
+
const change = closes[i] - closes[i - 1];
|
|
114
|
+
const gain = change > 0 ? change : 0;
|
|
115
|
+
const loss = change < 0 ? Math.abs(change) : 0;
|
|
116
|
+
|
|
117
|
+
avgGain = (avgGain * (period - 1) + gain) / period;
|
|
118
|
+
avgLoss = (avgLoss * (period - 1) + loss) / period;
|
|
119
|
+
|
|
120
|
+
if (avgLoss === 0) {
|
|
121
|
+
result[i] = 100;
|
|
122
|
+
} else {
|
|
123
|
+
result[i] = 100 - 100 / (1 + avgGain / avgLoss);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
return result;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* NaN → null 変換 + オプショナル丸め。
|
|
132
|
+
* analyze_indicators.ts など NumericSeries を返す呼び出し元で使用。
|
|
133
|
+
*
|
|
134
|
+
* @param values number[](NaN を含む)
|
|
135
|
+
* @param decimals 小数桁数(省略時は丸めなし)
|
|
136
|
+
* @returns (number | null)[]
|
|
137
|
+
*/
|
|
138
|
+
export function toNumericSeries(values: number[], decimals?: number): (number | null)[] {
|
|
139
|
+
return values.map((v) => {
|
|
140
|
+
if (!Number.isFinite(v)) return null;
|
|
141
|
+
return decimals != null ? Number(v.toFixed(decimals)) : v;
|
|
142
|
+
});
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// ============================================================
|
|
146
|
+
// Bollinger Bands
|
|
147
|
+
// ============================================================
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* ボリンジャーバンド
|
|
151
|
+
*
|
|
152
|
+
* @param values 価格配列(古い順)
|
|
153
|
+
* @param period SMA 期間(デフォルト 20)
|
|
154
|
+
* @param stdDev 標準偏差倍率(デフォルト 2)
|
|
155
|
+
* @returns { upper, middle, lower } — 各 number[](先頭 period-1 個は NaN)
|
|
156
|
+
*/
|
|
157
|
+
export function bollingerBands(
|
|
158
|
+
values: number[],
|
|
159
|
+
period: number = 20,
|
|
160
|
+
stdDev: number = 2,
|
|
161
|
+
): { upper: number[]; middle: number[]; lower: number[] } {
|
|
162
|
+
const n = values.length;
|
|
163
|
+
const upper: number[] = nanArray(n);
|
|
164
|
+
const middle: number[] = nanArray(n);
|
|
165
|
+
const lower: number[] = nanArray(n);
|
|
166
|
+
|
|
167
|
+
if (n < period) return { upper, middle, lower };
|
|
168
|
+
|
|
169
|
+
// 最初の window の合計
|
|
170
|
+
let sum = 0;
|
|
171
|
+
for (let i = 0; i < period; i++) sum += values[i];
|
|
172
|
+
|
|
173
|
+
for (let i = period - 1; i < n; i++) {
|
|
174
|
+
if (i > period - 1) {
|
|
175
|
+
sum = sum - values[i - period] + values[i];
|
|
176
|
+
}
|
|
177
|
+
const mean = sum / period;
|
|
178
|
+
|
|
179
|
+
let sumSq = 0;
|
|
180
|
+
for (let j = i - period + 1; j <= i; j++) {
|
|
181
|
+
sumSq += (values[j] - mean) ** 2;
|
|
182
|
+
}
|
|
183
|
+
const std = Math.sqrt(sumSq / period);
|
|
184
|
+
|
|
185
|
+
middle[i] = mean;
|
|
186
|
+
upper[i] = mean + stdDev * std;
|
|
187
|
+
lower[i] = mean - stdDev * std;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
return { upper, middle, lower };
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
// ============================================================
|
|
194
|
+
// MACD
|
|
195
|
+
// ============================================================
|
|
196
|
+
|
|
197
|
+
/**
|
|
198
|
+
* MACD (Moving Average Convergence Divergence)
|
|
199
|
+
*
|
|
200
|
+
* @param values 価格配列(古い順)
|
|
201
|
+
* @param fast 短期 EMA 期間(デフォルト 12)
|
|
202
|
+
* @param slow 長期 EMA 期間(デフォルト 26)
|
|
203
|
+
* @param signal シグナル EMA 期間(デフォルト 9)
|
|
204
|
+
* @returns { line, signal, hist } — 各 number[](NaN で埋め)
|
|
205
|
+
*/
|
|
206
|
+
export function macd(
|
|
207
|
+
values: number[],
|
|
208
|
+
fast: number = 12,
|
|
209
|
+
slow: number = 26,
|
|
210
|
+
signal: number = 9,
|
|
211
|
+
): { line: number[]; signal: number[]; hist: number[] } {
|
|
212
|
+
const emaFast = ema(values, fast);
|
|
213
|
+
const emaSlow = ema(values, slow);
|
|
214
|
+
const n = values.length;
|
|
215
|
+
|
|
216
|
+
// MACD line = fast EMA - slow EMA
|
|
217
|
+
const line: number[] = nanArray(n);
|
|
218
|
+
for (let i = 0; i < n; i++) {
|
|
219
|
+
if (!Number.isNaN(emaFast[i]) && !Number.isNaN(emaSlow[i])) {
|
|
220
|
+
line[i] = emaFast[i] - emaSlow[i];
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
// Signal EMA — 有効な MACD 値のみでシードする
|
|
225
|
+
const validStart = line.findIndex((v) => !Number.isNaN(v));
|
|
226
|
+
const signalLine: number[] = nanArray(n);
|
|
227
|
+
|
|
228
|
+
if (validStart >= 0) {
|
|
229
|
+
const validLine = line.slice(validStart).filter((v) => !Number.isNaN(v));
|
|
230
|
+
const sigEma = ema(validLine, signal);
|
|
231
|
+
let idx = 0;
|
|
232
|
+
for (let i = validStart; i < n; i++) {
|
|
233
|
+
if (!Number.isNaN(line[i])) {
|
|
234
|
+
signalLine[i] = sigEma[idx++];
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
// Histogram = line - signal
|
|
240
|
+
const hist: number[] = nanArray(n);
|
|
241
|
+
for (let i = 0; i < n; i++) {
|
|
242
|
+
if (!Number.isNaN(line[i]) && !Number.isNaN(signalLine[i])) {
|
|
243
|
+
hist[i] = line[i] - signalLine[i];
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
return { line, signal: signalLine, hist };
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
// ============================================================
|
|
251
|
+
// Ichimoku Kinko Hyo
|
|
252
|
+
// ============================================================
|
|
253
|
+
|
|
254
|
+
/**
|
|
255
|
+
* 一目均衡表の時系列(全ライン)
|
|
256
|
+
*
|
|
257
|
+
* @returns { tenkan, kijun, spanA, spanB, chikou } — 各 number[](NaN 埋め)
|
|
258
|
+
*/
|
|
259
|
+
export function ichimokuSeries(
|
|
260
|
+
highs: number[],
|
|
261
|
+
lows: number[],
|
|
262
|
+
closes: number[],
|
|
263
|
+
): { tenkan: number[]; kijun: number[]; spanA: number[]; spanB: number[]; chikou: number[] } {
|
|
264
|
+
const n = highs.length;
|
|
265
|
+
const tenkan: number[] = nanArray(n);
|
|
266
|
+
const kijun: number[] = nanArray(n);
|
|
267
|
+
const spanA: number[] = nanArray(n);
|
|
268
|
+
const spanB: number[] = nanArray(n);
|
|
269
|
+
|
|
270
|
+
const tenkanP = 9;
|
|
271
|
+
const kijunP = 26;
|
|
272
|
+
const senkouBP = 52;
|
|
273
|
+
|
|
274
|
+
for (let i = 0; i < n; i++) {
|
|
275
|
+
if (i >= tenkanP - 1) {
|
|
276
|
+
const hSlice = highs.slice(i - tenkanP + 1, i + 1);
|
|
277
|
+
const lSlice = lows.slice(i - tenkanP + 1, i + 1);
|
|
278
|
+
tenkan[i] = (Math.max(...hSlice) + Math.min(...lSlice)) / 2;
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
if (i >= kijunP - 1) {
|
|
282
|
+
const hSlice = highs.slice(i - kijunP + 1, i + 1);
|
|
283
|
+
const lSlice = lows.slice(i - kijunP + 1, i + 1);
|
|
284
|
+
kijun[i] = (Math.max(...hSlice) + Math.min(...lSlice)) / 2;
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
if (!Number.isNaN(tenkan[i]) && !Number.isNaN(kijun[i])) {
|
|
288
|
+
spanA[i] = (tenkan[i] + kijun[i]) / 2;
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
if (i >= senkouBP - 1) {
|
|
292
|
+
const hSlice = highs.slice(i - senkouBP + 1, i + 1);
|
|
293
|
+
const lSlice = lows.slice(i - senkouBP + 1, i + 1);
|
|
294
|
+
spanB[i] = (Math.max(...hSlice) + Math.min(...lSlice)) / 2;
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
// chikou は終値そのまま(遅行スパンの位置シフトは呼び出し元で行う)
|
|
299
|
+
const chikou = closes.slice();
|
|
300
|
+
|
|
301
|
+
return { tenkan, kijun, spanA, spanB, chikou };
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
/**
|
|
305
|
+
* 遅行スパン(chikou)を 26 本過去方向にシフトした系列を返す。
|
|
306
|
+
*
|
|
307
|
+
* シフト後の系列長は元の系列長と同一。
|
|
308
|
+
* 末尾 `shift` 個は NaN(未来に対応するデータがないため)。
|
|
309
|
+
*
|
|
310
|
+
* @param chikou 終値配列(= ichimokuSeries().chikou)
|
|
311
|
+
* @param shift シフト量(デフォルト 26)
|
|
312
|
+
* @returns シフト適用済みの number[](末尾 shift 個は NaN)
|
|
313
|
+
*/
|
|
314
|
+
export function shiftChikou(chikou: number[], shift: number = 26): number[] {
|
|
315
|
+
const n = chikou.length;
|
|
316
|
+
const result: number[] = nanArray(n);
|
|
317
|
+
for (let i = shift; i < n; i++) {
|
|
318
|
+
result[i - shift] = chikou[i];
|
|
319
|
+
}
|
|
320
|
+
return result;
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
/**
|
|
324
|
+
* 一目均衡表の最新スナップショット値
|
|
325
|
+
*
|
|
326
|
+
* @returns 最新の conversion/base/spanA/spanB、データ不足なら null
|
|
327
|
+
*/
|
|
328
|
+
export function ichimokuSnapshot(
|
|
329
|
+
highs: number[],
|
|
330
|
+
lows: number[],
|
|
331
|
+
_closes: number[],
|
|
332
|
+
): { conversion: number; base: number; spanA: number; spanB: number } | null {
|
|
333
|
+
if (highs.length < 52 || lows.length < 52) return null;
|
|
334
|
+
const conversion = (Math.max(...highs.slice(-9)) + Math.min(...lows.slice(-9))) / 2;
|
|
335
|
+
const base = (Math.max(...highs.slice(-26)) + Math.min(...lows.slice(-26))) / 2;
|
|
336
|
+
const spanA = (conversion + base) / 2;
|
|
337
|
+
const spanB = (Math.max(...highs.slice(-52)) + Math.min(...lows.slice(-52))) / 2;
|
|
338
|
+
return { conversion, base, spanA, spanB };
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
// ============================================================
|
|
342
|
+
// Classic Stochastic Oscillator
|
|
343
|
+
// ============================================================
|
|
344
|
+
|
|
345
|
+
/**
|
|
346
|
+
* クラシック・ストキャスティクス
|
|
347
|
+
*
|
|
348
|
+
* %K_raw = (Close - Low_n) / (High_n - Low_n) * 100
|
|
349
|
+
* %K = SMA(%K_raw, smoothK)
|
|
350
|
+
* %D = SMA(%K, smoothD)
|
|
351
|
+
*
|
|
352
|
+
* @returns { kSeries, dSeries } — 各 number[](NaN 埋め)
|
|
353
|
+
*/
|
|
354
|
+
export function stochastic(
|
|
355
|
+
highs: number[],
|
|
356
|
+
lows: number[],
|
|
357
|
+
closes: number[],
|
|
358
|
+
kPeriod: number = 14,
|
|
359
|
+
smoothK: number = 3,
|
|
360
|
+
smoothD: number = 3,
|
|
361
|
+
): { kSeries: number[]; dSeries: number[] } {
|
|
362
|
+
const n = Math.min(highs.length, lows.length, closes.length);
|
|
363
|
+
if (n < kPeriod + smoothK + smoothD - 2) {
|
|
364
|
+
return { kSeries: nanArray(n), dSeries: nanArray(n) };
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
// Raw %K
|
|
368
|
+
const rawK: number[] = nanArray(n);
|
|
369
|
+
for (let i = kPeriod - 1; i < n; i++) {
|
|
370
|
+
let hi = -Infinity;
|
|
371
|
+
let lo = Infinity;
|
|
372
|
+
for (let j = i - kPeriod + 1; j <= i; j++) {
|
|
373
|
+
if (highs[j] > hi) hi = highs[j];
|
|
374
|
+
if (lows[j] < lo) lo = lows[j];
|
|
375
|
+
}
|
|
376
|
+
const range = hi - lo;
|
|
377
|
+
rawK[i] = range === 0 ? 50 : ((closes[i] - lo) / range) * 100;
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
// %K = SMA(rawK, smoothK) — 手動ウィンドウ平均(NaN スキップ)
|
|
381
|
+
const kSeries: number[] = nanArray(n);
|
|
382
|
+
for (let i = 0; i < n; i++) {
|
|
383
|
+
if (Number.isNaN(rawK[i])) continue;
|
|
384
|
+
let sum = 0;
|
|
385
|
+
let cnt = 0;
|
|
386
|
+
for (let j = i - smoothK + 1; j <= i; j++) {
|
|
387
|
+
if (j >= 0 && !Number.isNaN(rawK[j])) {
|
|
388
|
+
sum += rawK[j];
|
|
389
|
+
cnt++;
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
if (cnt === smoothK) kSeries[i] = sum / cnt;
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
// %D = SMA(%K, smoothD)
|
|
396
|
+
const dSeries: number[] = nanArray(n);
|
|
397
|
+
for (let i = 0; i < n; i++) {
|
|
398
|
+
if (Number.isNaN(kSeries[i])) continue;
|
|
399
|
+
let sum = 0;
|
|
400
|
+
let cnt = 0;
|
|
401
|
+
for (let j = i - smoothD + 1; j <= i; j++) {
|
|
402
|
+
if (j >= 0 && !Number.isNaN(kSeries[j])) {
|
|
403
|
+
sum += kSeries[j];
|
|
404
|
+
cnt++;
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
if (cnt === smoothD) dSeries[i] = sum / cnt;
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
return { kSeries, dSeries };
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
// ============================================================
|
|
414
|
+
// Stochastic RSI
|
|
415
|
+
// ============================================================
|
|
416
|
+
|
|
417
|
+
/**
|
|
418
|
+
* ストキャスティクス RSI
|
|
419
|
+
*
|
|
420
|
+
* RSI 値にストキャスティクス計算を適用。
|
|
421
|
+
*
|
|
422
|
+
* @returns { kSeries, dSeries } — 各 number[](NaN 埋め)
|
|
423
|
+
*/
|
|
424
|
+
export function stochRSI(
|
|
425
|
+
closes: number[],
|
|
426
|
+
rsiPeriod: number = 14,
|
|
427
|
+
stochPeriod: number = 14,
|
|
428
|
+
smoothK: number = 3,
|
|
429
|
+
smoothD: number = 3,
|
|
430
|
+
): { kSeries: number[]; dSeries: number[] } {
|
|
431
|
+
const rsiValues = rsi(closes, rsiPeriod);
|
|
432
|
+
const n = rsiValues.length;
|
|
433
|
+
|
|
434
|
+
const validCount = rsiValues.filter((v) => !Number.isNaN(v)).length;
|
|
435
|
+
if (validCount < stochPeriod + smoothK + smoothD) {
|
|
436
|
+
return { kSeries: nanArray(n), dSeries: nanArray(n) };
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
// Raw %K over RSI window
|
|
440
|
+
const rawK: number[] = nanArray(n);
|
|
441
|
+
for (let i = 0; i < n; i++) {
|
|
442
|
+
if (Number.isNaN(rsiValues[i]) || i < stochPeriod - 1) continue;
|
|
443
|
+
const window: number[] = [];
|
|
444
|
+
for (let j = i - stochPeriod + 1; j <= i; j++) {
|
|
445
|
+
if (!Number.isNaN(rsiValues[j])) window.push(rsiValues[j]);
|
|
446
|
+
}
|
|
447
|
+
if (window.length < stochPeriod) continue;
|
|
448
|
+
const lo = Math.min(...window);
|
|
449
|
+
const hi = Math.max(...window);
|
|
450
|
+
const range = hi - lo;
|
|
451
|
+
rawK[i] = range === 0 ? 50 : ((rsiValues[i] - lo) / range) * 100;
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
// Smooth rawK → %K
|
|
455
|
+
const kSeries: number[] = nanArray(n);
|
|
456
|
+
for (let i = 0; i < n; i++) {
|
|
457
|
+
if (Number.isNaN(rawK[i])) continue;
|
|
458
|
+
let sum = 0;
|
|
459
|
+
let cnt = 0;
|
|
460
|
+
for (let j = i - smoothK + 1; j <= i; j++) {
|
|
461
|
+
if (j >= 0 && !Number.isNaN(rawK[j])) {
|
|
462
|
+
sum += rawK[j];
|
|
463
|
+
cnt++;
|
|
464
|
+
}
|
|
465
|
+
}
|
|
466
|
+
if (cnt === smoothK) kSeries[i] = sum / cnt;
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
// %D = SMA(%K, smoothD)
|
|
470
|
+
const dSeries: number[] = nanArray(n);
|
|
471
|
+
for (let i = 0; i < n; i++) {
|
|
472
|
+
if (Number.isNaN(kSeries[i])) continue;
|
|
473
|
+
let sum = 0;
|
|
474
|
+
let cnt = 0;
|
|
475
|
+
for (let j = i - smoothD + 1; j <= i; j++) {
|
|
476
|
+
if (j >= 0 && !Number.isNaN(kSeries[j])) {
|
|
477
|
+
sum += kSeries[j];
|
|
478
|
+
cnt++;
|
|
479
|
+
}
|
|
480
|
+
}
|
|
481
|
+
if (cnt === smoothD) dSeries[i] = sum / cnt;
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
return { kSeries, dSeries };
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
// ============================================================
|
|
488
|
+
// True Range / ATR
|
|
489
|
+
// ============================================================
|
|
490
|
+
|
|
491
|
+
/**
|
|
492
|
+
* True Range 系列
|
|
493
|
+
*
|
|
494
|
+
* TR = max(high - low, |high - prevClose|, |low - prevClose|)
|
|
495
|
+
*
|
|
496
|
+
* @param highs 高値配列(古い順)
|
|
497
|
+
* @param lows 安値配列(古い順)
|
|
498
|
+
* @param closes 終値配列(古い順)
|
|
499
|
+
* @returns TR 配列(先頭は NaN — prevClose が存在しない)
|
|
500
|
+
*/
|
|
501
|
+
export function trueRange(highs: number[], lows: number[], closes: number[]): number[] {
|
|
502
|
+
const n = Math.min(highs.length, lows.length, closes.length);
|
|
503
|
+
if (n < 2) return nanArray(n);
|
|
504
|
+
|
|
505
|
+
const result: number[] = nanArray(n);
|
|
506
|
+
for (let i = 1; i < n; i++) {
|
|
507
|
+
const h = highs[i];
|
|
508
|
+
const l = lows[i];
|
|
509
|
+
const pc = closes[i - 1];
|
|
510
|
+
if (!Number.isFinite(h) || !Number.isFinite(l) || !Number.isFinite(pc)) continue;
|
|
511
|
+
result[i] = Math.max(h - l, Math.abs(h - pc), Math.abs(l - pc));
|
|
512
|
+
}
|
|
513
|
+
return result;
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
/**
|
|
517
|
+
* ATR (Average True Range) — TR の SMA
|
|
518
|
+
*
|
|
519
|
+
* @param highs 高値配列(古い順)
|
|
520
|
+
* @param lows 安値配列(古い順)
|
|
521
|
+
* @param closes 終値配列(古い順)
|
|
522
|
+
* @param period 期間(デフォルト 14)
|
|
523
|
+
* @returns ATR 配列(NaN 埋め、先頭 period 個は NaN)
|
|
524
|
+
*/
|
|
525
|
+
export function atr(highs: number[], lows: number[], closes: number[], period: number = 14): number[] {
|
|
526
|
+
const tr = trueRange(highs, lows, closes);
|
|
527
|
+
const n = tr.length;
|
|
528
|
+
const result: number[] = nanArray(n);
|
|
529
|
+
|
|
530
|
+
if (n < period + 1) return result;
|
|
531
|
+
|
|
532
|
+
// TR[0] は NaN なので有効な TR は index 1 から
|
|
533
|
+
// 最初の ATR = SMA of TR[1..period]
|
|
534
|
+
let sum = 0;
|
|
535
|
+
for (let i = 1; i <= period; i++) {
|
|
536
|
+
if (Number.isNaN(tr[i])) return result;
|
|
537
|
+
sum += tr[i];
|
|
538
|
+
}
|
|
539
|
+
result[period] = sum / period;
|
|
540
|
+
|
|
541
|
+
// 以降は SMA スライディングウィンドウ
|
|
542
|
+
for (let i = period + 1; i < n; i++) {
|
|
543
|
+
if (Number.isNaN(tr[i])) continue;
|
|
544
|
+
sum = sum - tr[i - period] + tr[i];
|
|
545
|
+
result[i] = sum / period;
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
return result;
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
// ============================================================
|
|
552
|
+
// OBV (On-Balance Volume)
|
|
553
|
+
// ============================================================
|
|
554
|
+
|
|
555
|
+
/**
|
|
556
|
+
* OBV(出来高累積指標)
|
|
557
|
+
*
|
|
558
|
+
* @param closes 終値配列(古い順)
|
|
559
|
+
* @param volumes 出来高配列(古い順)
|
|
560
|
+
* @returns OBV の累積配列(number[])
|
|
561
|
+
*/
|
|
562
|
+
export function obv(closes: number[], volumes: number[]): number[] {
|
|
563
|
+
const n = Math.min(closes.length, volumes.length);
|
|
564
|
+
if (n < 1) return [];
|
|
565
|
+
|
|
566
|
+
const result: number[] = [0];
|
|
567
|
+
for (let i = 1; i < n; i++) {
|
|
568
|
+
const prev = result[i - 1];
|
|
569
|
+
if (closes[i] > closes[i - 1]) {
|
|
570
|
+
result.push(prev + volumes[i]);
|
|
571
|
+
} else if (closes[i] < closes[i - 1]) {
|
|
572
|
+
result.push(prev - volumes[i]);
|
|
573
|
+
} else {
|
|
574
|
+
result.push(prev);
|
|
575
|
+
}
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
return result;
|
|
579
|
+
}
|
package/lib/logger.ts
ADDED
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
import fs from 'node:fs';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import { nowIso, today } from './datetime.js';
|
|
5
|
+
|
|
6
|
+
const LOG_DIR = process.env.LOG_DIR || './logs';
|
|
7
|
+
const LOG_LEVEL = (process.env.LOG_LEVEL || 'info').toLowerCase();
|
|
8
|
+
const LEVELS: Record<string, number> = { error: 0, warn: 1, info: 2, debug: 3 };
|
|
9
|
+
const THRESH = LEVELS[LOG_LEVEL] ?? LEVELS.info;
|
|
10
|
+
|
|
11
|
+
function ensureDir(dir: string) {
|
|
12
|
+
if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function writeJsonl(file: string, obj: unknown) {
|
|
16
|
+
ensureDir(path.dirname(file));
|
|
17
|
+
fs.appendFileSync(file, `${JSON.stringify(obj)}\n`);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
// ── チェーンハッシュ(取引操作ログ専用) ──
|
|
21
|
+
|
|
22
|
+
let lastTradeHash = '0'.repeat(64);
|
|
23
|
+
|
|
24
|
+
/** チェーンハッシュ付きで取引操作ログを書き込む */
|
|
25
|
+
function writeTradeJsonl(file: string, record: Record<string, unknown>) {
|
|
26
|
+
ensureDir(path.dirname(file));
|
|
27
|
+
const withChain = { ...record, _prevHash: lastTradeHash };
|
|
28
|
+
const json = JSON.stringify(withChain);
|
|
29
|
+
lastTradeHash = createHash('sha256').update(json).digest('hex');
|
|
30
|
+
const finalRecord = { ...withChain, _hash: lastTradeHash };
|
|
31
|
+
fs.appendFileSync(file, `${JSON.stringify(finalRecord)}\n`);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function log(level: 'error' | 'warn' | 'info' | 'debug', event: Record<string, unknown>): void {
|
|
35
|
+
if ((LEVELS[level] ?? 2) > THRESH) return;
|
|
36
|
+
const date = today('YYYY-MM-DD');
|
|
37
|
+
const file = path.join(LOG_DIR, `${date}.jsonl`);
|
|
38
|
+
const record = { ts: nowIso(), level, ...event } as const;
|
|
39
|
+
try {
|
|
40
|
+
writeJsonl(file, record);
|
|
41
|
+
} catch {
|
|
42
|
+
// best-effort: ignore log failures
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/** ログ出力前に機密フィールドをマスクする */
|
|
47
|
+
// 方針(残高・注文数量・価格):
|
|
48
|
+
// - SENSITIVE_KEYS は「認証・署名・確認トークン」系のみを対象とする。bitbank API キー/シークレット/
|
|
49
|
+
// confirmation_token のログ混入を防ぐのが主目的。
|
|
50
|
+
// - balance / amount / price / free_amount 等はマスクしていない。ツール入力(例: create_order の amount)
|
|
51
|
+
// は maskSensitiveFields 後も数値文字列として jsonl に残り得る。これはサーバー管理者が MCP 利用者の
|
|
52
|
+
// 操作監査・障害調査のためにローカルログを読む前提の設計であり、LOG_LEVEL=debug でも現状コードは
|
|
53
|
+
// `log('debug', …)` を呼ばない(閾値は THRESH のみ)。tool_run の result は ok/summary/meta のみで
|
|
54
|
+
// 残高や約定一覧の data 本体は載せない。
|
|
55
|
+
// - 取引操作の `logTradeAction` は amount/price を平文で含むが、改ざん検知用チェーンハッシュ付きの
|
|
56
|
+
// 監査ログであり、集計・統計目的とは別レーンとして許容している。
|
|
57
|
+
const SENSITIVE_KEYS = new Set(['confirmation_token', 'token', 'key', 'secret', 'apiKey', 'apiSecret']);
|
|
58
|
+
|
|
59
|
+
function maskSensitiveFields(obj: unknown): unknown {
|
|
60
|
+
if (obj == null || typeof obj !== 'object') return obj;
|
|
61
|
+
if (Array.isArray(obj)) return obj.map(maskSensitiveFields);
|
|
62
|
+
const masked: Record<string, unknown> = {};
|
|
63
|
+
for (const [key, value] of Object.entries(obj as Record<string, unknown>)) {
|
|
64
|
+
masked[key] = SENSITIVE_KEYS.has(key) && typeof value === 'string' ? '***' : maskSensitiveFields(value);
|
|
65
|
+
}
|
|
66
|
+
return masked;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export function logToolRun(args: { tool: string; input: unknown; result: unknown; ms: number }): void {
|
|
70
|
+
const { tool, input, result, ms } = args;
|
|
71
|
+
const r = result as Record<string, unknown> | null | undefined;
|
|
72
|
+
const safeData = {
|
|
73
|
+
ok: r?.ok,
|
|
74
|
+
summary: r?.summary,
|
|
75
|
+
meta: r?.meta,
|
|
76
|
+
};
|
|
77
|
+
log('info', { type: 'tool_run', tool, input: maskSensitiveFields(input), ms, result: safeData });
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export function logError(tool: string, err: unknown, input: unknown): void {
|
|
81
|
+
log('error', {
|
|
82
|
+
type: 'tool_error',
|
|
83
|
+
tool,
|
|
84
|
+
input: maskSensitiveFields(input),
|
|
85
|
+
error: (err instanceof Error ? err.message : undefined) || String(err),
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// ── 取引操作ログ(チェーンハッシュ付き) ──
|
|
90
|
+
|
|
91
|
+
export function logTradeAction(action: {
|
|
92
|
+
type: 'create_order' | 'cancel_order' | 'cancel_orders';
|
|
93
|
+
orderId?: number;
|
|
94
|
+
orderIds?: number[];
|
|
95
|
+
pair: string;
|
|
96
|
+
side?: string;
|
|
97
|
+
orderType?: string;
|
|
98
|
+
amount?: string;
|
|
99
|
+
price?: string | null;
|
|
100
|
+
triggerPrice?: string | null;
|
|
101
|
+
positionSide?: string | null;
|
|
102
|
+
status: string;
|
|
103
|
+
confirmed: boolean;
|
|
104
|
+
/** どの経路から実行されたかの監査用ラベル(create_order / cancel_order / cancel_orders。二重発注や意図しないキャンセルの原因特定に使う) */
|
|
105
|
+
route?: 'elicitation' | 'ui-button' | 'direct-text';
|
|
106
|
+
}) {
|
|
107
|
+
const date = today('YYYY-MM-DD');
|
|
108
|
+
const file = path.join(LOG_DIR, `${date}.jsonl`);
|
|
109
|
+
const record: Record<string, unknown> = {
|
|
110
|
+
ts: nowIso(),
|
|
111
|
+
level: 'info',
|
|
112
|
+
category: 'trade_action',
|
|
113
|
+
...action,
|
|
114
|
+
};
|
|
115
|
+
try {
|
|
116
|
+
writeTradeJsonl(file, record);
|
|
117
|
+
} catch {
|
|
118
|
+
// best-effort
|
|
119
|
+
}
|
|
120
|
+
}
|