transmite full 1m data

This commit is contained in:
Chris Amow 2026-08-15 04:22:40 -05:00
parent 777ad4af91
commit 5ee32b327c
8 changed files with 46 additions and 5 deletions

View file

@ -169,3 +169,7 @@ markers.
price is sitting on. There is no durable state yet. price is sitting on. There is no durable state yet.
- Times are epoch seconds, UTC, everywhere. Only the display is localised — - Times are epoch seconds, UTC, everywhere. Only the display is localised —
never shift the stored values. 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.

View file

@ -12,6 +12,7 @@ from fastapi import APIRouter, Depends, HTTPException, Query, Request, Response
from pydantic import BaseModel, Field from pydantic import BaseModel, Field
from app.bars.models import Timeframe from app.bars.models import Timeframe
from app.config import DEFAULT_MAX_BARS_PER_TF
from app.analysis.levels import Side from app.analysis.levels import Side
from app.analysis.manual_lines import ManualLine from app.analysis.manual_lines import ManualLine
from app.api.deps import require_token from app.api.deps import require_token
@ -141,7 +142,11 @@ def status(request: Request):
@router.get("/bars") @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: try:
timeframe = Timeframe(tf) timeframe = Timeframe(tf)
except ValueError as exc: except ValueError as exc:

View file

@ -86,7 +86,7 @@ def snapshot(runtime, tf: Timeframe, prefs: dict | None = None) -> dict:
return { return {
"type": "snapshot", "type": "snapshot",
"tf": tf.value, "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], "levels": [level.to_dict() for level in runtime.levels],
"clusters": [cluster.to_dict() for cluster in connection_clusters(runtime, prefs)], "clusters": [cluster.to_dict() for cluster in connection_clusters(runtime, prefs)],
"price": runtime.store.get(Timeframe.M1, 1)[-1].c "price": runtime.store.get(Timeframe.M1, 1)[-1].c

View file

@ -2,6 +2,7 @@ from collections import defaultdict, deque
from typing import Protocol from typing import Protocol
from app.bars.models import Bar, Timeframe from app.bars.models import Bar, Timeframe
from app.config import DEFAULT_MAX_BARS_PER_TF
class BarStore(Protocol): class BarStore(Protocol):
@ -11,7 +12,7 @@ class BarStore(Protocol):
class InMemoryBarStore: 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( self._bars: dict[Timeframe, deque[Bar]] = defaultdict(
lambda: deque(maxlen=max_bars_per_tf) lambda: deque(maxlen=max_bars_per_tf)
) )

View file

@ -5,6 +5,8 @@ from pydantic_settings import BaseSettings, SettingsConfigDict
from app.bars.models import Timeframe from app.bars.models import Timeframe
DEFAULT_MAX_BARS_PER_TF = 5000
TIMEFRAME_WEIGHT = { TIMEFRAME_WEIGHT = {
Timeframe.M1: 1, Timeframe.M1: 1,
Timeframe.M2: 1, Timeframe.M2: 1,
@ -29,7 +31,7 @@ class Settings(BaseSettings):
seed_1m_range: str = "8d" seed_1m_range: str = "8d"
timeframes: str = "1m,5m,15m,30m,1h,1d" timeframes: str = "1m,5m,15m,30m,1h,1d"
base_timeframes: str = "1m,30m,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 # Rollback gate for source-timeframe trendline geometry. When false, both
# server pricing and browser rendering use the previous 1m/displayed grid. # server pricing and browser rendering use the previous 1m/displayed grid.
trendline_source_geometry: bool = True trendline_source_geometry: bool = True

View file

@ -1,6 +1,6 @@
# Current recommendations # 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, This file is the short list of work worth considering next. Verified history,
measurements and completed work remain in `docs/implementation.md`, and the 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 their logical/x coordinates, the first two future coordinates, canonical line
prices, and page-pixel samples from the rendered line before changing code. 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 ### Deepen freshness telemetry
The status bar now reports when the browser received its latest snapshot or bar. The status bar now reports when the browser received its latest snapshot or bar.

View file

@ -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 shared scale. The time-scale owner now uses transparent zero-valued points with an
`autoscaleInfoProvider` that returns null. They remain invisible and cannot affect `autoscaleInfoProvider` that returns null. They remain invisible and cannot affect
price range, but Lightweight Charts retains their timestamps as real series data. 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.

View file

@ -52,6 +52,14 @@ def test_websocket_layer_preferences_are_isolated_per_connection(tmp_path):
assert runtime.clusters == original_clusters 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): def test_quote_change_always_uses_the_daily_session_open(tmp_path):
runtime = Runtime(Settings(manual_lines_path=tmp_path / "manual_lines.json")) 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")) runtime.store.put(Bar(Timeframe.D1, 1000, 6123.25, 6140, 6100, 6130, 1, False, "/ES", "test"))