diff --git a/app/analysis/alerts.py b/app/analysis/alerts.py index 58c0897..7cabc2b 100644 --- a/app/analysis/alerts.py +++ b/app/analysis/alerts.py @@ -137,6 +137,7 @@ class AlertEngine: now: int, symbol: str, watched: list[Level] | None = None, + confluence: bool = True, ) -> list[Alert]: tolerance = 0.5 * atr15 if tolerance <= 0: @@ -184,7 +185,7 @@ class AlertEngine: # from 1 (5m) to 4 (1h) against a threshold of 28, so gating on # score would mean a line you deliberately drew could never alert. # A disarmed one has already had its say and no longer qualifies. - if not drawn and cluster.score < self.min_score: + if not drawn and (not confluence or cluster.score < self.min_score): continue if abs(cluster.center - current_price) > tolerance and not early_drawn: continue diff --git a/app/api/routes.py b/app/api/routes.py index 8a946e2..4a19599 100644 --- a/app/api/routes.py +++ b/app/api/routes.py @@ -231,6 +231,18 @@ def confluence(request: Request): } +@router.get("/prefs/confluence-alerts") +def get_confluence_alerts(request: Request): + return {"enabled": request.app.state.runtime.confluence_alerts_enabled()} + + +@router.put("/prefs/confluence-alerts") +def put_confluence_alerts(request: Request, payload: dict): + if "enabled" not in payload or not isinstance(payload["enabled"], bool): + raise HTTPException(400, "enabled must be a boolean") + return request.app.state.runtime.set_confluence_alerts(payload["enabled"]) + + @router.get("/prefs/ma-alerts") def get_ma_alerts(request: Request): return request.app.state.runtime.ma_alerts() diff --git a/app/runtime.py b/app/runtime.py index b3000d2..4784cd6 100644 --- a/app/runtime.py +++ b/app/runtime.py @@ -287,9 +287,17 @@ class Runtime: evaluation_t, self.stream.symbol, self.watched_ma_levels(), + confluence=self.confluence_alerts_enabled(), ) ) + def confluence_alerts_enabled(self) -> bool: + return bool(self.user_prefs.get("confluence_alerts")) + + def set_confluence_alerts(self, enabled: bool) -> dict: + self.user_prefs.put("confluence_alerts", bool(enabled)) + return {"enabled": self.confluence_alerts_enabled()} + def ma_alerts(self) -> dict: stored = self.user_prefs.get("ma_alerts") or {} return {"1d": list(stored.get("1d") or [])} diff --git a/docs/plan.md b/docs/plan.md index ba469ea..ffc39bb 100644 --- a/docs/plan.md +++ b/docs/plan.md @@ -1143,6 +1143,9 @@ this is a complete, useful product with zero hand-drawn input and zero tuning. **Done when:** a replayed tape produces a sane number of alerts (single digits per session), each corresponding to a real multi-timeframe convergence. +**Update 2026-08-27:** Auto confluence (ZONE) alerts are off by default. Config โ†’ +Confluence alerts. Armed drawings and DMA bells still fire. + ### M5 โ€” Manual trendlines Two-click drawing, snapping, persistence, feeding the same confluence engine (ยง7.3a). Hand-drawn lines are authoritative โ€” full weight, no quality discount. diff --git a/static/app.js b/static/app.js index c68a865..b0a6816 100644 --- a/static/app.js +++ b/static/app.js @@ -135,6 +135,7 @@ createApp({ const drawingFilter = ref(''); const drawingKind = ref('all'); const maAlerts = ref({ '1d': [] }); + const confluenceAlerts = ref(false); const drawingTf = ref('all'); const diagnosticMode = ref(ConfluenceChart.diagnosticMode()); const captureBusy = ref(false); @@ -1474,6 +1475,11 @@ createApp({ if (response.ok) maAlerts.value = await response.json(); } + async function refreshConfluenceAlerts() { + const response = await apiFetch('/api/prefs/confluence-alerts'); + if (response.ok) confluenceAlerts.value = Boolean((await response.json()).enabled); + } + async function toggleMaAlert(period) { const armed = new Set(maAlerts.value['1d'] || []); if (armed.has(period)) armed.delete(period); @@ -1549,6 +1555,14 @@ createApp({ localStorage.setItem('chart-color-row-labels', JSON.stringify(value)); }, { deep: true }); + watch(confluenceAlerts, value => { + apiFetch('/api/prefs/confluence-alerts', { + method: 'PUT', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ enabled: Boolean(value) }), + }).catch(() => {}); + }); + watch(animateCurrentPrice, value => { localStorage.setItem('chart-animate-current-price', String(value)); if (chartApi) chartApi.setCurrentPriceAnimation(value); @@ -1601,6 +1615,7 @@ createApp({ await refreshStatus(); await refreshComments(); await refreshMaAlerts(); + await refreshConfluenceAlerts(); const version = await fetch('/api/version').then(response => response.json()).catch(() => ({})); build.value = { commit: version.commit || 'dev', startedAt: version.started_at || '' }; connect(); @@ -1613,6 +1628,6 @@ createApp({ window.removeEventListener('keydown', handleKeydown); }); - return { status, price, sessionOpen, quoteChange, animateCurrentPrice, autoScrollLivePrice, extraDetail, hideLowerTfDrawings, barAge, dataUpdatedAt, buildStamp, timeframe, timeframes, drawingColors, drawingColorRows, drawingColorName, colorRowLabels, symbolChoices, selectedSymbol, symbolColor, symbolScale, symbolScales, symbolPanelOpen, prefs, clusters, clustersByPrice, events, eventsMore, loadOlderEvents, diagnosticMode, captureBusy, captureCountdown, captureDiagnostic, armedTool, drawName, drawColor, drawWidth, drawSide, snap, selectedDrawing, selectedDrawings, drawingList, startDrawingListResize, manualLines, hasDrawingSelection, allShownSelected, selectedAreHidden, alertPrice, alertNote, alertEarlyPoints, levelColor, levelWidth, addPriceAlert, armTool, selectTimeframe, allEnabled, toggleGroup, maValue, maAlertOn, toggleMaAlert, deleteSelected, deleteLine, toggleDrawingSelection, activateDrawing, toggleSelectAll, toggleSelectedVisibility, renameLine, updateLineStyle, updateLevelNumber, setArmed, commentText, commentFloat, comments, drawings, filteredDrawings, drawingFilter, drawingKind, drawingTf, deleteDrawing, toggleComment, togglePinned, chooseSymbol, toggleSymbolPanel, startSymbolDrag, dropSymbol, optionExpirations, optionExpiryId, optionSide, optionMode, optionMin, optionMax, optionContracts, optionUnderlying, optionBusy, optionError, optionSearched, optionCopied, onOptionsToggle, searchOptions, copyOption, canUndo, undoTitle, undo }; + return { status, price, sessionOpen, quoteChange, animateCurrentPrice, autoScrollLivePrice, extraDetail, hideLowerTfDrawings, confluenceAlerts, barAge, dataUpdatedAt, buildStamp, timeframe, timeframes, drawingColors, drawingColorRows, drawingColorName, colorRowLabels, symbolChoices, selectedSymbol, symbolColor, symbolScale, symbolScales, symbolPanelOpen, prefs, clusters, clustersByPrice, events, eventsMore, loadOlderEvents, diagnosticMode, captureBusy, captureCountdown, captureDiagnostic, armedTool, drawName, drawColor, drawWidth, drawSide, snap, selectedDrawing, selectedDrawings, drawingList, startDrawingListResize, manualLines, hasDrawingSelection, allShownSelected, selectedAreHidden, alertPrice, alertNote, alertEarlyPoints, levelColor, levelWidth, addPriceAlert, armTool, selectTimeframe, allEnabled, toggleGroup, maValue, maAlertOn, toggleMaAlert, deleteSelected, deleteLine, toggleDrawingSelection, activateDrawing, toggleSelectAll, toggleSelectedVisibility, renameLine, updateLineStyle, updateLevelNumber, setArmed, commentText, commentFloat, comments, drawings, filteredDrawings, drawingFilter, drawingKind, drawingTf, deleteDrawing, toggleComment, togglePinned, chooseSymbol, toggleSymbolPanel, startSymbolDrag, dropSymbol, optionExpirations, optionExpiryId, optionSide, optionMode, optionMin, optionMax, optionContracts, optionUnderlying, optionBusy, optionError, optionSearched, optionCopied, onOptionsToggle, searchOptions, copyOption, canUndo, undoTitle, undo }; }, }).mount('#app'); diff --git a/static/index.html b/static/index.html index cfe497c..becc6bb 100644 --- a/static/index.html +++ b/static/index.html @@ -74,6 +74,7 @@