dashboard: mark neutral votes and no-pattern rows on the indicator chart
A NEUTRAL vote (value 0) and an absent divergence/candlestick entry both drew no bar, so RSI, Div and Cndl looked missing on the live page. The engine computes them on every scan; divergence and candlestick only exist when a pattern is found (by design, "no opinion"). The chart now draws a grey dot on the zero line for a neutral vote and a faint "no pattern" tag for an absent pattern indicator; rows hover as a whole. Display-only, signal code and books untouched. Suite 554/554.
This commit is contained in:
+45
-5
@@ -28,6 +28,10 @@ const IND_NAME = {
|
|||||||
adx: "ADX", roc: "Rate of change", keltner: "Keltner channel",
|
adx: "ADX", roc: "Rate of change", keltner: "Keltner channel",
|
||||||
divergence: "Divergence", candlestick: "Candlestick pattern"
|
divergence: "Divergence", candlestick: "Candlestick pattern"
|
||||||
};
|
};
|
||||||
|
// Divergence and candlestick only exist on a scan that found a pattern
|
||||||
|
// (engine.py); for them an absent entry means "no pattern", not a failure.
|
||||||
|
const PATTERN_IND = new Set(["divergence", "candlestick"]);
|
||||||
|
const absentWord = name => PATTERN_IND.has(name) ? "no pattern" : "no reading";
|
||||||
const VOTE = { STRONG_BUY: 2, BUY: 1, NEUTRAL: 0, SELL: -1, STRONG_SELL: -2 };
|
const VOTE = { STRONG_BUY: 2, BUY: 1, NEUTRAL: 0, SELL: -1, STRONG_SELL: -2 };
|
||||||
const SIG_ICON = {
|
const SIG_ICON = {
|
||||||
STRONG_BUY: "fa-angles-up", BUY: "fa-arrow-trend-up", NEUTRAL: "fa-arrows-left-right",
|
STRONG_BUY: "fa-angles-up", BUY: "fa-arrow-trend-up", NEUTRAL: "fa-arrows-left-right",
|
||||||
@@ -275,11 +279,41 @@ function indChart(tf) {
|
|||||||
barThickness: 7
|
barThickness: 7
|
||||||
}]
|
}]
|
||||||
},
|
},
|
||||||
|
// A neutral vote and a missing reading both have no bar; this marks
|
||||||
|
// them on the zero line (grey dot = neutral, faint tag = nothing found).
|
||||||
|
plugins: [{
|
||||||
|
id: "indMarkers",
|
||||||
|
afterDatasetsDraw(chart) {
|
||||||
|
const cells = lastCells[tf];
|
||||||
|
if (!cells) return;
|
||||||
|
const { ctx, scales } = chart;
|
||||||
|
const x0 = scales.x.getPixelForValue(0);
|
||||||
|
ctx.save();
|
||||||
|
cells.forEach((cell, i) => {
|
||||||
|
const y = scales.y.getPixelForValue(i);
|
||||||
|
if (cell.present && cell.v === 0) {
|
||||||
|
ctx.fillStyle = C.flat;
|
||||||
|
ctx.beginPath();
|
||||||
|
ctx.arc(x0, y, 3, 0, Math.PI * 2);
|
||||||
|
ctx.fill();
|
||||||
|
} else if (!cell.present) {
|
||||||
|
ctx.fillStyle = "rgba(255,255,255,.32)";
|
||||||
|
ctx.font = '9px "IBM Plex Mono", monospace';
|
||||||
|
ctx.textAlign = "left";
|
||||||
|
ctx.textBaseline = "middle";
|
||||||
|
ctx.fillText(absentWord(IND_ORDER[i]), x0 + 6, y);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
ctx.restore();
|
||||||
|
}
|
||||||
|
}],
|
||||||
options: {
|
options: {
|
||||||
indexAxis: "y",
|
indexAxis: "y",
|
||||||
responsive: true,
|
responsive: true,
|
||||||
maintainAspectRatio: false,
|
maintainAspectRatio: false,
|
||||||
layout: { padding: { right: 4 } },
|
layout: { padding: { right: 4 } },
|
||||||
|
// Hover the whole row: a zero-length bar cannot be intersected.
|
||||||
|
interaction: { mode: "index", axis: "y", intersect: false },
|
||||||
scales: {
|
scales: {
|
||||||
x: {
|
x: {
|
||||||
min: -2, max: 2,
|
min: -2, max: 2,
|
||||||
@@ -291,8 +325,8 @@ function indChart(tf) {
|
|||||||
grid: { display: false },
|
grid: { display: false },
|
||||||
border: { display: false },
|
border: { display: false },
|
||||||
ticks: {
|
ticks: {
|
||||||
// An indicator with no reading is not a neutral vote: both draw
|
// An indicator with no reading is not a neutral vote: the label
|
||||||
// no bar, so the dimmed label is what tells them apart.
|
// dims and the indMarkers plugin tags the row instead of a dot.
|
||||||
color: ctx => (lastCells[tf] && lastCells[tf][ctx.index] && !lastCells[tf][ctx.index].present)
|
color: ctx => (lastCells[tf] && lastCells[tf][ctx.index] && !lastCells[tf][ctx.index].present)
|
||||||
? "rgba(255,255,255,.32)" : C.axis,
|
? "rgba(255,255,255,.32)" : C.axis,
|
||||||
font: { family: '"IBM Plex Mono", monospace', size: 10 },
|
font: { family: '"IBM Plex Mono", monospace', size: 10 },
|
||||||
@@ -307,7 +341,10 @@ function indChart(tf) {
|
|||||||
title: items => IND_NAME[IND_ORDER[items[0].dataIndex]],
|
title: items => IND_NAME[IND_ORDER[items[0].dataIndex]],
|
||||||
label: item => {
|
label: item => {
|
||||||
const cell = (lastCells[tf] || indCells(currentReport(tf)))[item.dataIndex];
|
const cell = (lastCells[tf] || indCells(currentReport(tf)))[item.dataIndex];
|
||||||
if (!cell || !cell.present) return "No reading on this scan";
|
if (!cell || !cell.present) {
|
||||||
|
const w = absentWord(IND_ORDER[item.dataIndex]);
|
||||||
|
return `${w[0].toUpperCase()}${w.slice(1)} on this scan`;
|
||||||
|
}
|
||||||
return `${sigWord(cell.sig)}, confidence ${Math.round(cell.conf)}%`;
|
return `${sigWord(cell.sig)}, confidence ${Math.round(cell.conf)}%`;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -687,9 +724,12 @@ function indTable(src) {
|
|||||||
const i = src && src[name];
|
const i = src && src[name];
|
||||||
const k = `<td class="ind-k">${IND_SHORT[name]}</td>`;
|
const k = `<td class="ind-k">${IND_SHORT[name]}</td>`;
|
||||||
if (!i) {
|
if (!i) {
|
||||||
return `<tr>${k}<td class="flat">no reading</td><td class="num text-end">—</td>
|
const why = PATTERN_IND.has(name)
|
||||||
|
? "no pattern was found on this scan"
|
||||||
|
: "this indicator had nothing to say on this scan";
|
||||||
|
return `<tr>${k}<td class="flat">${absentWord(name)}</td><td class="num text-end">—</td>
|
||||||
<td class="num text-end">—</td><td class="num text-end">—</td>
|
<td class="num text-end">—</td><td class="num text-end">—</td>
|
||||||
<td class="flat">this indicator had nothing to say on this scan</td></tr>`;
|
<td class="flat">${why}</td></tr>`;
|
||||||
}
|
}
|
||||||
const w = i.weight == null ? null : +i.weight;
|
const w = i.weight == null ? null : +i.weight;
|
||||||
return `<tr>${k}
|
return `<tr>${k}
|
||||||
|
|||||||
+13
@@ -5537,6 +5537,19 @@ def _():
|
|||||||
assert "scheduleBoundaryRefresh" in js and "localStorage" in js
|
assert "scheduleBoundaryRefresh" in js and "localStorage" in js
|
||||||
|
|
||||||
|
|
||||||
|
@test("dashboard: indicator chart marks neutral votes and no-pattern rows")
|
||||||
|
def _():
|
||||||
|
# A NEUTRAL vote (value 0) and an absent divergence/candlestick entry
|
||||||
|
# both draw no bar; the chart must mark them so they don't read as
|
||||||
|
# missing data.
|
||||||
|
with open(os.path.join(os.path.dirname(__file__), "static", "dashboard.js"),
|
||||||
|
encoding="utf-8") as fh:
|
||||||
|
js = fh.read()
|
||||||
|
assert 'id: "indMarkers"' in js and "afterDatasetsDraw" in js
|
||||||
|
assert 'PATTERN_IND = new Set(["divergence", "candlestick"])' in js
|
||||||
|
assert '"no pattern"' in js and 'intersect: false' in js
|
||||||
|
|
||||||
|
|
||||||
@test("dashboard: publish is atomic and leaves no temp file")
|
@test("dashboard: publish is atomic and leaves no temp file")
|
||||||
def _():
|
def _():
|
||||||
with tempfile.TemporaryDirectory() as tmp:
|
with tempfile.TemporaryDirectory() as tmp:
|
||||||
|
|||||||
Reference in New Issue
Block a user