Default confluence zone alerts off; Config can turn them back on.
Armed drawings and DMA bells still fire. The pref lives in user_prefs so pushes follow it, not a display checkbox.
This commit is contained in:
parent
3626795e03
commit
6f03560e74
9 changed files with 70 additions and 4 deletions
|
|
@ -137,6 +137,7 @@ class AlertEngine:
|
||||||
now: int,
|
now: int,
|
||||||
symbol: str,
|
symbol: str,
|
||||||
watched: list[Level] | None = None,
|
watched: list[Level] | None = None,
|
||||||
|
confluence: bool = True,
|
||||||
) -> list[Alert]:
|
) -> list[Alert]:
|
||||||
tolerance = 0.5 * atr15
|
tolerance = 0.5 * atr15
|
||||||
if tolerance <= 0:
|
if tolerance <= 0:
|
||||||
|
|
@ -184,7 +185,7 @@ class AlertEngine:
|
||||||
# from 1 (5m) to 4 (1h) against a threshold of 28, so gating on
|
# 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.
|
# score would mean a line you deliberately drew could never alert.
|
||||||
# A disarmed one has already had its say and no longer qualifies.
|
# 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
|
continue
|
||||||
if abs(cluster.center - current_price) > tolerance and not early_drawn:
|
if abs(cluster.center - current_price) > tolerance and not early_drawn:
|
||||||
continue
|
continue
|
||||||
|
|
|
||||||
|
|
@ -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")
|
@router.get("/prefs/ma-alerts")
|
||||||
def get_ma_alerts(request: Request):
|
def get_ma_alerts(request: Request):
|
||||||
return request.app.state.runtime.ma_alerts()
|
return request.app.state.runtime.ma_alerts()
|
||||||
|
|
|
||||||
|
|
@ -287,9 +287,17 @@ class Runtime:
|
||||||
evaluation_t,
|
evaluation_t,
|
||||||
self.stream.symbol,
|
self.stream.symbol,
|
||||||
self.watched_ma_levels(),
|
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:
|
def ma_alerts(self) -> dict:
|
||||||
stored = self.user_prefs.get("ma_alerts") or {}
|
stored = self.user_prefs.get("ma_alerts") or {}
|
||||||
return {"1d": list(stored.get("1d") or [])}
|
return {"1d": list(stored.get("1d") or [])}
|
||||||
|
|
|
||||||
|
|
@ -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
|
**Done when:** a replayed tape produces a sane number of alerts (single digits per
|
||||||
session), each corresponding to a real multi-timeframe convergence.
|
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
|
### M5 — Manual trendlines
|
||||||
Two-click drawing, snapping, persistence, feeding the same confluence engine (§7.3a).
|
Two-click drawing, snapping, persistence, feeding the same confluence engine (§7.3a).
|
||||||
Hand-drawn lines are authoritative — full weight, no quality discount.
|
Hand-drawn lines are authoritative — full weight, no quality discount.
|
||||||
|
|
|
||||||
|
|
@ -135,6 +135,7 @@ createApp({
|
||||||
const drawingFilter = ref('');
|
const drawingFilter = ref('');
|
||||||
const drawingKind = ref('all');
|
const drawingKind = ref('all');
|
||||||
const maAlerts = ref({ '1d': [] });
|
const maAlerts = ref({ '1d': [] });
|
||||||
|
const confluenceAlerts = ref(false);
|
||||||
const drawingTf = ref('all');
|
const drawingTf = ref('all');
|
||||||
const diagnosticMode = ref(ConfluenceChart.diagnosticMode());
|
const diagnosticMode = ref(ConfluenceChart.diagnosticMode());
|
||||||
const captureBusy = ref(false);
|
const captureBusy = ref(false);
|
||||||
|
|
@ -1474,6 +1475,11 @@ createApp({
|
||||||
if (response.ok) maAlerts.value = await response.json();
|
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) {
|
async function toggleMaAlert(period) {
|
||||||
const armed = new Set(maAlerts.value['1d'] || []);
|
const armed = new Set(maAlerts.value['1d'] || []);
|
||||||
if (armed.has(period)) armed.delete(period);
|
if (armed.has(period)) armed.delete(period);
|
||||||
|
|
@ -1549,6 +1555,14 @@ createApp({
|
||||||
localStorage.setItem('chart-color-row-labels', JSON.stringify(value));
|
localStorage.setItem('chart-color-row-labels', JSON.stringify(value));
|
||||||
}, { deep: true });
|
}, { 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 => {
|
watch(animateCurrentPrice, value => {
|
||||||
localStorage.setItem('chart-animate-current-price', String(value));
|
localStorage.setItem('chart-animate-current-price', String(value));
|
||||||
if (chartApi) chartApi.setCurrentPriceAnimation(value);
|
if (chartApi) chartApi.setCurrentPriceAnimation(value);
|
||||||
|
|
@ -1601,6 +1615,7 @@ createApp({
|
||||||
await refreshStatus();
|
await refreshStatus();
|
||||||
await refreshComments();
|
await refreshComments();
|
||||||
await refreshMaAlerts();
|
await refreshMaAlerts();
|
||||||
|
await refreshConfluenceAlerts();
|
||||||
const version = await fetch('/api/version').then(response => response.json()).catch(() => ({}));
|
const version = await fetch('/api/version').then(response => response.json()).catch(() => ({}));
|
||||||
build.value = { commit: version.commit || 'dev', startedAt: version.started_at || '' };
|
build.value = { commit: version.commit || 'dev', startedAt: version.started_at || '' };
|
||||||
connect();
|
connect();
|
||||||
|
|
@ -1613,6 +1628,6 @@ createApp({
|
||||||
window.removeEventListener('keydown', handleKeydown);
|
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');
|
}).mount('#app');
|
||||||
|
|
|
||||||
|
|
@ -74,6 +74,7 @@
|
||||||
</details>
|
</details>
|
||||||
<details class="sidebar-section config-section">
|
<details class="sidebar-section config-section">
|
||||||
<summary>Config</summary>
|
<summary>Config</summary>
|
||||||
|
<label><input type="checkbox" v-model="confluenceAlerts"> Confluence alerts</label>
|
||||||
<label><input type="checkbox" v-model="animateCurrentPrice"> Animate current price</label>
|
<label><input type="checkbox" v-model="animateCurrentPrice"> Animate current price</label>
|
||||||
<label><input type="checkbox" v-model="autoScrollLivePrice"> Autoscroll to live price</label>
|
<label><input type="checkbox" v-model="autoScrollLivePrice"> Autoscroll to live price</label>
|
||||||
<label><input type="checkbox" v-model="hideLowerTfDrawings"> Hide lower-TF drawings</label>
|
<label><input type="checkbox" v-model="hideLowerTfDrawings"> Hide lower-TF drawings</label>
|
||||||
|
|
|
||||||
|
|
@ -26,6 +26,14 @@ def test_oscillation_fires_once_until_separation_and_cooldown():
|
||||||
assert len(engine.evaluate(cluster, 100, 1, 903, "/ES")) == 1
|
assert len(engine.evaluate(cluster, 100, 1, 903, "/ES")) == 1
|
||||||
|
|
||||||
|
|
||||||
|
def test_confluence_off_skips_auto_zones_but_keeps_drawn_lines():
|
||||||
|
engine = AlertEngine(min_score=6, cooldown_seconds=900)
|
||||||
|
auto = cluster_levels([level("a", 100, 3), level("b", 100.1, 4)], 100, 100, 1)
|
||||||
|
assert engine.evaluate(auto, 100, 1, 0, "/ES", confluence=False) == []
|
||||||
|
drawn = cluster_levels([drawn_line("ml_1", 100)], 100, 100, 1)
|
||||||
|
assert len(engine.evaluate(drawn, 100, 1, 0, "/ES", confluence=False)) == 1
|
||||||
|
|
||||||
|
|
||||||
def test_score_threshold_blocks_two_daily_mas_at_default_calibration():
|
def test_score_threshold_blocks_two_daily_mas_at_default_calibration():
|
||||||
engine = AlertEngine(min_score=28)
|
engine = AlertEngine(min_score=28)
|
||||||
cluster = cluster_levels([level("a", 100, 12), level("b", 100.1, 12)], 100, 100, 1)
|
cluster = cluster_levels([level("a", 100, 12), level("b", 100.1, 12)], 100, 100, 1)
|
||||||
|
|
|
||||||
|
|
@ -122,7 +122,7 @@ def test_closed_bar_alerts_use_the_closed_timestamp_not_the_provisional_tail(
|
||||||
))
|
))
|
||||||
seen = {}
|
seen = {}
|
||||||
|
|
||||||
def evaluate(clusters, price, atr15, at, symbol, watched):
|
def evaluate(clusters, price, atr15, at, symbol, watched, **_):
|
||||||
seen.update(at=at, price=price, centers=[cluster.center for cluster in clusters])
|
seen.update(at=at, price=price, centers=[cluster.center for cluster in clusters])
|
||||||
return []
|
return []
|
||||||
|
|
||||||
|
|
@ -158,7 +158,7 @@ def test_anchor_eviction_excludes_stale_manual_geometry_before_alerting(
|
||||||
))
|
))
|
||||||
seen_members = []
|
seen_members = []
|
||||||
|
|
||||||
def evaluate(clusters, price, atr15, at, symbol, watched):
|
def evaluate(clusters, price, atr15, at, symbol, watched, **_):
|
||||||
seen_members.extend(member.id for cluster in clusters for member in cluster.members)
|
seen_members.extend(member.id for cluster in clusters for member in cluster.members)
|
||||||
return []
|
return []
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -37,3 +37,21 @@ def test_ma_alert_endpoints_persist(tmp_path):
|
||||||
assert client.get("/api/prefs/ma-alerts").json() == {"1d": [10, 200]}
|
assert client.get("/api/prefs/ma-alerts").json() == {"1d": [10, 200]}
|
||||||
assert client.put("/api/prefs/ma-alerts", json={"1d": [7]}).status_code == 400
|
assert client.put("/api/prefs/ma-alerts", json={"1d": [7]}).status_code == 400
|
||||||
assert SHARED_USER in (tmp_path / "user_prefs.json").read_text(encoding="utf-8")
|
assert SHARED_USER in (tmp_path / "user_prefs.json").read_text(encoding="utf-8")
|
||||||
|
|
||||||
|
|
||||||
|
def test_confluence_alerts_default_off_and_persist(tmp_path):
|
||||||
|
app = FastAPI()
|
||||||
|
app.include_router(router)
|
||||||
|
app.state.runtime = Runtime(Settings(
|
||||||
|
manual_lines_path=tmp_path / "manual_lines.json",
|
||||||
|
alert_state_path=tmp_path / "alert_state.json",
|
||||||
|
user_prefs_path=tmp_path / "user_prefs.json",
|
||||||
|
))
|
||||||
|
client = TestClient(app)
|
||||||
|
|
||||||
|
assert client.get("/api/prefs/confluence-alerts").json() == {"enabled": False}
|
||||||
|
assert client.put("/api/prefs/confluence-alerts", json={"enabled": True}).json() == {
|
||||||
|
"enabled": True,
|
||||||
|
}
|
||||||
|
assert client.get("/api/prefs/confluence-alerts").json() == {"enabled": True}
|
||||||
|
assert client.put("/api/prefs/confluence-alerts", json={"enabled": "yes"}).status_code == 400
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue