From ff7d9c6e1c9e82556acb3e2330270da5c1b6e05a Mon Sep 17 00:00:00 2001 From: Chris Amow Date: Sun, 9 Aug 2026 20:45:30 -0500 Subject: [PATCH] Implement M3.5 persistent layer controls --- app/api/ws.py | 5 +++- static/app.js | 58 ++++++++++++++++++++++++++++++++++++++++++----- static/index.html | 14 ++++++++++++ static/style.css | 1 + 4 files changed, 71 insertions(+), 7 deletions(-) diff --git a/app/api/ws.py b/app/api/ws.py index 575fee3..66abeeb 100644 --- a/app/api/ws.py +++ b/app/api/ws.py @@ -27,15 +27,18 @@ async def websocket_endpoint(websocket: WebSocket): queue: asyncio.Queue = asyncio.Queue(maxsize=100) runtime.subscribers.add(queue) tf = Timeframe.M1 + prefs = None await websocket.send_json(snapshot(runtime, tf)) async def receive(): - nonlocal tf + nonlocal tf, prefs while True: message = await websocket.receive_json() if message.get("type") == "subscribe": tf = Timeframe(message.get("tf", "1m")) await websocket.send_json(snapshot(runtime, tf)) + elif message.get("type") == "prefs": + prefs = message receiver = asyncio.create_task(receive()) try: diff --git a/static/app.js b/static/app.js index 4691923..beed3cb 100644 --- a/static/app.js +++ b/static/app.js @@ -1,10 +1,19 @@ -const { createApp, ref, computed, onMounted, onUnmounted } = Vue; +const { createApp, ref, computed, watch, onMounted, onUnmounted } = Vue; + +const defaultPrefs = { + base_tf: '1m', + enabled: { ma: { '1d': [10, 20, 50, 100, 200], '4h': [], '1h': [] }, manual: true, auto: false }, + hidden_levels_score: false, +}; createApp({ setup() { const status = ref({ stream: 'disconnected', bars_held: {} }); const price = ref(null); - const timeframe = ref('1m'); + const storedPrefs = localStorage.getItem('chart-layer-prefs'); + const prefs = ref(storedPrefs ? JSON.parse(storedPrefs) : structuredClone(defaultPrefs)); + const timeframe = ref(prefs.value.base_tf || '1m'); + const levels = ref([]); const timeframes = ['1m', '2m', '5m', '15m', '30m', '1h', '4h', '1d']; const now = ref(Date.now()); let chartApi = null; @@ -25,19 +34,24 @@ createApp({ function connect() { const protocol = location.protocol === 'https:' ? 'wss' : 'ws'; socket = new WebSocket(`${protocol}://${location.host}/ws`); - socket.onopen = () => socket.send(JSON.stringify({ type: 'subscribe', tf: timeframe.value })); + socket.onopen = () => { + socket.send(JSON.stringify({ type: 'subscribe', tf: timeframe.value })); + sendPrefs(); + }; socket.onmessage = ({ data }) => { const message = JSON.parse(data); if (message.type === 'snapshot') { chartApi.setBars(message.bars); - chartApi.syncLevels(message.levels || []); + levels.value = message.levels || []; + syncVisibleLevels(); price.value = message.price; } else if (message.type === 'bar') { chartApi.updateBar(message.bar); price.value = message.bar.c; status.value.last_bar_t = message.bar.t; } else if (message.type === 'levels') { - chartApi.syncLevels(message.levels); + levels.value = message.levels; + syncVisibleLevels(); } }; socket.onclose = () => { @@ -48,11 +62,43 @@ createApp({ function selectTimeframe(tf) { timeframe.value = tf; + prefs.value.base_tf = tf; if (socket?.readyState === WebSocket.OPEN) { socket.send(JSON.stringify({ type: 'subscribe', tf })); } } + function enabled(level) { + if (level.kind === 'ma') return (prefs.value.enabled.ma[level.tf] || []).includes(level.period); + if (level.kind === 'manual') return prefs.value.enabled.manual; + return prefs.value.enabled.auto; + } + + function syncVisibleLevels() { + if (chartApi) chartApi.syncLevels(levels.value.filter(enabled)); + } + + function sendPrefs() { + if (socket?.readyState === WebSocket.OPEN) { + socket.send(JSON.stringify({ type: 'prefs', ...prefs.value })); + } + } + + function allEnabled(tf) { + const available = tf === '1d' ? [10, 20, 50, 100, 200] : [9, 21]; + return available.every(period => (prefs.value.enabled.ma[tf] || []).includes(period)); + } + + function toggleGroup(tf, checked) { + prefs.value.enabled.ma[tf] = checked ? (tf === '1d' ? [10, 20, 50, 100, 200] : [9, 21]) : []; + } + + watch(prefs, () => { + localStorage.setItem('chart-layer-prefs', JSON.stringify(prefs.value)); + syncVisibleLevels(); + sendPrefs(); + }, { deep: true }); + onMounted(() => { chartApi = new ConfluenceChart(); chartApi.create(document.getElementById('chart')); @@ -66,6 +112,6 @@ createApp({ if (chartApi) chartApi.destroy(); }); - return { status, price, barAge, timeframe, timeframes, selectTimeframe }; + return { status, price, barAge, timeframe, timeframes, prefs, selectTimeframe, allEnabled, toggleGroup }; }, }).mount('#app'); diff --git a/static/index.html b/static/index.html index 552d89f..0e0547f 100644 --- a/static/index.html +++ b/static/index.html @@ -28,6 +28,20 @@