Đi tới nội dung chính
Chuyển tới nội dung chính

Bài 7 — Vẽ nhãn, hộp, đường và bảng

Bạn sẽ học: vẽ đối tượng tự do từ onBar — nhãn có chữ, đoạn thẳng, hộp chữ nhật — sửa / xoá chúng theo id, và đặt bảng tóm tắt ở góc biểu đồ.

Đối tượng vẽ và bảng chỉ tạo được trong onBar (Bài 6), qua ctx.draw và ctx.table.

7.1. Nhãn​

indicator({
name: "Nhãn khoảng trống giá",
pane: "price",
limits: { labels: 100 },
onBar: (ctx) => {
if (ctx.i === 0) return;
const truoc = ctx.bar(1);
if (ctx.low > truoc.high) {
ctx.draw.label({ at: { barsAgo: 0, price: ctx.low }, text: "Gap ↑", style: "up", color: "up", tooltip: `Khoảng trống ${truoc.high} – ${ctx.low}` });
}
if (ctx.high < truoc.low) {
ctx.draw.label({ at: { barsAgo: 0, price: ctx.high }, text: "Gap ↓", style: "down", color: "down" });
}
},
plot: () => ({}),
});
Tuỳ chọn của ctx.draw.labelNghĩa
atĐiểm gắn nhãn: { barsAgo: k, price } (k nến trước nến đang tính), { time, price } (mốc thời gian của nến), { barsAhead: k, price } (k nến sau nến cuối — vẽ chiếu về phía trước)
textChữ trên nhãn
styleMũi nhọn của nhãn: "up" (nhãn nằm dưới, chỉ lên điểm), "down" (mặc định), "left", "right", "none"
color, textColorMàu nền nhãn, màu chữ
size"s", "m", "l"
tooltipChữ hiện khi rê chuột
idĐịnh danh — xem 7.4

plot: () => ({}) — script không có đường nào, chỉ vẽ đối tượng.

7.2. Đoạn thẳng​

ctx.draw.line({ a, b, … }) nối hai điểm; extend: "right" kéo dài về bên phải. Ví dụ: kẻ mức kháng cự (đỉnh 60 phiên) từ nến lập đỉnh kéo sang phải, luôn cập nhật theo đỉnh mới nhất:

indicator({
name: "Kháng cự đỉnh 60 phiên",
pane: "price",
series: (d) => ({
dinh: d.high.highest(60),
tuDinh: d.high.gte(d.high.highest(60)).barsSince(),
}),
onBar: (ctx) => {
if (Number.isNaN(ctx.v.dinh) || Number.isNaN(ctx.v.tuDinh)) return;
const tu = { barsAgo: ctx.v.tuDinh, price: ctx.v.dinh };
ctx.draw.line({ id: "khang-cu", a: tu, b: { barsAgo: 0, price: ctx.v.dinh }, extend: "right", color: "down", width: 2, dash: "dash" });
ctx.draw.label({ id: "nhan-khang-cu", at: tu, text: `Kháng cự ${ctx.v.dinh}`, style: "down", color: "down", size: "s" });
},
plot: () => ({}),
});

Tuỳ chọn của line: a, b (điểm như at của nhãn), extend ("none", "left", "right", "both"), color, width, dash ("solid", "dash", "dot").

7.3. Hộp​

ctx.draw.box({ a, b, … }) vẽ hình chữ nhật có hai góc đối diện a, b. Ví dụ: hộp bao 20 phiên gần nhất khi biên độ của chúng hẹp (dưới 8 %) — vùng tích luỹ:

indicator({
name: "Vùng tích luỹ",
pane: "price",
params: {
n: param.int(20, { min: 5, max: 100, label: "Số phiên" }),
maxRange: param.pct(0.08, { min: 0.02, max: 0.3, label: "Biên độ tối đa" }),
},
limits: { boxes: 100 },
series: (d, p) => ({ cao: d.high.highest(p.n), thap: d.low.lowest(p.n) }),
state: () => ({ dangCo: false, batDau: 0 }),
onBar: (ctx, s, p) => {
const { cao, thap } = ctx.v;
const hep = cao / thap - 1 <= p.maxRange;
if (hep && !s.dangCo) {
s.dangCo = true;
s.batDau = ctx.bar(p.n - 1).time;
}
if (hep) {
ctx.draw.box({ id: `tl-${s.batDau}`, a: { time: s.batDau, price: cao }, b: { barsAgo: 0, price: thap }, color: "ref", fill: "ref", opacity: 0.1, text: "Tích luỹ" });
} else s.dangCo = false;
},
plot: () => ({}),
});

Tuỳ chọn của box: a, b, color (viền), fill (màu tô), opacity (độ đục phần tô, mặc định 0,12), text.

7.4. Id: sửa thay vì vẽ thêm​

  • Gọi ctx.draw.* có id: nếu đã có đối tượng cùng id thì thay đối tượng đó (di chuyển, đổi chữ). Như ví dụ 7.2: chỉ một đường kháng cự, luôn ở vị trí mới nhất.
  • Không có id: mỗi lần gọi là một đối tượng mới — như các nhãn gap ở 7.1.
  • Id theo thời điểm (tl-${s.batDau} ở 7.3) giữ mỗi vùng một hộp, và hộp của vùng đang diễn ra được kéo dài theo từng nến.
  • ctx.draw.remove(id) xoá một đối tượng; ctx.draw.clear() xoá tất cả, ctx.draw.clear("label") chỉ xoá nhãn.
  • Mỗi loại mặc định giữ tối đa 50 đối tượng; vẽ thêm thì cái cũ nhất bị bỏ. Đổi bằng limits: { lines: 200, boxes: 100, labels: 300 } (tối đa 500 mỗi loại).

