transmite full 1m data
This commit is contained in:
parent
777ad4af91
commit
5ee32b327c
8 changed files with 46 additions and 5 deletions
|
|
@ -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.
|
||||||
|
|
|
||||||
|
|
@ -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:
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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.
|
||||||
|
|
|
||||||
|
|
@ -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.
|
||||||
|
|
|
||||||
|
|
@ -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"))
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue