Improve local chart development
This commit is contained in:
parent
49cd06089f
commit
f7d0ffce8a
7 changed files with 73 additions and 19 deletions
1
.gitignore
vendored
1
.gitignore
vendored
|
|
@ -4,3 +4,4 @@ __pycache__/
|
|||
.env
|
||||
.schwab_token.json
|
||||
data/manual_lines.json
|
||||
artifacts/playwright/
|
||||
|
|
|
|||
|
|
@ -14,9 +14,9 @@ code; it records decisions and verified API facts that are expensive to rediscov
|
|||
docker compose up --build
|
||||
```
|
||||
|
||||
Then open <http://localhost:8000>. Override the host port with
|
||||
`PORT=8010 docker compose up` if 8000 is busy — on the VPS itself it always is,
|
||||
that's the Coolify UI. The source tree is bind-mounted and uvicorn
|
||||
Then open <http://localhost:8010>. Override the host port with, for example,
|
||||
`PORT=8020 docker compose up`. The port binds to all host interfaces, so another
|
||||
machine can connect at `http://HOST_IP:8010`. The source tree is bind-mounted and uvicorn
|
||||
runs with `--reload`, so edits to `main.py` or `static/` take effect without a
|
||||
rebuild. Rebuild only when `requirements.txt` changes.
|
||||
|
||||
|
|
|
|||
|
|
@ -6,12 +6,20 @@ services:
|
|||
context: .
|
||||
dockerfile: Dockerfile.dev
|
||||
ports:
|
||||
# Host port is overridable: `PORT=8010 docker compose up`. On the VPS
|
||||
# itself 8000 is already taken by the Coolify UI.
|
||||
- "${PORT:-8000}:8000"
|
||||
# Host port is overridable. Port 8000 is already used by Coolify on the VPS.
|
||||
- "0.0.0.0:${PORT:-8010}:8000"
|
||||
volumes:
|
||||
# Bind-mount the source so --reload picks up edits without a rebuild.
|
||||
- .:/app
|
||||
environment:
|
||||
- PYTHONDONTWRITEBYTECODE=1
|
||||
- PYTHONUNBUFFERED=1
|
||||
|
||||
playwright:
|
||||
image: mcr.microsoft.com/playwright:v1.55.0-noble
|
||||
command: sleep infinity
|
||||
depends_on:
|
||||
- api
|
||||
ipc: host
|
||||
volumes:
|
||||
- ./artifacts/playwright:/artifacts
|
||||
|
|
|
|||
|
|
@ -90,6 +90,12 @@ createApp({
|
|||
drawMode.value = !drawMode.value;
|
||||
drawPoints.value = [];
|
||||
selectedLine.value = null;
|
||||
chartApi.clearLinePreview();
|
||||
}
|
||||
|
||||
function handleChartMove(param) {
|
||||
if (!drawMode.value || drawPoints.value.length !== 1) return;
|
||||
chartApi.setLinePreview(drawPoints.value[0], chartApi.pointFromClick(param, snap.value), timeframe.value);
|
||||
}
|
||||
|
||||
async function handleChartClick(param) {
|
||||
|
|
@ -101,6 +107,7 @@ createApp({
|
|||
if (point.snappedSide) drawSide.value = point.snappedSide;
|
||||
drawPoints.value.push(point);
|
||||
if (drawPoints.value.length < 2) return;
|
||||
chartApi.clearLinePreview();
|
||||
const [start, end] = drawPoints.value.sort((a, b) => a.t - b.t);
|
||||
if (start.t === end.t) { drawPoints.value = []; return; }
|
||||
const temporaryId = `tmp_${Date.now()}`;
|
||||
|
|
@ -191,6 +198,7 @@ createApp({
|
|||
chartApi = new ConfluenceChart();
|
||||
chartApi.create(document.getElementById('chart'));
|
||||
chartApi.setClickHandler(handleChartClick);
|
||||
chartApi.setMoveHandler(handleChartMove);
|
||||
window.addEventListener('keydown', handleKeydown);
|
||||
refreshStatus();
|
||||
connect();
|
||||
|
|
|
|||
|
|
@ -4,9 +4,11 @@ class ConfluenceChart {
|
|||
this.candles = null;
|
||||
this.resizeObserver = null;
|
||||
this.levelSeries = new Map();
|
||||
this.previewSeries = null;
|
||||
this.bars = [];
|
||||
this.levels = [];
|
||||
this.onChartClick = null;
|
||||
this.onChartMove = null;
|
||||
}
|
||||
|
||||
create(el) {
|
||||
|
|
@ -24,8 +26,9 @@ class ConfluenceChart {
|
|||
rightPriceScale: { borderVisible: false },
|
||||
});
|
||||
this.candles = this.chart.addSeries(LightweightCharts.CandlestickSeries, {
|
||||
upColor: '#3fb984', downColor: '#e45757', borderVisible: false,
|
||||
wickUpColor: '#3fb984', wickDownColor: '#e45757',
|
||||
upColor: '#27825c', downColor: '#bd4545', borderVisible: true,
|
||||
borderUpColor: '#1d6849', borderDownColor: '#963737',
|
||||
wickUpColor: '#1d6849', wickDownColor: '#963737',
|
||||
});
|
||||
this.resizeObserver = new ResizeObserver(() => {
|
||||
this.chart.applyOptions({ width: el.clientWidth, height: el.clientHeight });
|
||||
|
|
@ -34,12 +37,18 @@ class ConfluenceChart {
|
|||
this.chart.subscribeClick(param => {
|
||||
if (param.point && param.time && this.onChartClick) this.onChartClick(param);
|
||||
});
|
||||
this.chart.subscribeCrosshairMove(param => {
|
||||
if (param.point && param.time && this.onChartMove) this.onChartMove(param);
|
||||
});
|
||||
}
|
||||
|
||||
setBars(bars) {
|
||||
this.bars = bars;
|
||||
this.candles.setData(bars.map(this.toCandle));
|
||||
this.chart.timeScale().fitContent();
|
||||
this.chart.timeScale().setVisibleLogicalRange({
|
||||
from: Math.max(0, bars.length - 160),
|
||||
to: bars.length + 5,
|
||||
});
|
||||
}
|
||||
|
||||
updateBar(bar) {
|
||||
|
|
@ -95,6 +104,32 @@ class ConfluenceChart {
|
|||
|
||||
setClickHandler(handler) { this.onChartClick = handler; }
|
||||
|
||||
setMoveHandler(handler) { this.onChartMove = handler; }
|
||||
|
||||
setLinePreview(start, end, tf) {
|
||||
if (start.t === end.t) return;
|
||||
if (!this.previewSeries) {
|
||||
this.previewSeries = this.chart.addSeries(LightweightCharts.LineSeries, {
|
||||
color: ConfluenceChart.tfColors[tf],
|
||||
lineWidth: 2,
|
||||
lineStyle: LightweightCharts.LineStyle.Dashed,
|
||||
priceLineVisible: false,
|
||||
lastValueVisible: false,
|
||||
crosshairMarkerVisible: false,
|
||||
});
|
||||
}
|
||||
this.previewSeries.applyOptions({ color: ConfluenceChart.tfColors[tf] });
|
||||
this.previewSeries.setData([start, end]
|
||||
.sort((a, b) => a.t - b.t)
|
||||
.map(point => ({ time: point.t, value: point.p })));
|
||||
}
|
||||
|
||||
clearLinePreview() {
|
||||
if (!this.previewSeries) return;
|
||||
this.chart.removeSeries(this.previewSeries);
|
||||
this.previewSeries = null;
|
||||
}
|
||||
|
||||
pointFromClick(param, snap) {
|
||||
const rawPrice = this.candles.coordinateToPrice(param.point.y);
|
||||
let point = { t: Number(param.time), p: rawPrice, snappedSide: null };
|
||||
|
|
|
|||
|
|
@ -50,7 +50,8 @@
|
|||
<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>
|
||||
<h2>Confluence zones</h2>
|
||||
<details class="sidebar-section" open>
|
||||
<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>
|
||||
|
|
@ -58,6 +59,7 @@
|
|||
<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.at" class="alert-entry"><time>{{ alert.at }}</time>{{ alert.message }}</div>
|
||||
|
|
|
|||
|
|
@ -1,5 +1,4 @@
|
|||
:root { color-scheme: dark; --bg:#090d12; --panel:#10161d; --chart-bg:#0c1117; --fg:#e8edf3; --muted:#82909f; --line:#202b36; --accent:#efb643; --green:#3fb984; --red:#e45757; }
|
||||
@media (prefers-color-scheme: light) { :root { color-scheme:light; --bg:#eef1f3; --panel:#fff; --chart-bg:#fff; --fg:#17202a; --muted:#66717d; --line:#dce2e7; } }
|
||||
:root { color-scheme:light; --bg:#e8dfcf; --panel:#f7f1e6; --chart-bg:#fbf7ef; --fg:#2c2924; --muted:#746c60; --line:#d2c5b2; --accent:#b7771d; --green:#27825c; --red:#bd4545; }
|
||||
* { box-sizing:border-box; }
|
||||
body { margin:0; background:var(--bg); color:var(--fg); font:14px/1.45 "IBM Plex Mono", "SFMono-Regular", Consolas, monospace; }
|
||||
#app { min-height:100vh; padding:18px; }
|
||||
|
|
@ -17,6 +16,7 @@ button { border:1px solid var(--line); background:transparent; color:var(--muted
|
|||
#chart { height:calc(100vh - 190px); min-height:420px; }
|
||||
.statusbar { min-height:34px; display:flex; align-items:center; gap:24px; padding:6px 13px; border-top:1px solid var(--line); color:var(--muted); font-size:10px; }.statusbar b { color:var(--fg); text-transform:uppercase; }
|
||||
aside { padding:16px; }h2 { margin:0 0 12px; color:var(--muted); font-size:11px; text-transform:uppercase; letter-spacing:1.3px; }h2:not(:first-child) { margin-top:30px; }.empty { border-left:2px solid var(--line); padding:10px 12px; color:var(--muted); font-size:11px; }
|
||||
.sidebar-section { margin-top:30px; }.sidebar-section summary { margin-bottom:12px; color:var(--muted); font-size:11px; text-transform:uppercase; letter-spacing:1.3px; cursor:pointer; user-select:none; }.sidebar-section:not([open]) summary { margin-bottom:0; }
|
||||
.layer-group { padding:9px 0; border-bottom:1px solid var(--line); display:grid; gap:7px; }.layer-group label,.score-hidden { display:flex; align-items:center; gap:7px; font-size:11px; cursor:pointer; }.layer-group input,.score-hidden input { accent-color:var(--accent); }.periods { display:flex; flex-wrap:wrap; gap:10px; padding-left:22px; }.periods label { color:var(--muted); }.swatch { width:13px; height:3px; display:inline-block; background:var(--muted); }.tf-1d { background:#d96073; }.tf-4h { background:#ec7b42; }.tf-1h { background:#efb643; }.manual { background:#65b7cf; }.optional { color:var(--muted); }.disabled { opacity:.45; }.score-hidden { margin-top:11px; color:var(--muted); line-height:1.25; }
|
||||
.cluster { margin:8px 0; padding:10px; border:1px solid var(--line); border-left:3px solid var(--green); background:var(--chart-bg); }.cluster.resistance { border-left-color:var(--red); }.cluster-top { display:flex; justify-content:space-between; text-transform:uppercase; font-size:10px; }.cluster-top strong { color:var(--accent); font-size:16px; }.zone { margin:4px 0; font-size:15px; }.members,.distance { color:var(--muted); font-size:9px; }.distance { margin-top:5px; }.alert-entry { white-space:pre-line; margin:8px 0; padding:9px; background:color-mix(in srgb,var(--accent) 8%,transparent); font-size:10px; }.alert-entry time { display:block; color:var(--accent); margin-bottom:4px; }
|
||||
@media (max-width:850px) { #app { padding:10px; }main { grid-template-columns:1fr; }#chart { height:55vh; min-height:360px; }aside { min-height:180px; }header { height:54px; }.chart-head { align-items:flex-start; flex-direction:column; }.timeframes { justify-content:flex-start; }.timeframes button { padding:5px 8px; } }
|
||||
|
|
|
|||
Loading…
Reference in a new issue