190 lines
13 KiB
HTML
190 lines
13 KiB
HTML
<!doctype html>
|
||
<html lang="en">
|
||
<head>
|
||
<meta charset="utf-8">
|
||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||
<title>/ESsence</title>
|
||
<link rel="stylesheet" href="/static/style.css">
|
||
<!-- Font Awesome Free 7.3.1, from unpkg like the other two dependencies.
|
||
Pinned deliberately: an unpinned icon set is a silent redesign on someone
|
||
else's release. cdnjs 404s on this path — it does not carry 7.3.1. -->
|
||
<link rel="stylesheet" href="https://unpkg.com/@fortawesome/fontawesome-free@7.3.1/css/all.min.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>
|
||
<h1><strong>/ES</strong>sence</h1>
|
||
<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 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>
|
||
<span v-if="armedTool === 'trendline'" class="arm-hint">Drag on the chart from one point to the other</span>
|
||
<span v-else-if="armedTool === 'level'" class="arm-hint">Click or drag on the chart to set the price</span>
|
||
<span v-else-if="selectedLine" class="arm-hint">Line selected — Delete removes it</span>
|
||
<span class="data-freshness">{{ dataReceiptVerb }} <b>{{ dataAge }}</b></span>
|
||
<span class="app-meta" :title="deploymentTitle">DEPLOY <b>{{ deployMeta }}</b></span>
|
||
</div>
|
||
</section>
|
||
<aside>
|
||
<details class="sidebar-section">
|
||
<summary>Layers</summary>
|
||
<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">
|
||
<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 layer-inline">
|
||
<label><input type="checkbox" v-model="prefs.enabled.manual"><span class="swatch manual"></span>Manual lines</label>
|
||
<label class="disabled" title="Automatic trendline detection is not built yet">(<input type="checkbox" disabled>auto)</label>
|
||
</div>
|
||
<label class="score-hidden"><input type="checkbox" v-model="prefs.hidden_levels_score">Hidden levels still count toward confluence</label>
|
||
</details>
|
||
<details class="sidebar-section tools-section" open>
|
||
<summary>Tools</summary>
|
||
<div class="tool" :class="{armed: armedTool === 'trendline'}">
|
||
<button class="tool-head" @click="armTool('trendline')" :aria-pressed="armedTool === 'trendline'">
|
||
<span class="tool-glyph"><i class="fa-solid fa-arrow-trend-up"></i></span>Trendline
|
||
<span class="tool-state">{{ armedTool === 'trendline' ? 'drag on chart' : '' }}</span>
|
||
</button>
|
||
<div class="tool-body" v-show="armedTool === 'trendline'">
|
||
<label>Label<input v-model.trim="drawName" placeholder="optional" aria-label="Trendline label"></label>
|
||
<div class="row">
|
||
<label>Colour<input type="color" v-model="drawColor" aria-label="Trendline colour"></label>
|
||
<label>Width<select v-model.number="drawWidth" aria-label="Trendline width"><option v-for="width in [1,2,3,4]" :value="width">{{ width }}px</option></select></label>
|
||
</div>
|
||
<div class="row">
|
||
<!-- Side is inferred from the extreme you snap to, so it only has
|
||
anything to say when snapping is off. -->
|
||
<label v-if="!snap">Side<select v-model="drawSide" aria-label="Trendline side"><option value="support">Support</option><option value="resistance">Resistance</option></select></label>
|
||
<span v-else class="side-auto" title="A high is resistance, a low is support">Side <b>auto</b></span>
|
||
<label class="check"><input type="checkbox" v-model="snap">Snap to highs/lows</label>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<div class="tool" :class="{armed: armedTool === 'level'}">
|
||
<button class="tool-head" @click="armTool('level')" :aria-pressed="armedTool === 'level'">
|
||
<span class="tool-glyph"><i class="fa-solid fa-minus"></i></span>Price level
|
||
<span class="tool-state">{{ armedTool === 'level' ? 'click on chart' : '' }}</span>
|
||
</button>
|
||
<div class="tool-body" v-show="armedTool === 'level'">
|
||
<label>Label<input v-model.trim="alertNote" placeholder="optional" aria-label="Level label"></label>
|
||
<div class="row">
|
||
<label>Colour<input type="color" v-model="levelColor" aria-label="Level colour"></label>
|
||
<label>Width<select v-model.number="levelWidth" aria-label="Level width"><option v-for="width in [1,2,3,4]" :value="width">{{ width }}px</option></select></label>
|
||
</div>
|
||
<form class="row price-row" @submit.prevent="addPriceAlert">
|
||
<label>Price<input type="number" step="0.25" v-model.number="alertPrice" :placeholder="price == null ? '0.00' : price.toFixed(2)" aria-label="Level price"></label>
|
||
<button type="submit" :disabled="!alertPrice">Add</button>
|
||
</form>
|
||
<p class="hint">Drag on the chart, or type an exact price. Alerts whenever price reaches it, whatever the confluence score.</p>
|
||
</div>
|
||
</div>
|
||
<div class="tool" :class="{armed: armedTool === 'comment'}">
|
||
<button class="tool-head" @click="armTool('comment')" :aria-pressed="armedTool === 'comment'">
|
||
<span class="tool-glyph"><i class="fa-solid fa-note-sticky"></i></span>Comment
|
||
<span class="tool-state">{{ armedTool === 'comment' ? 'click on chart' : '' }}</span>
|
||
</button>
|
||
<div class="tool-body" v-show="armedTool === 'comment'">
|
||
<label>Text<textarea v-model.trim="commentText" rows="2" placeholder="What happened here?" aria-label="Comment text"></textarea></label>
|
||
<label class="check"><input type="checkbox" v-model="commentFloat">Float (stay on screen)</label>
|
||
<p class="hint">Type the text, then click the chart to place it. Pinned comments sit on that bar; floating ones stay put as you scroll and can be dragged. Click one to collapse it.</p>
|
||
</div>
|
||
</div>
|
||
</details>
|
||
<details class="sidebar-section" open>
|
||
<summary>Drawings ({{ filteredDrawings.length }}<span v-if="filteredDrawings.length !== drawings.length"> of {{ drawings.length }}</span>)</summary>
|
||
<div class="drawing-filters">
|
||
<select v-model="drawingKind" aria-label="Filter drawings by type">
|
||
<option value="all">All types</option>
|
||
<option value="trendline">Trendlines</option>
|
||
<option value="level">Levels</option>
|
||
<option value="comment">Comments</option>
|
||
</select>
|
||
<input v-model="drawingFilter" placeholder="Filter by text…" aria-label="Filter drawings by text">
|
||
</div>
|
||
<div class="trendline-actions" v-if="drawings.length">
|
||
<button @click="toggleSelectAll">{{ allManualSelected ? 'Clear' : 'Select all' }}</button>
|
||
<button @click="deleteFilteredDrawings" :disabled="!filteredDrawings.length"
|
||
:title="`Delete the ${filteredDrawings.length} drawing(s) matching this filter`">
|
||
Delete shown ({{ filteredDrawings.length }})
|
||
</button>
|
||
</div>
|
||
<div v-if="!drawings.length" class="empty">Nothing drawn yet.</div>
|
||
<div v-else-if="!filteredDrawings.length" class="empty">No drawings match this filter.</div>
|
||
|
||
<div v-for="item in filteredDrawings" :key="item.id" class="trendline-row"
|
||
:class="{selected: selectedLines.includes(item.id)}"
|
||
@click="item.kind !== 'comment' && toggleLineSelection(item.id)">
|
||
<input v-if="item.kind !== 'comment'" class="line-select" type="checkbox"
|
||
:checked="selectedLines.includes(item.id)" :aria-label="`Select drawing ${item.number}`"
|
||
@click.stop @change="toggleLineSelection(item.id)">
|
||
<span v-else class="line-select drawing-icon"><i class="fa-solid fa-note-sticky"></i></span>
|
||
|
||
<input v-if="item.kind !== 'comment'" :value="item.line.label" aria-label="Drawing name"
|
||
@click.stop @change="renameLine(item.line, $event.target.value)">
|
||
<input v-else :value="item.comment.note" aria-label="Comment text"
|
||
@click.stop @change="renameLine(item.comment, $event.target.value)">
|
||
|
||
<span v-if="item.kind === 'comment'">#{{ item.number }} · comment · {{ item.comment.pinned ? 'pinned' : 'floating' }}</span>
|
||
<span v-else-if="item.kind === 'level'">#{{ item.number }} · level {{ item.line.anchor_p.toFixed(2) }}</span>
|
||
<span v-else>#{{ item.number }} · {{ item.line.tf }} · {{ item.line.side }}</span>
|
||
|
||
<label v-if="item.kind === 'comment'" class="armed-toggle" @click.stop
|
||
:title="item.comment.pinned ? 'Pinned to its bar — scrolls with the chart' : 'Floating — always on screen'">
|
||
<input type="checkbox" :checked="!item.comment.pinned" @change="togglePinned(item.comment)">
|
||
{{ item.comment.pinned ? 'pinned' : 'float' }}
|
||
</label>
|
||
<label v-else class="armed-toggle" :class="{off: !item.line.armed}" @click.stop
|
||
:title="item.line.armed ? 'Armed — will alert once, then disarm' : 'Tripped — re-arm to alert again'">
|
||
<input type="checkbox" :checked="item.line.armed" @change="setArmed(item.line, $event.target.checked)">
|
||
{{ item.line.armed ? 'armed' : 'tripped' }}
|
||
</label>
|
||
|
||
<button @click.stop="deleteDrawing(item)" aria-label="Delete drawing">Delete</button>
|
||
|
||
<div class="line-style-controls" @click.stop v-if="item.kind !== 'comment'">
|
||
<input type="color" :value="item.line.color || '#65b7cf'" aria-label="Drawing color" @change="updateLineStyle(item.line, { color: $event.target.value })">
|
||
<select :value="item.line.line_width || 2" aria-label="Drawing width" @change="updateLineStyle(item.line, { line_width: Number($event.target.value) })"><option v-for="width in [1,2,3,4]" :value="width">{{ width }}px</option></select>
|
||
</div>
|
||
<div class="line-style-controls" @click.stop v-else>
|
||
<input type="color" :value="item.comment.color || '#c8992f'" aria-label="Comment colour" @change="updateLineStyle(item.comment, { color: $event.target.value })">
|
||
<button class="collapse-toggle" @click.stop="toggleComment(item.comment)">{{ item.comment.collapsed ? 'expand' : 'collapse' }}</button>
|
||
</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>
|
||
<details class="sidebar-section">
|
||
<summary>Alert log ({{ alerts.length }})</summary>
|
||
<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>
|
||
</details>
|
||
</aside>
|
||
</main>
|
||
</div>
|
||
<script src="/static/chart.js"></script>
|
||
<script src="/static/app.js"></script>
|
||
</body>
|
||
</html>
|