Commit graph

67 commits

Author SHA1 Message Date
25c2714850 Use larger CSS triangles for the live-price ends.
Same plot overlay, no extra axis width.
2026-08-26 03:42:21 -05:00
a16a1d0e6e Do not widen the price scale for the live-price arrows.
← sits on the plot, just left of the label. After-the-digits needs
space we should not take from candles.
2026-08-26 03:41:38 -05:00
e1e0e40c10 Keep the live-price ← just inside the chart, after the label.
The plot overlay cannot reach the axis; this one sits on #chart.
2026-08-26 03:39:56 -05:00
bcc86fbfd2 Put live-price arrows on the plot: → left, ← right.
The last arrow sat off the right edge of the chart.
2026-08-26 03:35:52 -05:00
54d7f880af Mark live price with a left arrow after the axis label.
The scale-wide frame sat too far right and barely read.
2026-08-26 03:33:43 -05:00
20d02fc3d2 Use LWC LargeDashed for the live price line.
Custom overlay dash was the wrong place to spend. LWC already paints
that line with the last candle.
2026-08-26 03:30:50 -05:00
8108f2dea3 Draw the live price as a dash-dot ray across the plot.
LWC keeps the axis label only. The overlay uses a 14-4-2-4 dash so it
does not look like a trendline.
2026-08-26 03:29:30 -05:00
5708150ba3 Show the live-price frame: keyframes were fading from opacity 0. 2026-08-26 03:28:14 -05:00
c784f9ed12 Pulse a frame around the live price label, not the whole plot.
Opacity on a ~50x20 box can use the compositor. The old full-width
gradient could not.
2026-08-26 03:26:26 -05:00
111d367224 Animate the live-price cue with transform only.
background-position on a full-width gradient was ~15% CPU. The dash
stays put; only a small shine translates.
2026-08-26 03:25:30 -05:00
18585e42cd trendline selection issue 2026-08-23 23:25:55 -05:00
dae60fdf67 undo 2026-08-21 03:10:31 +00:00
6e69b7337a current bar fix and ma features 2026-08-20 23:15:34 +00:00
ac2943f248 fib look and feel tweaks 2026-08-19 09:33:18 +00:00
08cff3ce53 enable symbols and comments in the future whitespace 2026-08-19 08:36:31 +00:00
44cec38750 slope in tooltip 2026-08-19 05:30:16 +00:00
cc25871032 Keep the Schwab token alive, and reconnect from the header
The live socket never made a REST call, so the seven-day refresh
token expired while the chart still looked fine. A deploy then
could not log in. Ping user preferences every six hours, and when
the grant is already dead offer a one-click reconnect that writes
the token on the existing callback.
2026-08-18 10:11:02 +00:00
d9a272760b Add a Drawings layer toggle that hides lines and comments 2026-08-18 09:53:33 +00:00
baf2048867 ohlc and scroll 2026-08-16 21:33:09 -05:00
73beae051a tweaks 2026-08-15 05:14:28 -05:00
11508e5325 tweaks 2026-08-15 04:48:49 -05:00
3adb98c1fe resizable drawing list 2026-08-15 04:33:06 -05:00
777ad4af91 version at bottom 2026-08-15 03:57:31 -05:00
e1d2c60af7 diagnose line problem 2026-08-15 02:54:19 -05:00
796e6cdeb8 superior extended trendlines 2026-08-15 00:58:19 -05:00
6e781ed163 options finding feature 2026-08-14 06:18:53 -05:00
a921db4610 horizontal line to start of trendline problem 2026-08-14 06:06:16 -05:00
8ab054cf67 timeframe trendline interactions 2026-08-14 04:58:08 -05:00
952a3bb7f3 animated current price 2026-08-14 03:53:04 -05:00
a09e4f1208 better select and visibility 2026-08-14 03:33:53 -05:00
087d9d9d2a changed ntfy string 2026-08-14 02:44:10 -05:00
f9e02fc3e4 fixed del key bug 2026-08-14 01:12:20 -05:00
0a56d56261 reposition price level lines 2026-08-13 06:44:01 -05:00
32e25b84aa Number alerts, stamp them locally, and compact the zone list
Alerts get a number, assigned server-side and shown in both the push and the
Events list, so a notification on a phone can be matched to a row on a screen
when several fire together. It could not come from the browser: that counter
restarts on reload and differs between tabs. It is persisted next to the
cooldown state, because numbering restarting after a deploy would collide with a
phone's existing notification history — which changed that file from a list to
an object, with the loader still reading the old shape.

Pushes now carry a timestamp in the configured zone rather than the server's.
ALERT_TIMEZONE defaults to America/Chicago; containers run UTC, and a push
reading 02:14 to someone seeing 21:14 costs a translation every time. The
browser already formats its own times locally and is unchanged.

