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,373 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Flag detection — swing-point based with sliding pole scan.
|
|
3
|
+
*
|
|
4
|
+
* Pennant detection has been moved to detect_triangles.ts (Trendoscope 2-stage:
|
|
5
|
+
* triangle detection → pole check → reclassify as pennant).
|
|
6
|
+
*
|
|
7
|
+
* This module now handles flag patterns only:
|
|
8
|
+
* 1. Relaxed swing detection (swingDepth=1) for consolidation trendlines
|
|
9
|
+
* 2. Scan for impulsive moves (flagpoles) using ATR normalization
|
|
10
|
+
* 3. For each pole, examine subsequent swing points for consolidation
|
|
11
|
+
* 4. Fit R²-based regression trendlines on consolidation swing points
|
|
12
|
+
* 5. Classify as flag (roughly parallel channel, counter-trend to pole)
|
|
13
|
+
* 6. Detect breakout with ATR buffer
|
|
14
|
+
* 7. Apply deduplicatePatterns() before returning
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import { calcATR, deduplicatePatterns, finalizeConf } from './helpers.js';
|
|
18
|
+
import { clamp01 } from './regression.js';
|
|
19
|
+
import type { DetectContext, DetectResult, PatternEntry } from './types.js';
|
|
20
|
+
|
|
21
|
+
// ---------------------------------------------------------------------------
|
|
22
|
+
// 時間軸別パラメータ — 「日数」ベースで定義し、bars-per-day で変換
|
|
23
|
+
// ---------------------------------------------------------------------------
|
|
24
|
+
function barsPerDay(tf: string): number {
|
|
25
|
+
switch (tf) {
|
|
26
|
+
case '1min':
|
|
27
|
+
return 1440;
|
|
28
|
+
case '5min':
|
|
29
|
+
return 288;
|
|
30
|
+
case '15min':
|
|
31
|
+
return 96;
|
|
32
|
+
case '30min':
|
|
33
|
+
return 48;
|
|
34
|
+
case '1hour':
|
|
35
|
+
return 24;
|
|
36
|
+
case '4hour':
|
|
37
|
+
return 6;
|
|
38
|
+
case '8hour':
|
|
39
|
+
return 3;
|
|
40
|
+
case '12hour':
|
|
41
|
+
return 2;
|
|
42
|
+
case '1day':
|
|
43
|
+
return 1;
|
|
44
|
+
case '1week':
|
|
45
|
+
return 1 / 7;
|
|
46
|
+
case '1month':
|
|
47
|
+
return 1 / 30;
|
|
48
|
+
default:
|
|
49
|
+
return 1;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function getFlagParams(tf: string) {
|
|
54
|
+
const bpd = barsPerDay(tf);
|
|
55
|
+
|
|
56
|
+
// 旗竿: 1〜15日、保ち合い: 2〜30日(日数をバー数に変換)
|
|
57
|
+
const poleMinBars = Math.max(2, Math.round(1 * bpd));
|
|
58
|
+
const poleMaxBars = Math.max(5, Math.round(15 * bpd));
|
|
59
|
+
const consMinBars = Math.max(3, Math.round(2 * bpd));
|
|
60
|
+
const consMaxBars = Math.max(10, Math.round(30 * bpd));
|
|
61
|
+
|
|
62
|
+
// ATR 倍率・最小変化率は時間軸で微調整
|
|
63
|
+
const t = String(tf);
|
|
64
|
+
let minPoleATRMult = 1.5;
|
|
65
|
+
let minPolePct = 0.03;
|
|
66
|
+
if (t === '1day') {
|
|
67
|
+
minPoleATRMult = 2.0;
|
|
68
|
+
minPolePct = 0.05;
|
|
69
|
+
}
|
|
70
|
+
if (t === '1week') {
|
|
71
|
+
minPoleATRMult = 2.0;
|
|
72
|
+
minPolePct = 0.06;
|
|
73
|
+
}
|
|
74
|
+
if (t === '1month') {
|
|
75
|
+
minPoleATRMult = 2.5;
|
|
76
|
+
minPolePct = 0.08;
|
|
77
|
+
}
|
|
78
|
+
if (t === '1min' || t === '5min') {
|
|
79
|
+
minPolePct = 0.01;
|
|
80
|
+
}
|
|
81
|
+
if (t === '15min' || t === '30min') {
|
|
82
|
+
minPolePct = 0.015;
|
|
83
|
+
}
|
|
84
|
+
if (t === '1hour') {
|
|
85
|
+
minPolePct = 0.02;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
return { poleMinBars, poleMaxBars, minPoleATRMult, minPolePct, consMinBars, consMaxBars };
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export function detectPennantsFlags(ctx: DetectContext): DetectResult {
|
|
92
|
+
const { candles, want, includeForming, debugCandidates, lrWithR2 } = ctx;
|
|
93
|
+
const type = ctx.type;
|
|
94
|
+
let patterns: PatternEntry[] = [];
|
|
95
|
+
|
|
96
|
+
// This module now only handles flags. Pennants are detected via detect_triangles.ts.
|
|
97
|
+
const wantFlag = want.size === 0 || want.has('flag');
|
|
98
|
+
if (!wantFlag) return { patterns: [] };
|
|
99
|
+
|
|
100
|
+
const lastIdx = candles.length - 1;
|
|
101
|
+
if (lastIdx < 15) return { patterns: [] };
|
|
102
|
+
|
|
103
|
+
const params = getFlagParams(type);
|
|
104
|
+
|
|
105
|
+
// --- Relaxed swing detection (swingDepth=1) for consolidation zones ---
|
|
106
|
+
const relaxedPeaks: Array<{ idx: number; price: number }> = [];
|
|
107
|
+
const relaxedValleys: Array<{ idx: number; price: number }> = [];
|
|
108
|
+
for (let i = 1; i < candles.length - 1; i++) {
|
|
109
|
+
const c = candles[i],
|
|
110
|
+
prev = candles[i - 1],
|
|
111
|
+
next = candles[i + 1];
|
|
112
|
+
if (c.high > prev.high && c.high > next.high) {
|
|
113
|
+
relaxedPeaks.push({ idx: i, price: c.high });
|
|
114
|
+
}
|
|
115
|
+
if (c.low < prev.low && c.low < next.low) {
|
|
116
|
+
relaxedValleys.push({ idx: i, price: c.low });
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// --- Scan for flagpoles across entire history ---
|
|
121
|
+
const outerStep = params.poleMaxBars > 100 ? 2 : 1;
|
|
122
|
+
const innerStep = params.poleMaxBars > 50 ? 2 : 1;
|
|
123
|
+
|
|
124
|
+
for (let poleEnd = params.poleMinBars; poleEnd <= lastIdx - params.consMinBars; poleEnd += outerStep) {
|
|
125
|
+
// Find the strongest pole ending at this position
|
|
126
|
+
let bestPoleStart = -1;
|
|
127
|
+
let bestPoleMag = 0;
|
|
128
|
+
let bestPoleATRMult = 0;
|
|
129
|
+
|
|
130
|
+
for (let poleLen = params.poleMinBars; poleLen <= Math.min(params.poleMaxBars, poleEnd); poleLen += innerStep) {
|
|
131
|
+
const ps = poleEnd - poleLen;
|
|
132
|
+
const startPrice = candles[ps].close;
|
|
133
|
+
const endPrice = candles[poleEnd].close;
|
|
134
|
+
const magnitude = endPrice - startPrice;
|
|
135
|
+
const changePct = Math.abs(magnitude) / Math.max(1e-12, startPrice);
|
|
136
|
+
|
|
137
|
+
// Local ATR at pole
|
|
138
|
+
const localATR = calcATR(candles, Math.max(1, ps), poleEnd, 14);
|
|
139
|
+
if (localATR <= 0) continue;
|
|
140
|
+
|
|
141
|
+
const atrMult = Math.abs(magnitude) / localATR;
|
|
142
|
+
if (atrMult < params.minPoleATRMult || changePct < params.minPolePct) continue;
|
|
143
|
+
|
|
144
|
+
// Keep the strongest pole
|
|
145
|
+
if (atrMult > bestPoleATRMult) {
|
|
146
|
+
bestPoleStart = ps;
|
|
147
|
+
bestPoleMag = magnitude;
|
|
148
|
+
bestPoleATRMult = atrMult;
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
if (bestPoleStart < 0) continue;
|
|
153
|
+
|
|
154
|
+
const poleUp = bestPoleMag > 0;
|
|
155
|
+
const localATR = calcATR(candles, Math.max(1, bestPoleStart), poleEnd, 14);
|
|
156
|
+
if (localATR <= 0) continue;
|
|
157
|
+
|
|
158
|
+
// --- Check consolidation after pole ---
|
|
159
|
+
const consStart = poleEnd + 1;
|
|
160
|
+
if (consStart > lastIdx - 2) continue;
|
|
161
|
+
|
|
162
|
+
const consMaxEnd = Math.min(lastIdx, poleEnd + params.consMaxBars);
|
|
163
|
+
|
|
164
|
+
// Get swing points in consolidation zone
|
|
165
|
+
const consHighs = relaxedPeaks.filter((p) => p.idx >= consStart && p.idx <= consMaxEnd);
|
|
166
|
+
const consLows = relaxedValleys.filter((p) => p.idx >= consStart && p.idx <= consMaxEnd);
|
|
167
|
+
|
|
168
|
+
if (consHighs.length < 2 || consLows.length < 2) {
|
|
169
|
+
debugCandidates.push({
|
|
170
|
+
type: 'flag',
|
|
171
|
+
accepted: false,
|
|
172
|
+
reason: 'insufficient_consolidation_swings',
|
|
173
|
+
indices: [bestPoleStart, poleEnd],
|
|
174
|
+
details: { highs: consHighs.length, lows: consLows.length, poleATRMult: Number(bestPoleATRMult.toFixed(2)) },
|
|
175
|
+
});
|
|
176
|
+
continue;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
// --- Trendline span balance check ---
|
|
180
|
+
const upperSpan = consHighs[consHighs.length - 1].idx - consHighs[0].idx;
|
|
181
|
+
const lowerSpan = consLows[consLows.length - 1].idx - consLows[0].idx;
|
|
182
|
+
const actualConsEnd = Math.max(consHighs[consHighs.length - 1].idx, consLows[consLows.length - 1].idx);
|
|
183
|
+
const consZoneWidth = Math.max(1, actualConsEnd - consStart);
|
|
184
|
+
const minSpanRatio = 0.3;
|
|
185
|
+
|
|
186
|
+
if (upperSpan < consZoneWidth * minSpanRatio || lowerSpan < consZoneWidth * minSpanRatio) {
|
|
187
|
+
debugCandidates.push({
|
|
188
|
+
type: 'flag',
|
|
189
|
+
accepted: false,
|
|
190
|
+
reason: 'trendline_span_too_short',
|
|
191
|
+
indices: [bestPoleStart, poleEnd],
|
|
192
|
+
details: {
|
|
193
|
+
upperSpan,
|
|
194
|
+
lowerSpan,
|
|
195
|
+
consZoneWidth,
|
|
196
|
+
actualConsEnd,
|
|
197
|
+
upperRatio: Number((upperSpan / consZoneWidth).toFixed(3)),
|
|
198
|
+
lowerRatio: Number((lowerSpan / consZoneWidth).toFixed(3)),
|
|
199
|
+
minSpanRatio,
|
|
200
|
+
},
|
|
201
|
+
});
|
|
202
|
+
continue;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
// Fit trendlines with R²-based regression on swing points
|
|
206
|
+
const upperLine = lrWithR2(consHighs.map((p) => ({ x: p.idx, y: p.price })));
|
|
207
|
+
const lowerLine = lrWithR2(consLows.map((p) => ({ x: p.idx, y: p.price })));
|
|
208
|
+
|
|
209
|
+
const minR2 = 0.65; // 平行チャネルは線形性が命 — 0.25 では偽陽性が多すぎた
|
|
210
|
+
if (upperLine.r2 < minR2 || lowerLine.r2 < minR2) {
|
|
211
|
+
debugCandidates.push({
|
|
212
|
+
type: 'flag',
|
|
213
|
+
accepted: false,
|
|
214
|
+
reason: 'poor_trendline_fit',
|
|
215
|
+
indices: [bestPoleStart, consMaxEnd],
|
|
216
|
+
details: { r2Upper: Number(upperLine.r2.toFixed(3)), r2Lower: Number(lowerLine.r2.toFixed(3)), minR2 },
|
|
217
|
+
});
|
|
218
|
+
continue;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
// Consolidation geometry
|
|
222
|
+
const consEndIdx = actualConsEnd;
|
|
223
|
+
const gapStart = upperLine.valueAt(consStart) - lowerLine.valueAt(consStart);
|
|
224
|
+
const gapEnd = upperLine.valueAt(consEndIdx) - lowerLine.valueAt(consEndIdx);
|
|
225
|
+
|
|
226
|
+
if (gapStart <= 0 || gapEnd <= 0) continue;
|
|
227
|
+
|
|
228
|
+
const poleRange = Math.abs(bestPoleMag);
|
|
229
|
+
if (gapStart > poleRange * 0.9) {
|
|
230
|
+
debugCandidates.push({
|
|
231
|
+
type: 'flag',
|
|
232
|
+
accepted: false,
|
|
233
|
+
reason: 'consolidation_too_wide',
|
|
234
|
+
indices: [bestPoleStart, consEndIdx],
|
|
235
|
+
details: {
|
|
236
|
+
consRange: Number(gapStart.toFixed(2)),
|
|
237
|
+
poleRange: Number(poleRange.toFixed(2)),
|
|
238
|
+
ratio: Number((gapStart / poleRange).toFixed(3)),
|
|
239
|
+
},
|
|
240
|
+
});
|
|
241
|
+
continue;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
const convergenceRatio = gapEnd / gapStart;
|
|
245
|
+
|
|
246
|
+
// --- Classify: Flag only (parallel channel, counter-trend to pole) ---
|
|
247
|
+
const avgSlope = (upperLine.slope + lowerLine.slope) / 2;
|
|
248
|
+
const slopeDiff = Math.abs(upperLine.slope - lowerLine.slope);
|
|
249
|
+
const isParallel = slopeDiff < Math.abs(avgSlope) * 0.6 || convergenceRatio > 0.7;
|
|
250
|
+
const isAgainstPole = poleUp ? avgSlope < 0 : avgSlope > 0;
|
|
251
|
+
|
|
252
|
+
if (!(isParallel && isAgainstPole && convergenceRatio > 0.6)) {
|
|
253
|
+
debugCandidates.push({
|
|
254
|
+
type: 'flag',
|
|
255
|
+
accepted: false,
|
|
256
|
+
reason: 'classification_failed',
|
|
257
|
+
indices: [bestPoleStart, consEndIdx],
|
|
258
|
+
details: {
|
|
259
|
+
convergenceRatio: Number(convergenceRatio.toFixed(3)),
|
|
260
|
+
upperSlope: Number(upperLine.slope.toFixed(6)),
|
|
261
|
+
lowerSlope: Number(lowerLine.slope.toFixed(6)),
|
|
262
|
+
poleDirection: poleUp ? 'up' : 'down',
|
|
263
|
+
isParallel,
|
|
264
|
+
isAgainstPole,
|
|
265
|
+
},
|
|
266
|
+
});
|
|
267
|
+
continue;
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
// --- Breakout detection (close-based with ATR buffer) ---
|
|
271
|
+
let breakoutIdx = -1;
|
|
272
|
+
let breakoutDirection: 'up' | 'down' | null = null;
|
|
273
|
+
|
|
274
|
+
const scanStart = consStart + Math.max(3, Math.floor((consEndIdx - consStart) * 0.3));
|
|
275
|
+
for (let i = scanStart; i <= lastIdx; i++) {
|
|
276
|
+
const close = candles[i].close;
|
|
277
|
+
const uVal = upperLine.valueAt(i);
|
|
278
|
+
const lVal = lowerLine.valueAt(i);
|
|
279
|
+
|
|
280
|
+
if (close > uVal + localATR * 0.3) {
|
|
281
|
+
breakoutIdx = i;
|
|
282
|
+
breakoutDirection = 'up';
|
|
283
|
+
break;
|
|
284
|
+
}
|
|
285
|
+
if (close < lVal - localATR * 0.3) {
|
|
286
|
+
breakoutIdx = i;
|
|
287
|
+
breakoutDirection = 'down';
|
|
288
|
+
break;
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
// --- Status determination ---
|
|
293
|
+
const hasBreakout = breakoutIdx !== -1;
|
|
294
|
+
const patternEndIdx = hasBreakout ? breakoutIdx : consEndIdx;
|
|
295
|
+
const isExpectedBreakout =
|
|
296
|
+
hasBreakout && ((poleUp && breakoutDirection === 'up') || (!poleUp && breakoutDirection === 'down'));
|
|
297
|
+
|
|
298
|
+
let status: 'completed' | 'invalid' | 'forming' | 'near_completion';
|
|
299
|
+
if (hasBreakout) {
|
|
300
|
+
status = isExpectedBreakout ? 'completed' : 'invalid';
|
|
301
|
+
} else {
|
|
302
|
+
// Flag: duration-based completion estimate
|
|
303
|
+
const consBars = consEndIdx - consStart;
|
|
304
|
+
status = consBars > params.consMaxBars * 0.7 ? 'near_completion' : 'forming';
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
// Skip forming if not requested
|
|
308
|
+
if ((status === 'forming' || status === 'near_completion') && !includeForming) continue;
|
|
309
|
+
|
|
310
|
+
const startIso = candles[bestPoleStart]?.isoTime;
|
|
311
|
+
const endIso = candles[patternEndIdx]?.isoTime;
|
|
312
|
+
if (!startIso || !endIso) continue;
|
|
313
|
+
|
|
314
|
+
// --- Scoring ---
|
|
315
|
+
const poleScore = clamp01(bestPoleATRMult / (params.minPoleATRMult * 3));
|
|
316
|
+
const convScore = clamp01(1 - Math.abs(1 - convergenceRatio) / 0.5); // closer to 1.0 = better for flags
|
|
317
|
+
const fitScore = (upperLine.r2 + lowerLine.r2) / 2;
|
|
318
|
+
const touchScore = clamp01((consHighs.length + consLows.length) / 6);
|
|
319
|
+
|
|
320
|
+
const baseScore = poleScore * 0.3 + convScore * 0.25 + fitScore * 0.2 + touchScore * 0.25;
|
|
321
|
+
const confidence = finalizeConf(baseScore, 'flag');
|
|
322
|
+
|
|
323
|
+
const outcome = hasBreakout ? (isExpectedBreakout ? 'success' : 'failure') : undefined;
|
|
324
|
+
|
|
325
|
+
// --- ターゲット価格計算(flagpole_projection 方式) ---
|
|
326
|
+
let breakoutTarget: number | undefined;
|
|
327
|
+
let targetReachedPct: number | undefined;
|
|
328
|
+
if (hasBreakout && breakoutDirection) {
|
|
329
|
+
const bp = candles[breakoutIdx].close;
|
|
330
|
+
breakoutTarget = breakoutDirection === 'up' ? bp + poleRange : bp - poleRange;
|
|
331
|
+
breakoutTarget = Math.round(breakoutTarget);
|
|
332
|
+
const curPrice = Number(candles[lastIdx]?.close);
|
|
333
|
+
if (Number.isFinite(curPrice) && Math.abs(breakoutTarget - bp) > 1e-12) {
|
|
334
|
+
targetReachedPct = Math.round(((curPrice - bp) / (breakoutTarget - bp)) * 100);
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
patterns.push({
|
|
339
|
+
type: 'flag',
|
|
340
|
+
confidence,
|
|
341
|
+
range: { start: startIso, end: endIso },
|
|
342
|
+
status,
|
|
343
|
+
poleDirection: poleUp ? 'up' : 'down',
|
|
344
|
+
flagpoleHeight: Math.round(poleRange),
|
|
345
|
+
breakoutDirection: breakoutDirection ?? undefined,
|
|
346
|
+
outcome,
|
|
347
|
+
breakoutBarIndex: hasBreakout ? breakoutIdx : undefined,
|
|
348
|
+
...(breakoutTarget !== undefined ? { breakoutTarget, targetMethod: 'flagpole_projection' as const } : {}),
|
|
349
|
+
...(targetReachedPct !== undefined ? { targetReachedPct } : {}),
|
|
350
|
+
});
|
|
351
|
+
|
|
352
|
+
debugCandidates.push({
|
|
353
|
+
type: 'flag',
|
|
354
|
+
accepted: true,
|
|
355
|
+
reason: 'detected',
|
|
356
|
+
indices: [bestPoleStart, patternEndIdx],
|
|
357
|
+
details: {
|
|
358
|
+
poleATRMult: Number(bestPoleATRMult.toFixed(2)),
|
|
359
|
+
convergenceRatio: Number(convergenceRatio.toFixed(3)),
|
|
360
|
+
r2Upper: Number(upperLine.r2.toFixed(3)),
|
|
361
|
+
r2Lower: Number(lowerLine.r2.toFixed(3)),
|
|
362
|
+
touchCount: consHighs.length + consLows.length,
|
|
363
|
+
breakout: hasBreakout ? { idx: breakoutIdx, direction: breakoutDirection } : null,
|
|
364
|
+
status,
|
|
365
|
+
confidence,
|
|
366
|
+
},
|
|
367
|
+
});
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
patterns = deduplicatePatterns(patterns);
|
|
371
|
+
|
|
372
|
+
return { patterns };
|
|
373
|
+
}
|