Five defects found by exercising the running app rather than reading it: - Backspace inside the sidebar rename field deleted the trendline instead of a character. The window keydown handler never checked what was focused, so correcting a typo in a line's name destroyed the line. - playAlert() built a new AudioContext per alert and never closed it. Browsers cap a document at roughly six, after which alerts stop making any sound. One shared context now, with nodes released on end and a resume() for the autoplay policy. - Stored layer preferences were used verbatim, so any key added to defaultPrefs later would be missing for existing visitors. A missing enabled.ma is a crash rather than a cosmetic gap. They are now deep-merged onto the defaults, and unparseable state falls back instead of throwing. - The alert log keyed rows on a second-resolution timestamp, so two alerts in the same second collided. - Clusters embedded whole Level objects, including a moving average's entire point history — hundreds of entries reaching back years. Because clusters are re-sent on every closed 1m bar, this shipped the whole levels payload once a minute. Members are now compact summaries and the client joins on id; Cluster.to_dict() also stops round-tripping through asdict(), which was deep-copying those arrays before discarding them. /api/confluence drops from 61,838 to 1,245 bytes with five clusters live. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
69 lines
1.7 KiB
Python
69 lines
1.7 KiB
Python
from dataclasses import asdict, dataclass
|
|
from enum import Enum
|
|
from typing import Any
|
|
|
|
from app.bars.models import Timeframe
|
|
|
|
|
|
class LevelKind(str, Enum):
|
|
MANUAL = "manual"
|
|
MA = "ma"
|
|
TRENDLINE = "trendline"
|
|
HORIZONTAL = "horizontal"
|
|
|
|
|
|
class Side(str, Enum):
|
|
SUPPORT = "support"
|
|
RESISTANCE = "resistance"
|
|
|
|
|
|
@dataclass(slots=True)
|
|
class Level:
|
|
id: str
|
|
kind: LevelKind
|
|
tf: Timeframe
|
|
side: Side
|
|
weight: float
|
|
score: float
|
|
label: str
|
|
anchor_t: int
|
|
anchor_p: float
|
|
slope: float
|
|
points: list[tuple[int, float]] | None
|
|
touches: int
|
|
first_t: int
|
|
last_t: int
|
|
provisional: bool
|
|
hidden: bool
|
|
period: int | None = None
|
|
color: str | None = None
|
|
line_width: int | None = None
|
|
number: int | None = None
|
|
cutoff_t: int | None = None
|
|
|
|
def price_at(self, t: int) -> float:
|
|
return self.anchor_p + self.slope * (t - self.anchor_t)
|
|
|
|
def to_dict(self) -> dict[str, Any]:
|
|
value = asdict(self)
|
|
value["kind"] = self.kind.value
|
|
value["tf"] = self.tf.value
|
|
value["side"] = self.side.value
|
|
return value
|
|
|
|
def summary(self) -> dict[str, Any]:
|
|
"""Compact form for embedding inside a cluster.
|
|
|
|
Clusters go out on every closed 1m bar, and a moving average carries its
|
|
whole point history — hundreds of entries reaching back years. Embedding
|
|
the full level duplicated the entire levels payload once a minute. The
|
|
client already holds the full levels and joins on id.
|
|
"""
|
|
return {
|
|
"id": self.id,
|
|
"kind": self.kind.value,
|
|
"tf": self.tf.value,
|
|
"side": self.side.value,
|
|
"weight": self.weight,
|
|
"label": self.label,
|
|
}
|