diff --git a/app/analysis/bar_space.py b/app/analysis/bar_space.py index 892238e..22aa14a 100644 --- a/app/analysis/bar_space.py +++ b/app/analysis/bar_space.py @@ -39,33 +39,6 @@ def fill_short_gaps(times: list[int], tf: Timeframe) -> list[int]: return filled -def drop_times_in_compressed_gaps( - times: list[int], display_times: list[int], -) -> list[int]: - """Drop HTF opens that sit inside a 1m halt/weekend the chart compresses. - - Yahoo 30m/1h history can print through Saturday. Those timestamps add - index steps while 1m display is one slot, so a 30m line kinks at Sunday - 17:00. Short holes stay — they are already filled on both series. - """ - if len(display_times) < 2 or len(times) < 2: - return times - kept = [] - for time in times: - after = bisect_right(display_times, time) - before = after - 1 - if before < 0 or after >= len(display_times): - kept.append(time) - continue - left = display_times[before] - right = display_times[after] - gap = right - left - if gap > MAX_INTRADAY_GAP_SECONDS and left < time < right: - continue - kept.append(time) - return kept - - def index_at(times: list[int], t: int) -> float: """Fractional index of a timestamp within an ascending bar-time series.""" if not times: diff --git a/app/api/ws.py b/app/api/ws.py index 65358a2..8f70030 100644 --- a/app/api/ws.py +++ b/app/api/ws.py @@ -6,7 +6,7 @@ from fastapi import APIRouter, WebSocket, WebSocketDisconnect from app.api.deps import SESSION_COOKIE, session_matches, token_matches from app.analysis.levels import LevelKind from app.bars.models import Timeframe -from app.analysis.bar_space import drop_times_in_compressed_gaps, fill_short_gaps +from app.analysis.bar_space import fill_short_gaps from app.bars.session import bucket_duration, future_bucket_starts from app.analysis.confluence import cluster_levels @@ -72,10 +72,6 @@ def trendline_geometry(runtime) -> dict: def trendline_series(runtime, tf: Timeframe) -> dict: times = fill_short_gaps([bar.t for bar in runtime.store.get(tf)], tf) - if tf is not Timeframe.M1: - times = drop_times_in_compressed_gaps( - times, [bar.t for bar in runtime.store.get(Timeframe.M1)], - ) value = {"times": times} if tf is Timeframe.D1: value["durations"] = [bucket_duration(t, tf) for t in times] diff --git a/docs/implementation.md b/docs/implementation.md index 1936e84..cf994bc 100644 --- a/docs/implementation.md +++ b/docs/implementation.md @@ -1402,20 +1402,6 @@ The header quote paints the DOM directly; Vue's `price` flushes at 1 Hz. Schwab still prints many forming ticks per frame. Those now coalesce to one `candles.update` on the next animation frame. A new minute flushes immediately. -### 2026-08-31 — #63 kinked at 17:00: HTF source times inside a 1m halt - -Capture `c-FnHdmYtvsOJn` (deleted after inspect): 30m-looking line on 1m, corner -at 17:00 CT session open — steep through the last pre-halt candles, then flat. -Short 1–10 minute holes were already slotted and counted. This was the other -coordinate-system disagreement: Yahoo 30m/1h history can print through -settlement/Saturday. Those opens add source-index steps while 1m display -compresses the halt to one slot, so one screen column carries a weekend of -30m slope. - -`drop_times_in_compressed_gaps` strips HTF timestamps that sit strictly inside -a 1m gap longer than 30 minutes. Friday 15:30 and Sunday 17:00 become adjacent -again. Short holes and native 1m sampling are unchanged. - **Animate current price** was two infinite CSS animations across the full plot: `background-position` on a masked `repeating-linear-gradient`, plus a `color-mix` shine. That cannot run on the compositor. Turning it off dropped diff --git a/static/chart.js b/static/chart.js index 8d2f435..b428759 100644 --- a/static/chart.js +++ b/static/chart.js @@ -765,8 +765,6 @@ class ConfluenceChart { source.futureDurations = futureDurations.map(Number); source.allTimes = [...source.times, ...source.futureTimes]; source._filled = null; - source._stripped = null; - source._stripKey = null; return true; } @@ -2498,60 +2496,19 @@ class ConfluenceChart { sourceSeries(level) { if (this.trendlineGeometryMode !== 'source_tf') return null; const source = this.trendlineSourceSeries.get(level.tf); - if (!source) { - if (this.bars[0]?.tf !== level.tf) return null; - const duration = ConfluenceChart.tfSeconds[level.tf] || null; - return { - times: this.bars.map(bar => bar.t), - duration, - durations: duration == null - ? this.bars.map((bar, index, values) => values[index + 1]?.t - bar.t || 86400) - : [], - futureTimes: [], - futureDurations: [], - allTimes: this.bars.map(bar => bar.t), - }; - } - if (this.bars[0]?.tf === level.tf) return source; - return this.stripSourceCompressedGaps(source); - } - - stripSourceCompressedGaps(source) { - const bars = this.bars; - if (bars.length < 2) return source; - const key = `${bars[0].t}:${bars[bars.length - 1].t}:${bars.length}`; - if (source._stripKey === key && source._stripped) return source._stripped; - const times = ConfluenceChart.sourceTimes(source); - const max = ConfluenceChart.MAX_INTRADAY_GAP_SECONDS; - const kept = []; - for (const time of times) { - let lo = 0; - let hi = bars.length; - while (lo < hi) { - const mid = (lo + hi) >> 1; - if (bars[mid].t < time) lo = mid + 1; - else hi = mid; - } - const before = lo - 1; - if (before >= 0 && lo < bars.length) { - const left = bars[before].t; - const right = bars[lo].t; - if (right - left > max && left < time && time < right) continue; - } - kept.push(time); - } - const historical = kept.filter(time => time <= bars[bars.length - 1].t); - const future = kept.filter(time => time > bars[bars.length - 1].t); - const stripped = { - ...source, - times: historical, - futureTimes: future, - allTimes: kept, - _filled: kept, + if (source) return source; + if (this.bars[0]?.tf !== level.tf) return null; + const duration = ConfluenceChart.tfSeconds[level.tf] || null; + return { + times: this.bars.map(bar => bar.t), + duration, + durations: duration == null + ? this.bars.map((bar, index, values) => values[index + 1]?.t - bar.t || 86400) + : [], + futureTimes: [], + futureDurations: [], + allTimes: this.bars.map(bar => bar.t), }; - source._stripKey = key; - source._stripped = stripped; - return stripped; } static sourceDuration(source, index) { diff --git a/tests/e2e/trendline.test.mjs b/tests/e2e/trendline.test.mjs index a7f73e3..2a280d9 100644 --- a/tests/e2e/trendline.test.mjs +++ b/tests/e2e/trendline.test.mjs @@ -414,66 +414,6 @@ test('a 30m line stays straight on 1m when the bucket is missing its first minut }); }); -test('a 30m line stays straight on 1m when source times print through a halt', - { timeout: 180000 }, async () => { - await withChart(async page => { - const result = await page.evaluate(() => { - const c = window.__chart; - const minute = 60; - const half = 30 * minute; - const halt = 65 * minute; - const start = 800000; - const bars = []; - for (let t = start; t < start + 2 * half; t += minute) { - bars.push({ tf: '1m', t, o: 100, h: 101, l: 99, c: 100, v: 1, closed: true }); - } - const resume = start + 2 * half + halt; - for (let t = resume; t < resume + 2 * half; t += minute) { - bars.push({ tf: '1m', t, o: 100, h: 101, l: 99, c: 100, v: 1, closed: true }); - } - const sourceTimes = []; - for (let t = start; t <= resume + 2 * half; t += half) sourceTimes.push(t); - const line = { - id: 'halt-30m', kind: 'manual', tf: '30m', side: 'support', label: 'halt', - anchor_t: start, anchor_p: 100, last_t: start + half, - slope: 3 / half, cutoff_t: null, hidden: false, color: '#65b7cf', - line_width: 2, provisional: false, - }; - c.setDisplayFutureTimes(Array.from( - { length: ConfluenceChart.FUTURE_SLOTS }, (_, index) => bars.at(-1).t + (index + 1) * minute, - )); - c.setBars(bars); - c.setTrendlineGeometry({ - mode: 'source_tf', - series: { '30m': { times: sourceTimes, duration: half } }, - }); - c.syncLevels([line]); - const data = c.levelSeries.get(line.id).series.data(); - const point = time => { - const row = data.find(value => Number(value.time) === time); - return { - x: c.chart.timeScale().timeToCoordinate(time), - y: row ? c.candles.priceToCoordinate(row.value) : null, - }; - }; - const a = point(start); - const b = point(start + half); - const d = point(start + 5 * half); - const e = point(start + 6 * half); - return { - before: (b.y - a.y) / (b.x - a.x), - after: (e.y - d.y) / (e.x - d.x), - dropped: !ConfluenceChart.sourceTimes(c.sourceSeries(line)) - .includes(start + 2 * half), - }; - }); - assert.equal(result.dropped, true, 'halt 30m open remained in source index'); - assert.ok(Math.abs(result.before - result.after) < 1e-4, - `30m line kinked from ${result.before} to ${result.after}`); - assertNoPageErrors(page, assert); - }); - }); - test('a 1m line stays straight across a slotted mid-session hole', { timeout: 180000 }, async () => { await withChart(async page => { diff --git a/tests/test_bar_space.py b/tests/test_bar_space.py index c211cce..6b89939 100644 --- a/tests/test_bar_space.py +++ b/tests/test_bar_space.py @@ -1,6 +1,6 @@ from app.analysis.bar_space import ( - drop_times_in_compressed_gaps, fill_short_gaps, index_at, - price_in_bar_space, price_in_timeframe_space, timeframe_index_at, + fill_short_gaps, index_at, price_in_bar_space, price_in_timeframe_space, + timeframe_index_at, ) from app.analysis.levels import Level, LevelKind, Side from app.bars.models import Timeframe @@ -130,15 +130,6 @@ def test_a_weekend_is_not_filled_as_short_gaps(): assert fill_short_gaps(times, Timeframe.M1) == times -def test_weekend_30m_prints_inside_a_1m_halt_are_dropped(): - weekend = 49 * 3600 - display = [0, MINUTE, MINUTE + weekend, MINUTE + weekend + MINUTE] - source = [0, MINUTE, MINUTE + 30 * MINUTE, MINUTE + weekend] - assert drop_times_in_compressed_gaps(source, display) == [ - 0, MINUTE, MINUTE + weekend, - ] - - def test_a_missing_5m_bucket_inside_a_ten_minute_hole_is_filled(): five = 5 * MINUTE times = [0, five, 3 * five]