Confluence zones are one line each, ordered by price rather than by proximity,
so the list reads top to bottom the way the chart does and all of them fit on
screen — sixteen zones in 394px, about 25px each, where each previously took a
four-line block. Ordering is a display concern only: the server still returns
them nearest-first, which is what the alert path wants.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-11 21:44:57 -05:00
e957991242 symbol improvements 2026-08-11 19:51:39 -05:00
81045e2128 trendlines should only show numbers until selected or hover 2026-08-11 18:59:12 -05:00
c653e65d5b diagnostic capture feature 2026-08-11 16:08:32 -05:00
a395818581 Post cross-thread events through the loop, and measure how late it runs
P0 from docs/async_refactor.md. The mutating routes are sync `def`, so FastAPI
runs them in a threadpool, and they reach Runtime.broadcast through
rebuild_levels — writing asyncio.Queue directly from there. That queue is not
thread-safe: it wakes a consumer by resolving a Future, which only the loop
thread may do. A dropped wakeup means a drawing made in one browser does not
reach another until the next market tick.

broadcast now posts through call_soon_threadsafe when it is off the loop, and
publishes directly when it is on it, so the stream's own path pays nothing.

Worth being straight about the tests: the race is timing-dependent and did not
reproduce in twenty attempts — a foreign-thread put_nowait usually lands in the
ready queue before the loop sleeps, and a tick every second covers the rest.
Even asyncio's debug thread-affinity check stays quiet unless a consumer is
parked on the Future at that instant. So the tests assert the contract rather
than provoke the failure: a broadcast from a worker thread must go through
call_soon_threadsafe, one from the loop must deliver synchronously, and both
must arrive.

Also adds the loop-lag probe, which reports scheduling drift as loop_lag_ms on
/api/status. It found P1 on its first run: 19,441ms worst against 1.5ms in
steady state, which is seeding blocking the loop. "The chart feels laggy" is now
a number.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-11 15:30:45 -05:00
8ca774f821 compact drawing list 2026-08-11 15:13:48 -05:00
e02f919b27 better default names 2026-08-11 14:36:58 -05:00
3b3c06a1f3 - Drag a selected trendline body to reposition the entire line.
- Duplicate and delete from the line context menu.
     - Copies shift ten bars right.
     - Default names are up and down; copies become up 2, down 2, etc.
     - Exact local data receipt time including seconds.
     - Deployment timestamp removed.
     - Test cleanup no longer deletes drawings created from your browser.
     - JWT password session flow.
2026-08-11 05:46:09 -05:00
4488c7d4b9 Fix freshness and daily date labels 2026-08-11 04:23:19 -05:00
91677127a0 Unify chart geometry and deepen 30m history 2026-08-11 04:08:02 -05:00
01f5cd4060 Keep chart overlays aligned with plot 2026-08-11 02:22:38 -05:00
decd069ec8 Position chart overlays against the plot, not the element
The trendline snap indicator was 66 pixels out, and so was every other overlay.

Lightweight Charts reports coordinates from the plot area's origin. The chart
element also contains the price scales, so enabling the left scale for the daily
labels moved the plot 66px into the element — and each overlay positioned with
left: against the element inherited that error twice over. The cursor's
element-x was read as a plot-x, resolving a bar about 66px right of the pointer;
the indicator was then drawn at that bar's plot-x interpreted as element-x,
landing 66px left of where the bar is painted. Neither near the cursor nor near
the bar, and scaling with zoom — two bars at 30m, a dozen at 1m — which is why
it read as random rather than as an offset.

Every diagnostic number agreed with itself the whole time, because dot_y,
expected_y and bar_low_y all come from the same API and shared the same wrong
origin. Instrumentation cannot see a systematic error in its own frame of
reference; what found this was comparing canvas.width to element.clientWidth and
getting 0.894.

Overlays now live in one container positioned over the plot canvas and inherit
plot coordinates untranslated — snap dot and label, comments, anchor handles,
preview line, tooltip, price tag, context menu — and eventPoint subtracts the
same offset so a pointer position and a chart coordinate mean the same thing.
The container follows the plot on resize.

Verified in page pixels rather than through the coordinate API: with the cursor
placed 30px below a known bar's low, the dot lands 30px above the cursor, on
that low, labelled 7789.00 L against a bar low of 7789.

Device pixel ratio, viewport size, resize desynchronisation and the chart
scrolling under the gesture were each measured and ruled out before this. The
e2e helper computed expected times from element-relative x, the same mistake in
the tests, and is corrected here.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-10 23:55:51 -05:00
7ce6d33bb5 Show the price the anchor will use, and record where things run
The snap dot said where an anchor would land but not what it was, which made
"the dot is in the wrong place" and "the dot is on the wrong bar" impossible to
tell apart from a screenshot. It now carries a label with the price, whether it
is a high or a low, and the bar's time.

