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,277 @@
|
|
|
1
|
+
import { today } from './datetime.js';
|
|
2
|
+
|
|
3
|
+
export type MaPrefix = 'SMA' | 'EMA';
|
|
4
|
+
|
|
5
|
+
/** Slope detection threshold (0.2% deadband) */
|
|
6
|
+
export const SLOPE_THRESHOLD = 0.002;
|
|
7
|
+
/** Lookback window for recent cross detection */
|
|
8
|
+
export const CROSS_LOOKBACK = 30;
|
|
9
|
+
|
|
10
|
+
// ── Shared interfaces ──
|
|
11
|
+
|
|
12
|
+
export interface MaLineEntry {
|
|
13
|
+
period: number;
|
|
14
|
+
value: number | null;
|
|
15
|
+
distancePct: number | null;
|
|
16
|
+
distanceAbs: number | null;
|
|
17
|
+
slope: 'rising' | 'falling' | 'flat';
|
|
18
|
+
slopePctPerBar: number | null;
|
|
19
|
+
pricePosition?: 'above' | 'below' | 'equal';
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export interface CrossStatus {
|
|
23
|
+
a: string;
|
|
24
|
+
b: string;
|
|
25
|
+
type: 'golden' | 'dead';
|
|
26
|
+
delta: number;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export interface RecentCrossEntry {
|
|
30
|
+
type: 'golden_cross' | 'dead_cross';
|
|
31
|
+
pair: [number, number];
|
|
32
|
+
barsAgo: number;
|
|
33
|
+
date: string;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export interface MaExtEntry {
|
|
37
|
+
value: number | null;
|
|
38
|
+
distancePct: number | null;
|
|
39
|
+
distanceAbs: number | null;
|
|
40
|
+
slope: 'rising' | 'falling' | 'flat';
|
|
41
|
+
slopePctPerBar: number | null;
|
|
42
|
+
slopePctTotal: number | null;
|
|
43
|
+
barsWindow: number | null;
|
|
44
|
+
slopePctPerDay?: number | null;
|
|
45
|
+
pricePosition?: 'above' | 'below' | 'equal';
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// ── Shared functions ──
|
|
49
|
+
|
|
50
|
+
export function slopeOfSeries(series: Array<number | null>): 'rising' | 'falling' | 'flat' {
|
|
51
|
+
const n = series.length;
|
|
52
|
+
if (n < 6) return 'flat';
|
|
53
|
+
let curIdx = n - 1;
|
|
54
|
+
while (curIdx >= 0 && series[curIdx] == null) curIdx--;
|
|
55
|
+
let prevIdx = curIdx - 5;
|
|
56
|
+
while (prevIdx >= 0 && series[prevIdx] == null) prevIdx--;
|
|
57
|
+
if (curIdx < 0 || prevIdx < 0) return 'flat';
|
|
58
|
+
const cur = series[curIdx] as number;
|
|
59
|
+
const prev = series[prevIdx] as number;
|
|
60
|
+
if (!Number.isFinite(cur) || !Number.isFinite(prev) || prev === 0) return 'flat';
|
|
61
|
+
const pct = (cur - prev) / Math.abs(prev);
|
|
62
|
+
if (pct > SLOPE_THRESHOLD) return 'rising';
|
|
63
|
+
if (pct < -SLOPE_THRESHOLD) return 'falling';
|
|
64
|
+
return 'flat';
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export function computeSlopeRates(series: Array<number | null>): {
|
|
68
|
+
pctTotal: number | null;
|
|
69
|
+
pctPerBar: number | null;
|
|
70
|
+
barsWindow: number | null;
|
|
71
|
+
} {
|
|
72
|
+
const n = series.length;
|
|
73
|
+
if (n < 6) return { pctTotal: null, pctPerBar: null, barsWindow: null };
|
|
74
|
+
let curIdx = n - 1;
|
|
75
|
+
while (curIdx >= 0 && series[curIdx] == null) curIdx--;
|
|
76
|
+
let prevIdx = curIdx - 5;
|
|
77
|
+
while (prevIdx >= 0 && series[prevIdx] == null) prevIdx--;
|
|
78
|
+
if (curIdx < 0 || prevIdx < 0) return { pctTotal: null, pctPerBar: null, barsWindow: null };
|
|
79
|
+
const cur = series[curIdx] as number;
|
|
80
|
+
const prev = series[prevIdx] as number;
|
|
81
|
+
const bars = curIdx - prevIdx;
|
|
82
|
+
if (!Number.isFinite(cur) || !Number.isFinite(prev) || prev === 0 || bars <= 0)
|
|
83
|
+
return { pctTotal: null, pctPerBar: null, barsWindow: null };
|
|
84
|
+
const pctTotal = ((cur - prev) / Math.abs(prev)) * 100;
|
|
85
|
+
const pctPerBar = pctTotal / bars;
|
|
86
|
+
return { pctTotal, pctPerBar, barsWindow: bars };
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export function generateCrossPairs(periods: number[]): Array<[number, number]> {
|
|
90
|
+
const uniquePeriods = [...new Set(periods)];
|
|
91
|
+
const pairs: Array<[number, number]> = [];
|
|
92
|
+
for (let i = 0; i < uniquePeriods.length; i++) {
|
|
93
|
+
for (let j = i + 1; j < uniquePeriods.length; j++) {
|
|
94
|
+
pairs.push([uniquePeriods[i], uniquePeriods[j]]);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
return pairs;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
export function detectCrossStatuses(
|
|
101
|
+
crossPairs: Array<[number, number]>,
|
|
102
|
+
map: Record<string, number | null>,
|
|
103
|
+
prefix: MaPrefix,
|
|
104
|
+
): CrossStatus[] {
|
|
105
|
+
const crosses: CrossStatus[] = [];
|
|
106
|
+
for (const [a, b] of crossPairs) {
|
|
107
|
+
const va = map[`${prefix}_${a}`];
|
|
108
|
+
const vb = map[`${prefix}_${b}`];
|
|
109
|
+
if (va != null && vb != null) {
|
|
110
|
+
const delta = (va as number) - (vb as number);
|
|
111
|
+
crosses.push({
|
|
112
|
+
a: `${prefix}_${a}`,
|
|
113
|
+
b: `${prefix}_${b}`,
|
|
114
|
+
type: delta >= 0 ? 'golden' : 'dead',
|
|
115
|
+
delta: Number(delta.toFixed(2)),
|
|
116
|
+
});
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
return crosses;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
export function detectRecentCrosses(
|
|
123
|
+
crossPairs: Array<[number, number]>,
|
|
124
|
+
chartInd: Record<string, unknown>,
|
|
125
|
+
candles: Array<{ isoTime?: string | null }>,
|
|
126
|
+
prefix: MaPrefix,
|
|
127
|
+
): RecentCrossEntry[] {
|
|
128
|
+
const recentCrosses: RecentCrossEntry[] = [];
|
|
129
|
+
for (const [a, b] of crossPairs) {
|
|
130
|
+
const sa: Array<number | null> = Array.isArray(chartInd?.[`${prefix}_${a}`])
|
|
131
|
+
? (chartInd[`${prefix}_${a}`] as Array<number | null>)
|
|
132
|
+
: [];
|
|
133
|
+
const sb: Array<number | null> = Array.isArray(chartInd?.[`${prefix}_${b}`])
|
|
134
|
+
? (chartInd[`${prefix}_${b}`] as Array<number | null>)
|
|
135
|
+
: [];
|
|
136
|
+
const n = Math.min(sa.length, sb.length, candles.length);
|
|
137
|
+
if (n < 2) continue;
|
|
138
|
+
const start = Math.max(1, n - CROSS_LOOKBACK);
|
|
139
|
+
for (let i = start; i < n; i++) {
|
|
140
|
+
const prevA = sa[i - 1];
|
|
141
|
+
const prevB = sb[i - 1];
|
|
142
|
+
const curA = sa[i];
|
|
143
|
+
const curB = sb[i];
|
|
144
|
+
if (prevA == null || prevB == null || curA == null || curB == null) continue;
|
|
145
|
+
const prev = prevA - prevB;
|
|
146
|
+
const curr = curA - curB;
|
|
147
|
+
if ((prev <= 0 && curr > 0) || (prev >= 0 && curr < 0)) {
|
|
148
|
+
const type = curr > 0 ? 'golden_cross' : 'dead_cross';
|
|
149
|
+
const barsAgo = n - 1 - i;
|
|
150
|
+
const date = String(candles[i]?.isoTime || '').slice(0, 10) || today('YYYY-MM-DD');
|
|
151
|
+
recentCrosses.push({ type, pair: [a, b], barsAgo, date });
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
return recentCrosses;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
export function detectAlignment(
|
|
159
|
+
sortedVals: (number | null)[],
|
|
160
|
+
opts: { minPeriods: number; strict: boolean },
|
|
161
|
+
): 'bullish' | 'bearish' | 'mixed' | 'unknown' {
|
|
162
|
+
if (sortedVals.length < opts.minPeriods) return 'unknown';
|
|
163
|
+
if (!sortedVals.every((v) => v != null)) return 'unknown';
|
|
164
|
+
const nums = sortedVals as number[];
|
|
165
|
+
const allDesc = opts.strict
|
|
166
|
+
? nums.every((v, i) => i === 0 || v < nums[i - 1])
|
|
167
|
+
: nums.every((v, i) => i === 0 || v <= nums[i - 1]);
|
|
168
|
+
const allAsc = opts.strict
|
|
169
|
+
? nums.every((v, i) => i === 0 || v > nums[i - 1])
|
|
170
|
+
: nums.every((v, i) => i === 0 || v >= nums[i - 1]);
|
|
171
|
+
if (allDesc) return 'bullish';
|
|
172
|
+
if (allAsc) return 'bearish';
|
|
173
|
+
return 'mixed';
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
export function detectPosition(
|
|
177
|
+
close: number | null,
|
|
178
|
+
maVals: number[],
|
|
179
|
+
): 'above_all' | 'below_all' | 'between' | 'unknown' {
|
|
180
|
+
if (close == null || maVals.length === 0) return 'unknown';
|
|
181
|
+
const minV = Math.min(...maVals);
|
|
182
|
+
const maxV = Math.max(...maVals);
|
|
183
|
+
if (close > maxV) return 'above_all';
|
|
184
|
+
if (close < minV) return 'below_all';
|
|
185
|
+
return 'between';
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
export function getSeries(chartInd: Record<string, unknown>, prefix: MaPrefix, period: number): Array<number | null> {
|
|
189
|
+
const key = `${prefix}_${period}`;
|
|
190
|
+
return Array.isArray(chartInd?.[key]) ? (chartInd[key] as Array<number | null>) : [];
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
export function computeMaExt(
|
|
194
|
+
close: number | null,
|
|
195
|
+
val: number | null,
|
|
196
|
+
series: Array<number | null>,
|
|
197
|
+
type: string,
|
|
198
|
+
): MaExtEntry {
|
|
199
|
+
const distancePct =
|
|
200
|
+
close != null && val != null && val !== 0 ? Number((((close - val) / val) * 100).toFixed(2)) : null;
|
|
201
|
+
const distanceAbs = close != null && val != null ? Number((close - val).toFixed(2)) : null;
|
|
202
|
+
const slope = slopeOfSeries(series);
|
|
203
|
+
const rates = computeSlopeRates(series);
|
|
204
|
+
const slopePctPerBar = rates.pctPerBar != null ? Number(rates.pctPerBar.toFixed(3)) : null;
|
|
205
|
+
const slopePctTotal = rates.pctTotal != null ? Number(rates.pctTotal.toFixed(2)) : null;
|
|
206
|
+
const barsWindow = rates.barsWindow;
|
|
207
|
+
const entry: MaExtEntry = { value: val, distancePct, distanceAbs, slope, slopePctPerBar, slopePctTotal, barsWindow };
|
|
208
|
+
if (type === '1day') entry.slopePctPerDay = slopePctPerBar;
|
|
209
|
+
if (close != null && val != null) entry.pricePosition = close > val ? 'above' : close < val ? 'below' : 'equal';
|
|
210
|
+
return entry;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
export function buildMaLines(periods: number[], maExt: Record<string, MaExtEntry>): MaLineEntry[] {
|
|
214
|
+
const topPeriods = Array.from(new Set(periods)).sort((a, b) => a - b);
|
|
215
|
+
return topPeriods.map((p) => {
|
|
216
|
+
const it = maExt[String(p)];
|
|
217
|
+
return {
|
|
218
|
+
period: p,
|
|
219
|
+
value: it?.value ?? null,
|
|
220
|
+
distancePct: it?.distancePct ?? null,
|
|
221
|
+
distanceAbs: it?.distanceAbs ?? null,
|
|
222
|
+
slope: it?.slope ?? 'flat',
|
|
223
|
+
slopePctPerBar: it?.slopePctPerBar ?? null,
|
|
224
|
+
pricePosition: it?.pricePosition,
|
|
225
|
+
};
|
|
226
|
+
});
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
export interface BuildMaSnapshotTextInput {
|
|
230
|
+
baseSummary: string;
|
|
231
|
+
type: string;
|
|
232
|
+
prefix: MaPrefix;
|
|
233
|
+
maLines: MaLineEntry[];
|
|
234
|
+
crossStatuses: CrossStatus[];
|
|
235
|
+
recentCrosses: RecentCrossEntry[];
|
|
236
|
+
footerLines: string[];
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
export function buildMaSnapshotText(input: BuildMaSnapshotTextInput): string {
|
|
240
|
+
const { baseSummary, type, prefix, maLines, crossStatuses, recentCrosses, footerLines } = input;
|
|
241
|
+
const distanceLines = maLines.map((it) => {
|
|
242
|
+
const valStr = it.value != null ? it.value : 'n/a';
|
|
243
|
+
const pctStr = it.distancePct != null ? `${it.distancePct >= 0 ? '+' : ''}${it.distancePct}%` : 'n/a';
|
|
244
|
+
const absStr =
|
|
245
|
+
it.distanceAbs != null
|
|
246
|
+
? `${it.distanceAbs >= 0 ? '+' : ''}${Number(it.distanceAbs).toLocaleString('ja-JP')}円`
|
|
247
|
+
: 'n/a';
|
|
248
|
+
const slopeRate =
|
|
249
|
+
it.slopePctPerBar != null
|
|
250
|
+
? `${it.slopePctPerBar >= 0 ? '+' : ''}${it.slopePctPerBar}%/${type === '1day' ? 'day' : 'bar'}`
|
|
251
|
+
: null;
|
|
252
|
+
const pos = it.pricePosition
|
|
253
|
+
? it.pricePosition === 'above'
|
|
254
|
+
? '(価格は上)'
|
|
255
|
+
: it.pricePosition === 'below'
|
|
256
|
+
? '(価格は下)'
|
|
257
|
+
: '(同水準)'
|
|
258
|
+
: '';
|
|
259
|
+
return `${prefix}(${it.period}): ${valStr} (${pctStr}, ${absStr}) slope=${it.slope}${slopeRate ? ` (${slopeRate})` : ''}${pos}`;
|
|
260
|
+
});
|
|
261
|
+
const crossStatusLines = crossStatuses.map((c) => `${c.a}/${c.b}: ${c.type} (delta:${c.delta})`);
|
|
262
|
+
const allRecentLines = recentCrosses.map(
|
|
263
|
+
(rc) => `${rc.type} ${rc.pair.join('/')} - ${rc.barsAgo} bars ago (${rc.date})`,
|
|
264
|
+
);
|
|
265
|
+
return [
|
|
266
|
+
baseSummary,
|
|
267
|
+
'',
|
|
268
|
+
...distanceLines,
|
|
269
|
+
...(crossStatusLines.length ? ['', 'Cross Status:', ...crossStatusLines] : []),
|
|
270
|
+
...(allRecentLines.length ? ['', 'Recent Crosses (all):', ...allRecentLines] : []),
|
|
271
|
+
'',
|
|
272
|
+
'---',
|
|
273
|
+
...footerLines,
|
|
274
|
+
]
|
|
275
|
+
.filter(Boolean)
|
|
276
|
+
.join('\n');
|
|
277
|
+
}
|
package/lib/math.ts
ADDED
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 数値演算ユーティリティ
|
|
3
|
+
* 各ツールで重複していた関数を統一
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
/** ゼロ除算ガード用イプシロン */
|
|
7
|
+
export const EPSILON = 1e-12;
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* 配列の平均値を計算
|
|
11
|
+
* @param arr 数値配列
|
|
12
|
+
* @returns 平均値、空配列の場合はnull
|
|
13
|
+
*/
|
|
14
|
+
export function avg(arr: number[]): number | null {
|
|
15
|
+
return arr.length ? arr.reduce((s, v) => s + v, 0) / arr.length : null;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* 配列の中央値を計算
|
|
20
|
+
* @param arr 数値配列
|
|
21
|
+
* @returns 中央値、空配列の場合はnull
|
|
22
|
+
*/
|
|
23
|
+
export function median(arr: number[]): number | null {
|
|
24
|
+
if (!arr.length) return null;
|
|
25
|
+
const sorted = [...arr].sort((a, b) => a - b);
|
|
26
|
+
const mid = Math.floor(sorted.length / 2);
|
|
27
|
+
return sorted.length % 2 ? sorted[mid] : (sorted[mid - 1] + sorted[mid]) / 2;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* 配列の標準偏差を計算
|
|
32
|
+
* @param values 数値配列
|
|
33
|
+
* @returns 標準偏差、空配列の場合は0
|
|
34
|
+
*/
|
|
35
|
+
export function stddev(values: number[]): number {
|
|
36
|
+
const n = values.length;
|
|
37
|
+
if (n === 0) return 0;
|
|
38
|
+
const mean = values.reduce((s, v) => s + v, 0) / n;
|
|
39
|
+
const variance = values.reduce((s, v) => s + (v - mean) * (v - mean), 0) / n;
|
|
40
|
+
return Math.sqrt(Math.max(0, variance));
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* スライディングウィンドウ平均(SMA)
|
|
45
|
+
* @param values 数値配列
|
|
46
|
+
* @param window ウィンドウサイズ(>= 1)
|
|
47
|
+
* @returns 各ウィンドウの平均値配列(長さ = values.length - window + 1)
|
|
48
|
+
*/
|
|
49
|
+
export function slidingMean(values: number[], window: number): number[] {
|
|
50
|
+
const out: number[] = [];
|
|
51
|
+
if (!Number.isFinite(window) || window <= 0) return out;
|
|
52
|
+
let sum = 0;
|
|
53
|
+
for (let i = 0; i < values.length; i++) {
|
|
54
|
+
sum += values[i];
|
|
55
|
+
if (i >= window) sum -= values[i - window];
|
|
56
|
+
if (i >= window - 1) out.push(sum / window);
|
|
57
|
+
}
|
|
58
|
+
return out;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* スライディングウィンドウ標準偏差
|
|
63
|
+
* @param values 数値配列
|
|
64
|
+
* @param window ウィンドウサイズ(>= 2)
|
|
65
|
+
* @returns 各ウィンドウの標準偏差配列(長さ = values.length - window + 1)
|
|
66
|
+
*/
|
|
67
|
+
export function slidingStddev(values: number[], window: number): number[] {
|
|
68
|
+
const out: number[] = [];
|
|
69
|
+
if (window <= 1) return out;
|
|
70
|
+
let sum = 0;
|
|
71
|
+
let sumsq = 0;
|
|
72
|
+
for (let i = 0; i < values.length; i++) {
|
|
73
|
+
const v = values[i];
|
|
74
|
+
sum += v;
|
|
75
|
+
sumsq += v * v;
|
|
76
|
+
if (i >= window) {
|
|
77
|
+
const old = values[i - window];
|
|
78
|
+
sum -= old;
|
|
79
|
+
sumsq -= old * old;
|
|
80
|
+
}
|
|
81
|
+
if (i >= window - 1) {
|
|
82
|
+
const n = window;
|
|
83
|
+
const mean = sum / n;
|
|
84
|
+
const variance = Math.max(0, sumsq / n - mean * mean);
|
|
85
|
+
out.push(Math.sqrt(variance));
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
return out;
|
|
89
|
+
}
|