Skip to content

Commit bae19f1

Browse files
author
deeleeramone
committed
fix more inline tv nonsense
1 parent d0f89af commit bae19f1

9 files changed

Lines changed: 163 additions & 64 deletions

File tree

pywry/examples/pywry_demo_tvchart.ipynb

Lines changed: 6 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,7 @@
77
"source": [
88
"# PyWry TradingView Lightweight Charts Demo\n",
99
"\n",
10-
"This notebook demonstrates displaying OHLCV candlestick charts with volume\n",
11-
"using `show_tvchart()` from PyWry's inline module."
10+
"This notebook demonstrates the minimum viable code required to render a TradingView chart using PyWry. It covers two concepts - static time series & UDF Adapter. "
1211
]
1312
},
1413
{
@@ -54,16 +53,10 @@
5453
"metadata": {},
5554
"outputs": [],
5655
"source": [
57-
"from pywry.inline import show_tvchart\n",
56+
"from pywry import PyWry\n",
5857
"\n",
59-
"widget = show_tvchart(\n",
60-
" data,\n",
61-
" title=\"OHLCV Demo\",\n",
62-
" height=500,\n",
63-
" callbacks={\n",
64-
" \"tvchart:click\": lambda d, t, l: print(f\"Clicked: time={d.get('time')}\"),\n",
65-
" },\n",
66-
")"
58+
"app = PyWry()\n",
59+
"widget = app.show_tvchart(data)"
6760
]
6861
},
6962
{
@@ -86,9 +79,9 @@
8679
"source": [
8780
"from pywry.tvchart import build_tvchart_toolbars\n",
8881
"\n",
89-
"widget2 = show_tvchart(\n",
82+
"widget2 = app.show_tvchart(\n",
9083
" data,\n",
91-
" title=\"Chart with Toolbars\",\n",
84+
" symbol=\"Mock Symbol\",\n",
9285
" height=600,\n",
9386
" toolbars=build_tvchart_toolbars(),\n",
9487
")"

pywry/examples/pywry_demo_tvchart_yfinance.py

Lines changed: 41 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -807,6 +807,8 @@ def __init__(self, app: PyWry, cache: BarCache) -> None:
807807
self._stop = threading.Event()
808808
# symbol → delay in minutes (from exchangeDataDelayedBy)
809809
self._delay: dict[str, int] = {}
810+
# Active marquee symbol — only this symbol may update the marquee.
811+
self._active_marquee_symbol: str = ""
810812
# Periodic backfill timers (symbol → Timer)
811813
self._backfill_timers: dict[str, threading.Timer] = {}
812814

@@ -830,7 +832,13 @@ def subscribe(self, guid: str, symbol: str, resolution: str, delay_minutes: int
830832
self._subs[guid] = {"symbol": symbol, "resolution": resolution}
831833
symbols = list({s["symbol"] for s in self._subs.values()})
832834

833-
if self._ws is None:
835+
# (Re)create WebSocket if not running or if the listener died.
836+
ws_alive = self._ws is not None and self._thread is not None and self._thread.is_alive()
837+
if not ws_alive:
838+
# Close stale handle if it exists
839+
if self._ws is not None:
840+
with contextlib.suppress(Exception):
841+
self._ws.close()
834842
self._ws = yf.WebSocket(verbose=False)
835843
self._ws.subscribe(symbols)
836844
self._stop.clear()
@@ -932,6 +940,9 @@ def _listen_loop(self) -> None:
932940
except Exception as exc:
933941
if not self._stop.is_set():
934942
print(f"[websocket] Connection closed: {exc}")
943+
finally:
944+
# Mark WebSocket as dead so subscribe() recreates it.
945+
self._ws = None
935946

936947
def _on_message(self, msg: dict[str, Any]) -> None:
937948
"""Handle a WebSocket tick message from Yahoo Finance.
@@ -991,8 +1002,11 @@ def _on_message(self, msg: dict[str, Any]) -> None:
9911002
if not _is_overnight(bar_time, symbol):
9921003
self._cache.append_bar(symbol, "1m", bar)
9931004

994-
# Push live fields to the marquee ticker strip
995-
self._update_marquee(symbol, msg)
1005+
# Only update the marquee for the currently active symbol so that
1006+
# stale ticks from a previously subscribed symbol don't overwrite
1007+
# the freshly seeded marquee after a symbol change.
1008+
if symbol.upper() == self._active_marquee_symbol:
1009+
self._update_marquee(symbol, msg)
9961010

9971011
def _update_marquee(self, symbol: str, msg: dict[str, Any]) -> None:
9981012
"""Emit marquee-set-item events for each live data field."""
@@ -1123,6 +1137,24 @@ def _emit(
11231137
payload["styles"] = styles
11241138
app.emit("toolbar:marquee-set-item", payload)
11251139

1140+
# -- Reset ALL marquee slots so stale data from the prior symbol
1141+
# never persists (e.g. Mkt Cap from a stock showing on a future).
1142+
for slot in (
1143+
"ws-price",
1144+
"ws-change",
1145+
"ws-session",
1146+
"ws-ext-price",
1147+
"ws-ext-change",
1148+
"ws-open",
1149+
"ws-high",
1150+
"ws-low",
1151+
"ws-volume",
1152+
"ws-mktcap",
1153+
"ws-vol24",
1154+
"ws-symbol",
1155+
):
1156+
_emit(slot, "")
1157+
11261158
# Always show regular session close in the primary slots.
11271159
reg_price = info.get("regularMarketPrice")
11281160
reg_change = info.get("regularMarketChange")
@@ -1190,8 +1222,10 @@ def _emit(
11901222
_emit("ws-volume", _fmt_volume(float(volume)))
11911223

11921224
mkt_cap = info.get("marketCap")
1193-
if mkt_cap is not None:
1225+
if mkt_cap is not None and mkt_cap > 0:
11941226
_emit("ws-mktcap", _fmt_number(float(mkt_cap), 2))
1227+
else:
1228+
_emit("ws-mktcap", "—")
11951229

11961230
# Crypto-specific 24h volume
11971231
is_crypto = (info.get("quoteType") or "").lower() == "cryptocurrency"
@@ -1290,6 +1324,9 @@ def on_resolve(data: dict[str, Any]) -> None:
12901324
symbol_info=info,
12911325
chart_id=chart_id,
12921326
)
1327+
# Mark this symbol as the active marquee target BEFORE seeding
1328+
# so that stale WebSocket ticks for the old symbol are filtered.
1329+
streamer._active_marquee_symbol = symbol.upper()
12931330
# Seed the marquee with snapshot data from ticker.info
12941331
_seed_marquee(app, symbol)
12951332

pywry/pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -91,7 +91,7 @@ notebook = [
9191
"ipykernel>=7.2.0",
9292
]
9393
mcp = [
94-
"fastmcp>=3.0.0",
94+
"fastmcp>=3.2.2",
9595
]
9696
auth = [
9797
"authlib>=1.3.0",

pywry/pywry/frontend/src/tvchart-widget.js

Lines changed: 31 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -99,6 +99,28 @@ function render({ model, el }) {
9999
}
100100
el = el.parentElement;
101101
}
102+
// No sourceEl or DOM walk failed — resolve from data.chartId
103+
// via the chart registry (covers tvchart emit calls that don't
104+
// pass a source element, e.g. compare, symbol-change, intervals).
105+
var chartId = data && data.chartId;
106+
if (chartId && window.__PYWRY_TVCHARTS__) {
107+
var entry = window.__PYWRY_TVCHARTS__[chartId];
108+
if (entry) {
109+
if (entry.bridge) { entry.bridge.emit(type, data); return; }
110+
if (entry.container) {
111+
var w = entry.container.closest && entry.container.closest('.pywry-widget');
112+
if (w && w._pywryInstance) { w._pywryInstance.emit(type, data); return; }
113+
}
114+
}
115+
}
116+
// Last resort: find any widget instance on the page
117+
var widgets = document.querySelectorAll('.pywry-widget');
118+
for (var i = 0; i < widgets.length; i++) {
119+
if (widgets[i]._pywryInstance) {
120+
widgets[i]._pywryInstance.emit(type, data);
121+
return;
122+
}
123+
}
102124
console.warn('[PyWry] No bridge found for event:', type);
103125
},
104126
on: function() {},
@@ -131,7 +153,15 @@ function render({ model, el }) {
131153
if (event.type) {
132154
pywry._fire(event.type, event.data);
133155
}
134-
} catch(e) { /* ignore */ }
156+
} catch(e) { console.error('[PyWry] change:_py_event parse error:', e); }
157+
});
158+
159+
// Custom comm messages bypass trait sync/batching — primary delivery
160+
// for Python→JS events when emit() runs inside a traitlets observer.
161+
model.on('msg:custom', function(msg) {
162+
if (msg && msg.type) {
163+
pywry._fire(msg.type, msg.data || {});
164+
}
135165
});
136166

137167
// Alert/toast support

pywry/pywry/frontend/src/tvchart/02-datafeed.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -644,7 +644,7 @@ function _tvInitDatafeedMode(entry, seriesList, theme) {
644644

645645
var _isDaily = function() {
646646
var r = entry._currentResolution || '';
647-
return /^[1-9]?[DWM]$/i.test(r) || /^\d+[DWM]$/i.test(r);
647+
return /^[1-9]?[DWM]$/.test(r) || /^\d+[DWM]$/.test(r);
648648
};
649649
var _isoDate = function(d, tz) {
650650
var y = d.toLocaleString('en-US', { timeZone: tz, year: 'numeric' });

pywry/pywry/frontend/src/tvchart/08-settings.js

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1918,7 +1918,7 @@ function _tvShowSeriesSettings(chartId, seriesId) {
19181918
panel.appendChild(footer);
19191919

19201920
renderBody();
1921-
document.body.appendChild(overlay);
1921+
_tvOverlayContainer(chartId).appendChild(overlay);
19221922
}
19231923

19241924
function _tvShowVolumeSettings(chartId) {
@@ -2380,7 +2380,7 @@ function _tvShowVolumeSettings(chartId) {
23802380
footer.appendChild(okBtn);
23812381
panel.appendChild(footer);
23822382

2383-
document.body.appendChild(overlay);
2383+
_tvOverlayContainer(chartId).appendChild(overlay);
23842384
}
23852385

23862386
function _tvShowChartSettings(chartId) {
@@ -3523,7 +3523,7 @@ function _tvShowChartSettings(chartId) {
35233523
if (target.tagName === 'INPUT' || target.tagName === 'SELECT') scheduleSettingsPreview();
35243524
});
35253525

3526-
document.body.appendChild(overlay);
3526+
_tvOverlayContainer(chartId).appendChild(overlay);
35273527
}
35283528

35293529
// ---------------------------------------------------------------------------

pywry/pywry/frontend/src/tvchart/11-legend.js

Lines changed: 38 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,15 @@ function _tvSetupLegendControls(chartId) {
1515
var entry = resolved.entry;
1616
if (!entry.chart) return;
1717

18+
// Live entry accessor — always returns the current chart entry from the
19+
// global registry. The closure-captured `entry` can go stale when the
20+
// chart is destroyed and recreated (e.g. on interval change). Functions
21+
// that read symbol metadata must use _liveEntry() to stay in sync.
22+
function _liveEntry() {
23+
var r = _tvResolveChartEntry(chartId);
24+
return (r && r.entry) ? r.entry : entry;
25+
}
26+
1827
function scopedById(id) {
1928
return _tvScopedById(chartId, id);
2029
}
@@ -710,13 +719,14 @@ function _tvSetupLegendControls(chartId) {
710719
_legendCloseSecurityInfo();
711720
var sid = String(seriesId || '');
712721
if (!sid) return;
713-
var info = (entry && entry._compareSymbolInfo && entry._compareSymbolInfo[sid])
714-
? entry._compareSymbolInfo[sid]
715-
: (entry && entry._resolvedSymbolInfo && entry._resolvedSymbolInfo[sid])
716-
? entry._resolvedSymbolInfo[sid]
722+
var e = _liveEntry();
723+
var info = (e && e._compareSymbolInfo && e._compareSymbolInfo[sid])
724+
? e._compareSymbolInfo[sid]
725+
: (e && e._resolvedSymbolInfo && e._resolvedSymbolInfo[sid])
726+
? e._resolvedSymbolInfo[sid]
717727
: {};
718728
var label = _legendSeriesLabel(sid);
719-
var rawSymbol = (entry && entry._compareSymbols && entry._compareSymbols[sid]) ? entry._compareSymbols[sid] : label;
729+
var rawSymbol = (e && e._compareSymbols && e._compareSymbols[sid]) ? e._compareSymbols[sid] : label;
720730

721731
var name = String(info.ticker || info.displaySymbol || label || '').trim() || 'Unknown';
722732
var description = String(info.fullName || info.description || '').trim() || 'Unavailable';
@@ -1203,23 +1213,25 @@ function _tvSetupLegendControls(chartId) {
12031213
}
12041214

12051215
function _legendTitleBase(ds) {
1216+
var e = _liveEntry();
12061217
var base = (ds && ds.baseTitle) ? ds.baseTitle : '';
1207-
if (!base && entry && entry.payload && entry.payload.useDatafeed && entry.payload.series && entry.payload.series[0] && entry.payload.series[0].symbol) {
1208-
base = String(entry.payload.series[0].symbol);
1218+
if (!base && e && e.payload && e.payload.useDatafeed && e.payload.series && e.payload.series[0] && e.payload.series[0].symbol) {
1219+
base = String(e.payload.series[0].symbol);
12091220
}
1210-
if (!base && entry && entry.payload && entry.payload.title) {
1211-
base = String(entry.payload.title);
1221+
if (!base && e && e.payload && e.payload.title) {
1222+
base = String(e.payload.title);
12121223
}
1213-
if (!base && entry && entry.payload && entry.payload.series && entry.payload.series[0] && entry.payload.series[0].seriesId) {
1214-
var s0 = String(entry.payload.series[0].seriesId);
1224+
if (!base && e && e.payload && e.payload.series && e.payload.series[0] && e.payload.series[0].seriesId) {
1225+
var s0 = String(e.payload.series[0].seriesId);
12151226
if (s0 && s0 !== 'main') base = s0;
12161227
}
12171228
if (!base) base = (mainKey === 'main' ? '' : mainKey);
12181229
// Description mode replaces the base title with resolved symbol info
12191230
if (ds && ds.description && ds.description !== 'Off') {
12201231
var descMode = ds.description;
1221-
var symInfo = (entry && entry._resolvedSymbolInfo && entry._resolvedSymbolInfo.main)
1222-
|| (entry && entry._mainSymbolInfo) || {};
1232+
var le = _liveEntry();
1233+
var symInfo = (le && le._resolvedSymbolInfo && le._resolvedSymbolInfo.main)
1234+
|| (le && le._mainSymbolInfo) || {};
12231235
var ticker = String(symInfo.ticker || symInfo.displaySymbol || symInfo.symbol || base || '').trim();
12241236
var descText = String(symInfo.description || symInfo.fullName || '').trim();
12251237
if (descMode === 'Description' && descText) {
@@ -1524,22 +1536,23 @@ function _tvSetupLegendControls(chartId) {
15241536

15251537
function _legendSeriesLabel(seriesId) {
15261538
var sid = String(seriesId || 'main');
1539+
var e = _liveEntry();
15271540
if (sid === 'main') {
15281541
var ds = _legendDataset() || {};
15291542
var base = (ds && ds.baseTitle) ? String(ds.baseTitle) : '';
1530-
if (!base && entry && entry.payload && entry.payload.useDatafeed && entry.payload.series && entry.payload.series[0] && entry.payload.series[0].symbol) {
1531-
base = String(entry.payload.series[0].symbol);
1543+
if (!base && e && e.payload && e.payload.useDatafeed && e.payload.series && e.payload.series[0] && e.payload.series[0].symbol) {
1544+
base = String(e.payload.series[0].symbol);
15321545
}
1533-
if (!base && entry && entry.payload && entry.payload.title) {
1534-
base = String(entry.payload.title);
1546+
if (!base && e && e.payload && e.payload.title) {
1547+
base = String(e.payload.title);
15351548
}
15361549
return base || 'Main';
15371550
}
1538-
if (entry && entry._compareLabels && entry._compareLabels[sid]) {
1539-
return String(entry._compareLabels[sid]);
1551+
if (e && e._compareLabels && e._compareLabels[sid]) {
1552+
return String(e._compareLabels[sid]);
15401553
}
1541-
if (entry && entry._compareSymbolInfo && entry._compareSymbolInfo[sid]) {
1542-
var info = entry._compareSymbolInfo[sid] || {};
1554+
if (e && e._compareSymbolInfo && e._compareSymbolInfo[sid]) {
1555+
var info = e._compareSymbolInfo[sid] || {};
15431556
var display = String(info.displaySymbol || info.ticker || '').trim();
15441557
if (display) return display.toUpperCase();
15451558
var full = String(info.fullName || '').trim();
@@ -1551,8 +1564,8 @@ function _tvSetupLegendControls(chartId) {
15511564
: infoSymbol.toUpperCase();
15521565
}
15531566
}
1554-
if (entry && entry._compareSymbols && entry._compareSymbols[sid]) {
1555-
var raw = String(entry._compareSymbols[sid]);
1567+
if (e && e._compareSymbols && e._compareSymbols[sid]) {
1568+
var raw = String(e._compareSymbols[sid]);
15561569
return raw.indexOf(':') >= 0 ? raw.split(':').pop().trim().toUpperCase() : raw.toUpperCase();
15571570
}
15581571
return sid;
@@ -1571,7 +1584,8 @@ function _tvSetupLegendControls(chartId) {
15711584
}
15721585

15731586
function _legendGetTimezone() {
1574-
var info = entry && entry._resolvedSymbolInfo && entry._resolvedSymbolInfo.main;
1587+
var e = _liveEntry();
1588+
var info = e && e._resolvedSymbolInfo && e._resolvedSymbolInfo.main;
15751589
return (info && info.timezone) ? String(info.timezone).trim() : 'America/New_York';
15761590
}
15771591

0 commit comments

Comments
 (0)