AGENTS.md gains the environment fact this session kept rediscovering: the agent
runs on a remote machine over SSH while the user's browser runs on another, so
headless runs here render on different hardware at a different size and pixel
ratio, and "it passes in my headless run" is not evidence the user's problem is
fixed. Three consecutive reproductions passed server-side while the chart was
unusable on the user's screen. When a visual bug will not reproduce, match their
viewport and deviceScaleFactor explicitly — and prefer putting the numbers on
screen over asking for another console paste.

Device pixel ratio is ruled out for the current trendline complaint: the
bottom-sweep check lands 115 of 115 at 1600x1000 and at 1900x1400 with ratios of
1, 1.25, 1.5 and 2.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-10 22:35:40 -05:00
0e32c0bbab Rename to /ESsence and write down the house rules
The header spent a whole line on a "CME FUTURES" eyebrow that told you nothing
the chart didn't — it only ever shows /ES. Dropping it takes the header from
64px to 44px and hands the space to the chart. The title becomes /ESsence.

AGENTS.md records the rules worth keeping, chief among them that a bug should
prompt the question of whether a unit test could reasonably have caught it —
written when the answer is yes, skipped when it is a rendering or data-source
quirk, and named after the failure rather than the function.

It is AGENTS.md rather than CLAUDE.md deliberately: opencode's instruction
loader walks up looking for AGENTS.md only and never reads CLAUDE.md, and the
ask-opencode skill asks the calling agent to distil house rules by hand rather
than forwarding a file. CLAUDE.md is a symlink to it so both tools resolve to
one source of truth.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-10 17:10:24 -05:00
0b3244b725 Make trendline placement match what the cursor shows
Two faults, one of them a regression that reached production.

Making a pending anchor always win fixed a twitch on the second click stealing
the start point, and broke the opposite case: a genuine press-drag begun after
an abandoned click was hijacked by that stale anchor, so the line started far
from where the drag did. A single threshold decides now — 12px of travel between
press and release makes a gesture a drag, wide enough to survive a twitch on a
deliberate click and unambiguous for a real one. A drag abandons any half-placed
anchor instead of adopting it.

The crosshair was also lying. Lightweight Charts defaults to CrosshairMode.Magnet,
which snaps the crosshair to the bar's close, so hovering beside a bar's low drew
it mid-bar and a correctly-placed anchor looked wrong. Measured: aiming 4px above
a bar low anchors at the low, 7773, not the close, 7773.25 — the placement was
right and only the feedback was wrong. Arming a tool switches the crosshair to
Normal, and a snap dot now marks the exact point the anchor will use, coloured by
the side that extreme implies.

Verified in a browser across all four paths: two clicks with a twitch on the
second, an abandoned click then a real drag, a plain press-drag, and hovering.
Each starts where it should and lands on a bar extreme, and the stale anchor is
no longer adopted.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-10 16:29:00 -05:00
9008e9cd8e Show the trendline Side control only when it can do anything
Snapping now always lands an anchor on a bar extreme, and which extreme it is
decides the side — a high is resistance, a low is support. That left the Side
dropdown unable to affect the result: it was overridden on every drawn line.

It now appears only when "Snap to highs/lows" is off, the one case where there
is no extreme to infer from. With snapping on the row reads "Side auto" instead,
so the behaviour is stated rather than implied by a control that does nothing.
An "Auto" option in the dropdown would have been the same no-op wearing a label.

Verified in a browser both ways, and that the inference itself holds with the
dropdown left on its default: drawing above the candles yields resistance
snapped to the bar high, drawing below yields support snapped to the bar low.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-10 16:18:56 -05:00
9ab68f6cff Add chart comments and turn Lines & levels into Drawings
A comment is a ManualLine with kind="comment", so it inherits persistence, the
shared drawing-number sequence, the list, filtering and deletion rather than
needing a parallel set of endpoints. The rule that must never bend is that
ManualLineStore.levels() excludes them: a comment reaching the level list would
join a confluence cluster and push a phone notification about a piece of text.
It is created with armed=False, and PATCH returns to_dict() rather than
to_level() for a comment, so nothing is ever handed a level-shaped comment.
Three tests cover the exclusion, the shared numbering and the kind derived for
drawings saved before comments existed.

Pinned comments carry anchor_t and anchor_p and travel with the chart; floating
ones carry x and y as fractions of the pane, hold their place through any zoom
and can be dragged. They render as DOM rather than canvas because they hold
arbitrary text, collapse to a numbered dot, and a floating one must ignore the
time scale entirely. A pinned comment scrolled out of view parks on the edge it
left, pointing back toward itself, so it never simply disappears.

Lines & levels becomes Drawings, filtered by type and by text. The text match
covers the label, the kind and the #number, so "comment", "cpi" and "7" all
narrow the list, and Delete acts on whatever the filter shows — which is what
makes deleting by type or by string one button.

Verified in a browser: placing a comment renders it at the click, clicking it
collapses it, scrolling away parks it on the edge, a floating one holds its
pixel position through a 300-bar scroll, and the filters cut 3 rows to 1 by type,
1 by text and 0 for a miss, with no console errors.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-10 15:58:21 -05:00