From 5ee32b327c825a11401a995d752b88fb05c5962f Mon Sep 17 00:00:00 2001 From: Chris Amow Date: Sat, 15 Aug 2026 04:22:40 -0500 Subject: [PATCH] transmite full 1m data --- AGENTS.md | 4 ++++ app/api/routes.py | 7 ++++++- app/api/ws.py | 2 +- app/bars/store.py | 3 ++- app/config.py | 4 +++- docs/NEXT_STEPS.md | 14 +++++++++++++- docs/implementation.md | 9 +++++++++ tests/test_ws_preferences.py | 8 ++++++++ 8 files changed, 46 insertions(+), 5 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index 9bbbb5d..64248da 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -169,3 +169,7 @@ markers. price is sitting on. There is no durable state yet. - Times are epoch seconds, UTC, everywhere. Only the display is localised — never shift the stored values. +- **Do not hardcode how many bars a client gets.** A leftover `1000` on the + snapshot made 1m look empty past ~1am while the store held 5,000. The + store cap is `max_bars_per_tf`. The next step is a visible-window fetch, + not another silent number. diff --git a/app/api/routes.py b/app/api/routes.py index 99328c5..2b2461a 100644 --- a/app/api/routes.py +++ b/app/api/routes.py @@ -12,6 +12,7 @@ from fastapi import APIRouter, Depends, HTTPException, Query, Request, Response from pydantic import BaseModel, Field from app.bars.models import Timeframe +from app.config import DEFAULT_MAX_BARS_PER_TF from app.analysis.levels import Side from app.analysis.manual_lines import ManualLine from app.api.deps import require_token @@ -141,7 +142,11 @@ def status(request: Request): @router.get("/bars") -def bars(request: Request, tf: str = "1m", limit: int = Query(500, ge=1, le=5000)): +def bars( + request: Request, + tf: str = "1m", + limit: int | None = Query(None, ge=1, le=DEFAULT_MAX_BARS_PER_TF), +): try: timeframe = Timeframe(tf) except ValueError as exc: diff --git a/app/api/ws.py b/app/api/ws.py index 0b2b1b7..a52df4c 100644 --- a/app/api/ws.py +++ b/app/api/ws.py @@ -86,7 +86,7 @@ def snapshot(runtime, tf: Timeframe, prefs: dict | None = None) -> dict: return { "type": "snapshot", "tf": tf.value, - "bars": [bar.to_dict() for bar in runtime.store.get(tf, 1000)], + "bars": [bar.to_dict() for bar in runtime.store.get(tf)], "levels": [level.to_dict() for level in runtime.levels], "clusters": [cluster.to_dict() for cluster in connection_clusters(runtime, prefs)], "price": runtime.store.get(Timeframe.M1, 1)[-1].c diff --git a/app/bars/store.py b/app/bars/store.py index 97005cd..d9e34f9 100644 --- a/app/bars/store.py +++ b/app/bars/store.py @@ -2,6 +2,7 @@ from collections import defaultdict, deque from typing import Protocol from app.bars.models import Bar, Timeframe +from app.config import DEFAULT_MAX_BARS_PER_TF class BarStore(Protocol): @@ -11,7 +12,7 @@ class BarStore(Protocol): class InMemoryBarStore: - def __init__(self, max_bars_per_tf: int = 5000): + def __init__(self, max_bars_per_tf: int = DEFAULT_MAX_BARS_PER_TF): self._bars: dict[Timeframe, deque[Bar]] = defaultdict( lambda: deque(maxlen=max_bars_per_tf) ) diff --git a/app/config.py b/app/config.py index 9350662..44c5125 100644 --- a/app/config.py +++ b/app/config.py @@ -5,6 +5,8 @@ from pydantic_settings import BaseSettings, SettingsConfigDict from app.bars.models import Timeframe +DEFAULT_MAX_BARS_PER_TF = 5000 + TIMEFRAME_WEIGHT = { Timeframe.M1: 1, Timeframe.M2: 1, @@ -29,7 +31,7 @@ class Settings(BaseSettings): seed_1m_range: str = "8d" timeframes: str = "1m,5m,15m,30m,1h,1d" base_timeframes: str = "1m,30m,1d" - max_bars_per_tf: int = 5000 + max_bars_per_tf: int = DEFAULT_MAX_BARS_PER_TF # Rollback gate for source-timeframe trendline geometry. When false, both # server pricing and browser rendering use the previous 1m/displayed grid. trendline_source_geometry: bool = True diff --git a/docs/NEXT_STEPS.md b/docs/NEXT_STEPS.md index 4a805c2..0433660 100644 --- a/docs/NEXT_STEPS.md +++ b/docs/NEXT_STEPS.md @@ -1,6 +1,6 @@ # Current recommendations -Last reviewed: 2026-08-11 05:58 CDT. +Last reviewed: 2026-08-15 00:00 CDT. This file is the short list of work worth considering next. Verified history, measurements and completed work remain in `docs/implementation.md`, and the @@ -26,6 +26,18 @@ missed a real overlay offset. Capture the asset commit, final three bar times, their logical/x coordinates, the first two future coordinates, canonical line prices, and page-pixel samples from the rendered line before changing code. +### Load bars from the visible window + +The 1m chart stopping at ~1am was a leftover `get(tf, 1000)` on the +WebSocket snapshot. The store already held 5,000. The socket now sends +everything in the store (`MAX_BARS_PER_TF`). That is the immediate fix, +not the destination. + +Next: send the visible window and fetch older bars when the time scale +hits the left edge. Do not put another silent numeric cap on a payload. +How much the client gets is either “what is on screen” or the named +store limit — never a bare `1000` in a socket handler. + ### Deepen freshness telemetry The status bar now reports when the browser received its latest snapshot or bar. diff --git a/docs/implementation.md b/docs/implementation.md index 25bf5de..ef114cc 100644 --- a/docs/implementation.md +++ b/docs/implementation.md @@ -1074,3 +1074,12 @@ Production then showed that whitespace-only points could still disappear from th shared scale. The time-scale owner now uses transparent zero-valued points with an `autoscaleInfoProvider` that returns null. They remain invisible and cannot affect price range, but Lightweight Charts retains their timestamps as real series data. + +### 2026-08-15 — 1m zoom-out died at ~1am because the socket sent 1,000 bars + +Compressing the 1m time scale stopped around 01:00, then looked empty. +`HELD` was 5,000; `window.__chart.bars.length` was 1,000. 5m looked deep +because the same 1,000 bars are 3.5 days. The cap was a literal from the +first live-chart commit, never a setting. Snapshot now sends the store +(`max_bars_per_tf`). Viewport fetch is the follow-up — do not replace +one silent number with another. diff --git a/tests/test_ws_preferences.py b/tests/test_ws_preferences.py index d940228..ff2c077 100644 --- a/tests/test_ws_preferences.py +++ b/tests/test_ws_preferences.py @@ -52,6 +52,14 @@ def test_websocket_layer_preferences_are_isolated_per_connection(tmp_path): assert runtime.clusters == original_clusters +def test_a_1m_snapshot_is_not_capped_shorter_than_the_store(tmp_path): + runtime = Runtime(Settings(manual_lines_path=tmp_path / "manual_lines.json")) + for minute in range(1001): + runtime.store.put(Bar(Timeframe.M1, 60 * minute, 1, 1, 1, 1, 1, True, "/ES", "test")) + + assert len(snapshot(runtime, Timeframe.M1)["bars"]) == 1001 + + def test_quote_change_always_uses_the_daily_session_open(tmp_path): runtime = Runtime(Settings(manual_lines_path=tmp_path / "manual_lines.json")) runtime.store.put(Bar(Timeframe.D1, 1000, 6123.25, 6140, 6100, 6130, 1, False, "/ES", "test"))