7.5. Bảng​

ctx.table(tên, { position, header, rows }) đặt một bảng ở một trong 9 vị trí của vùng vẽ. Mỗi vị trí giữ một bảng; gọi lại cùng vị trí thì bảng mới thay bảng cũ — nên gọi ở mọi nến, bảng luôn hiện số của nến cuối.

indicator({
name: "Bảng tóm tắt kỹ thuật",
pane: "price",
series: (d) => ({
rsi: ta.rsi(d.close, 14),
ma50: ta.sma(d.close, 50),
ma200: ta.sma(d.close, 200),
atr: ta.atr(d, 14),
}),
onBar: (ctx) => {
const { rsi, ma50, ma200, atr } = ctx.v;
const xuHuong = ma50 > ma200 ? "Tăng" : ma50 < ma200 ? "Giảm" : "—";
ctx.table("Tóm tắt", {
position: "top-right",
header: true,
rows: [
["Chỉ số", "Giá trị"],
["RSI 14", { text: rsi, color: rsi > 70 ? "down" : rsi < 30 ? "up" : "text" }],
["So với MA 200", { text: ctx.close / ma200 - 1, format: "percent", color: ctx.close > ma200 ? "up" : "down" }],
["ATR / giá", { text: atr / ctx.close, format: "percent" }],
["Xu hướng MA 50/200", { text: xuHuong, color: xuHuong === "Tăng" ? "up" : xuHuong === "Giảm" ? "down" : "textMuted" }],
],
});
},
plot: () => ({}),
});
position"top-left", "top-center", "top-right" (mặc định), "middle-left", "middle-center", "middle-right", "bottom-left", "bottom-center", "bottom-right"
header: trueDòng đầu là tiêu đề (chữ đậm hơn)
rowsMảng các dòng; mỗi dòng là mảng ô. Ô là chữ, số, hoặc { text, format, color, bg, tooltip }
format của ô"percent" (0,123 → 12,3 %), "integer", "volume" / "compact" (1,2 Tr), "price", "number"

Mỗi bảng tối đa 100 ô. ctx.table(tên, null) xoá bảng.

Nhãn và đường kháng cự đỉnh 60 phiên vẽ bằng ctx.draw

7.6. Đỉnh, đáy xác nhận và nhãn​

Đỉnh cục bộ chỉ xác nhận được sau khi đủ nến phía sau thấp hơn nó. Ví dụ đánh dấu đỉnh / đáy trong cửa sổ 2n + 1 nến, nhãn đặt lùi về đúng nến đỉnh (không nhìn trước: nhãn xuất hiện ở nến thứ n sau đỉnh):

indicator({
name: "Đỉnh / đáy xác nhận",
pane: "price",
params: { n: param.int(5, { min: 2, max: 30, label: "Số nến mỗi bên" }) },
limits: { labels: 300 },
onBar: (ctx, s, p) => {
if (ctx.i < 2 * p.n) return;
const giua = ctx.bar(p.n);
let dinh = true;
let day = true;
for (let k = 0; k <= 2 * p.n; k++) {
if (k === p.n) continue;
const b = ctx.bar(k);
if (b.high >= giua.high) dinh = false;
if (b.low <= giua.low) day = false;
}
if (dinh) ctx.draw.label({ at: { barsAgo: p.n, price: giua.high }, text: "Đ", style: "down", color: "down", size: "s" });
if (day) ctx.draw.label({ at: { barsAgo: p.n, price: giua.low }, text: "Đáy", style: "up", color: "up", size: "s" });
},
plot: () => ({}),
});

Bài tập​

Vẽ đoạn thẳng nối hai đáy xác nhận gần nhất (đường xu hướng đáy), kéo dài sang phải.

Lời giải
indicator({
name: "Đường nối hai đáy gần nhất",
pane: "price",
params: { n: param.int(5, { min: 2, max: 30, label: "Số nến mỗi bên" }) },
state: () => ({ days: [] }),
onBar: (ctx, s, p) => {
if (ctx.i < 2 * p.n) return;
const giua = ctx.bar(p.n);
let day = true;
for (let k = 0; k <= 2 * p.n; k++) if (k !== p.n && ctx.bar(k).low <= giua.low) day = false;
if (day) {
s.days.push({ time: giua.time, price: giua.low });
if (s.days.length > 2) s.days.shift();
}
if (s.days.length === 2) {
ctx.draw.line({ id: "xu-huong-day", a: s.days[0], b: s.days[1], extend: "right", color: "up", width: 2 });
}
},
plot: () => ({}),
});

Điểm { time, price } gắn đúng nến theo mốc thời gian, nên lưu được trong trạng thái và dùng lại ở nến sau.

Tiếp theo: Bài 8 — Khung lớn hơn và dữ liệu khác