The confluence engine had nothing to work with. Daily moving averages were the only level source, and they sat 163 to 697 points from price, so every cluster had exactly one member and no alert could ever fire. Two new sources, chosen for having a real following — the engine is a bet that many participants watch the same price, which is what makes a level hold: - Prior day high/low/close, from the last *closed* daily bar so mid-session the levels do not silently switch to today's own developing range. Full daily weight rather than the 0.75 average discount: a traded high is structure, not a derived average. - Session VWAP, anchored to the 18:00 ET open like the daily bars. Institutional execution is benchmarked against it, and zero-volume overnight minutes are skipped rather than dividing by zero. Both are stamped 1d, so they get their own colours to stay distinguishable from the daily averages. Prior-day levels draw as price lines, which span the chart and label the axis instead of relying on bar-index interpolation. VWAP re-prices every minute while a daily average carries hundreds of points and changes once a session, so broadcasting the whole level set on the VWAP cadence would have pushed the entire history every minute. Levels now go out as a delta that clients merge by id. Adding the levels then exposed two defects that had been invisible while nothing could cluster: - Cluster identity was sha1(side + round(center / tolerance)), and tolerance derives from ATR, so it changed every bar. The same zone was continually issued a new id, never matched the cooldown table, and the cooldown did nothing. Identity is now the set of converging levels. - Alert suppression keyed on that identity, so a level drifting in or out of a group read as a new zone. It now suppresses by proximity: two zones within an ATR are the same zone, and the strongest is the one reported. Over six replayed sessions at threshold 28 that is 247 alerts, then 54, then 40; raising the cooldown to 4h — which only affects repeats of the same area, never a genuinely new zone — gives 17 total with a worst session of 9. calibrate_alerts.py now sweeps threshold and cooldown together in one pass, since the threshold turns out to be quantised and nearly useless as a control. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
96 lines
6.5 KiB
HTML
96 lines
6.5 KiB
HTML
<!doctype html>
|
||
<html lang="en">
|
||
<head>
|
||
<meta charset="utf-8">
|
||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||
<title>/ES Confluence</title>
|
||
<link rel="stylesheet" href="/static/style.css">
|
||
<script src="https://unpkg.com/vue@3/dist/vue.global.prod.js"></script>
|
||
<script src="https://unpkg.com/lightweight-charts@5.2.0/dist/lightweight-charts.standalone.production.js"></script>
|
||
</head>
|
||
<body>
|
||
<div id="app">
|
||
<header>
|
||
<div><span class="eyebrow">CME FUTURES</span><h1>/ES <strong>CONFLUENCE</strong></h1></div>
|
||
<div class="status" :class="status.stream"><i></i>{{ status.stream }}<span v-if="status.delay_minutes"> ({{ status.delay_minutes }}min delay)</span> · {{ status.source || 'source' }}</div>
|
||
</header>
|
||
<main>
|
||
<section class="chart-shell">
|
||
<div class="chart-head">
|
||
<div><span class="symbol">{{ status.symbol || 'ES=F' }}</span><span class="price">{{ price == null ? '—' : price.toFixed(2) }}</span></div>
|
||
<div class="timeframes"><button v-for="tf in timeframes" :key="tf" :class="{active: timeframe === tf}" @click="selectTimeframe(tf)">{{ tf }}</button></div>
|
||
</div>
|
||
<div class="drawing-tools">
|
||
<button :class="{active: drawMode}" @click="toggleDraw">Trendline</button>
|
||
<input class="line-name" v-model.trim="drawName" placeholder="Line name" aria-label="Trendline name">
|
||
<input type="color" v-model="drawColor" aria-label="New trendline color">
|
||
<select v-model.number="drawWidth" aria-label="New trendline width"><option v-for="width in [1,2,3,4]" :value="width">{{ width }}px</option></select>
|
||
<select v-model="drawSide" aria-label="Line side"><option value="support">Support</option><option value="resistance">Resistance</option></select>
|
||
<label><input type="checkbox" v-model="snap">Snap</label>
|
||
<button @click="deleteSelected" :disabled="!hasLineSelection">Delete</button>
|
||
<span v-if="drawMode">{{ drawPoints.length ? 'Place second point' : 'Place first point' }}</span>
|
||
<span v-else-if="selectedLine">Line selected</span>
|
||
</div>
|
||
<div id="chart"></div>
|
||
<div class="statusbar">
|
||
<span>FEED <b>{{ status.stream }}</b></span>
|
||
<span>LAST BAR <b>{{ barAge }}</b></span>
|
||
<span>HELD <b>{{ status.bars_held?.[timeframe] || 0 }} {{ timeframe }}</b></span>
|
||
</div>
|
||
</section>
|
||
<aside>
|
||
<h2>Layers</h2>
|
||
<div class="layer-group">
|
||
<label class="master"><input type="checkbox" :checked="allEnabled('1d')" @change="toggleGroup('1d', $event.target.checked)"><span class="swatch tf-1d"></span>Daily MAs</label>
|
||
<div class="periods"><label v-for="period in [10,20,50,100,200]" :key="period"><input type="checkbox" :value="period" v-model="prefs.enabled.ma['1d']">{{ period }}</label></div>
|
||
</div>
|
||
<div class="layer-group optional">
|
||
<label><input type="checkbox" :checked="allEnabled('1h')" @change="toggleGroup('1h', $event.target.checked)"><span class="swatch tf-1h"></span>1h MAs</label>
|
||
</div>
|
||
<div class="layer-group">
|
||
<label><input type="checkbox" v-model="prefs.enabled.horizontal"><span class="swatch horizontal"></span>Prior day H/L/C</label>
|
||
<label><input type="checkbox" v-model="prefs.enabled.vwap"><span class="swatch vwap"></span>Session VWAP</label>
|
||
</div>
|
||
<div class="layer-group">
|
||
<label><input type="checkbox" v-model="prefs.enabled.manual"><span class="swatch manual"></span>Manual lines</label>
|
||
<label class="disabled"><input type="checkbox" disabled>Auto trendlines</label>
|
||
</div>
|
||
<label class="score-hidden"><input type="checkbox" v-model="prefs.hidden_levels_score">Hidden levels still count toward confluence</label>
|
||
<details class="sidebar-section" open>
|
||
<summary>Trendlines ({{ manualLines.length }})</summary>
|
||
<div class="trendline-actions" v-if="manualLines.length">
|
||
<button @click="toggleSelectAll">{{ allManualSelected ? 'Clear' : 'Select all' }}</button>
|
||
<button @click="deleteSelectedLines" :disabled="!selectedLines.length">Delete selected ({{ selectedLines.length }})</button>
|
||
</div>
|
||
<div v-if="!manualLines.length" class="empty">No manual trendlines.</div>
|
||
<div v-for="line in manualLines" :key="line.id" class="trendline-row" :class="{selected: selectedLines.includes(line.id)}" @click="toggleLineSelection(line.id)">
|
||
<input class="line-select" type="checkbox" :checked="selectedLines.includes(line.id)" :aria-label="`Select trendline ${line.number}`" @click.stop @change="toggleLineSelection(line.id)">
|
||
<input :value="line.label" aria-label="Trendline name" @click.stop @change="renameLine(line, $event.target.value)">
|
||
<span>#{{ line.number }} · {{ line.tf }} · {{ line.side }}</span>
|
||
<button @click.stop="deleteLine(line.id)" aria-label="Delete trendline">Delete</button>
|
||
<div class="line-style-controls" @click.stop>
|
||
<input type="color" :value="line.color || '#65b7cf'" aria-label="Trendline color" @change="updateLineStyle(line, { color: $event.target.value })">
|
||
<select :value="line.line_width || 2" aria-label="Trendline width" @change="updateLineStyle(line, { line_width: Number($event.target.value) })"><option v-for="width in [1,2,3,4]" :value="width">{{ width }}px</option></select>
|
||
</div>
|
||
</div>
|
||
</details>
|
||
<details class="sidebar-section">
|
||
<summary>Confluence zones</summary>
|
||
<div v-if="!clusters.length" class="empty">No active zones near current structure.</div>
|
||
<div v-for="cluster in clusters" :key="cluster.id" class="cluster" :class="cluster.side">
|
||
<div class="cluster-top"><b>{{ cluster.side }}</b><strong>{{ cluster.score.toFixed(1) }}</strong></div>
|
||
<div class="zone">{{ cluster.low.toFixed(2) }} – {{ cluster.high.toFixed(2) }}</div>
|
||
<div class="members">{{ cluster.members.map(member => member.label).join(' · ') }}</div>
|
||
<div class="distance">{{ cluster.distance > 0 ? '+' : '' }}{{ cluster.distance.toFixed(2) }} pts</div>
|
||
</div>
|
||
</details>
|
||
<h2>Alert log</h2>
|
||
<div v-if="!alerts.length" class="empty">No alerts fired.</div>
|
||
<div v-for="alert in alerts" :key="alert.key" class="alert-entry"><time>{{ alert.at }}</time>{{ alert.message }}</div>
|
||
</aside>
|
||
</main>
|
||
</div>
|
||
<script src="/static/chart.js"></script>
|
||
<script src="/static/app.js"></script>
|
||
</body>
|
||
</html>
|