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

onBar

onBar(ctx, s, p) chạy một lần cho mỗi nến, từ nến đầu tới nến cuối, rồi chạy lại nến cuối mỗi khi có giá mới. Dùng khi cần nhớ qua các nến, tự tính theo từng nến, vẽ đối tượng, bảng. Hướng dẫn từng bước: Bài 6, Bài 7.

indicator({
series?: (d, p) => ({ tên: chuỗi }), // chuỗi đầu vào → ctx.v.tên
state?: () => ({ … }), // trạng thái ban đầu → s
onBar: (ctx, s, p) => ({ tênĐầuRa: số }), // giá trị nến này → o.tênĐầuRa trong plot / alerts
plot: (d, p, o) => ({ … }),
})
Đối sốNghĩa
ctxNgữ cảnh của nến đang tính (bảng dưới)
sTrạng thái: đối tượng từ state(), sửa trực tiếp (s.dem += 1). Thiếu state → undefined
pGiá trị tham số
Giá trị trả vềĐối tượng { tên: số | true | false }: giá trị của các đầu ra onBar ở nến này (true = 1, false = 0, thiếu khoá → NaN). Trả undefined (hoặc return;) → mọi đầu ra NaN ở nến này

Đầu ra onBar dùng trong plot / alerts qua đối số thứ ba o (o.tên là chuỗi). Giá trị onBar không tự vẽ — phải đưa vào plot. Dùng o.x mà script không có onBar: lỗi o.x: script không có onBar.

ctx: nến đang tính​

Thuộc tínhKiểuNghĩa
ctx.timesốMốc mở nến (ms UTC)
ctx.open, ctx.high, ctx.low, ctx.close, ctx.volumesốOHLCV của nến
ctx.isốChỉ số nến: 0 = nến đầu của dữ liệu đã nạp (kể cả đoạn khởi động)
ctx.closedđúng / saiNến đã đóng; false ở nến đang chạy (lần chạy này sẽ được chạy lại khi có giá mới)
ctx.realtimeđúng / saiĐang chạy theo giá realtime (không phải tính lại lịch sử)
ctx.warmupđúng / saiNến thuộc đoạn khởi động (chưa hiện, chưa xét cảnh báo)
ctx.symbolchữMã đang tính
ctx.resolutionchữKhung đang tính
ctx.v.tênsốGiá trị nến này của chuỗi tên khai báo trong series
ctx.prev(tên, k)sốGiá trị k nến trước của chuỗi tên trong series (k ≥ 0). Tên không có: ctx.prev: không có chuỗi "…" trong series
ctx.bar(k){ time, open, high, low, close, volume }OHLCV của nến k nến trước (k ≥ 0; 0 = nến này)

k âm ở ctx.prev, ctx.bar, ctx.track(…).at báo lỗi lookahead (… phải ≥ 0 (không đọc được nến sau)).

ctx.ltf(tên) — mảng giá trị các nến khung nhỏ (d.ltf) của nến đang chạy cho chuỗi khai báo trong series (series: (d) => ({ c: d.ltf("1").close })); ctx.v.c là giá trị ở nến nhỏ cuối. Nến không có nến nhỏ → mảng rỗng.

ctx.ta: chỉ báo trên số tự tính​

Gọi mỗi nến với giá trị (số) của nến đó; bộ tính tự nhớ lịch sử. Kết quả trùng ta.* / phép chuỗi cùng tên khi áp trên cùng chuỗi.

HàmKết quảMặc định
ctx.ta.sma(x, n)Trung bình n giá trịn = 20
ctx.ta.ema(x, n)EMAn = 20
ctx.ta.rma(x, n)Làm trơn Wildern = 14
ctx.ta.wma(x, n)Trung bình trọng sốn = 20
ctx.ta.rsi(x, n)RSIn = 14
ctx.ta.stdev(x, n)Độ lệch chuẩn tổng thển = 20
ctx.ta.sum(x, n)Tổng n giá trịn = 20
ctx.ta.slope(x, n)Độ dốc hồi quyn = 20
ctx.ta.highest(x, n), ctx.ta.lowest(x, n)Lớn / nhỏ nhất n giá trịn = 20
ctx.ta.prev(x, n)x của n lần gọi trướcn = 1
ctx.ta.change(x, n)x − x của n lần gọi trướcn = 1
ctx.ta.cum(x)Cộng dồn từ nến đầu
ctx.ta.barsSince(dk)Số nến từ lần dk đúng gần nhất (chưa từng → NaN)
ctx.ta.crossAbove(a, b), ctx.ta.crossBelow(a, b), ctx.ta.cross(a, b)1 / 0: cắt lên / xuống / cả hai
ctx.ta.valueWhen(x, dk, thu)x ở lần thứ thu (0 = gần nhất) dk đúngthu = 0
ctx.ta.atr(n)ATR (Wilder) của nến đang tính — không nhận x; gọi ở mọi nến thì trùng ta.atr(d, n)n = 14

Thứ tự gọi. Mỗi chỗ gọi là một bộ tính riêng, nhận ra theo thứ tự gọi trong một lượt onBar. Phải gọi ở mọi nến, cùng thứ tự. Gọi trong nhánh if thì truyền khoá riêng là đối số chữ cuối cùng: ctx.ta.ema(x, 10, "ema-tang"). Lệch thứ tự: lỗi ta-order ctx.ta: lần gọi thứ 0 trước là sma:5, nến này là ema:10 — gọi ctx.ta ở mọi nến theo cùng thứ tự, hoặc truyền key khi gọi trong nhánh. n phải là số nguyên ≥ 1 (thu ≥ 0).

ctx.track: lịch sử biến tự tính​

ctx.track(khoa, giaTri?) → { at(k = 0) }

  • ctx.track("x", v) ghi v cho nến này (ghi nhiều lần trong một nến: lần cuối thắng) và trả lịch sử.
  • ctx.track("x") chỉ đọc.
  • .at(0) = giá trị đã ghi ở nến này (chưa ghi → NaN); .at(k) = giá trị cuối của nến k nến trước (nến đó không ghi → NaN).
  • Giữ 500 nến gần nhất mỗi khoá (đổi bằng limits: { track: 2000 }, tối đa 5.000). Quay lui cùng trạng thái khi tick.

Đây là cách viết x[n] cho biến tự tính: đọc ctx.track("x").at(n) ở bất kỳ đâu, ghi ctx.track("x", x) ở cuối onBar.

ctx.persist: giá trị không quay lui​

ctx.persist(khoa, () => giaTriDau) → đối tượng

Tạo một lần theo khoa, giữ qua mọi lần chạy — kể cả các lần chạy lại nến cuối khi có giá mới (trạng thái s thì quay lui về bản đã chốt). Dùng để đếm số lần giá đổi trong nến, đo trong nến. Phải là dữ liệu JSON.

ctx.draw: đối tượng vẽ​

Điểm (a, b, at) viết một trong ba cách:

ĐiểmNghĩa
{ barsAgo: k, price }Nến k nến trước nến đang tính (0 = nến này)
{ time, price }Nến có mốc thời gian time (ms) — lưu được trong trạng thái để dùng ở nến sau
{ barsAhead: k, price }k nến sau nến đang tính — vẽ chiếu về phía trước (vùng chưa có nến)
HàmTuỳ chọnTrả về
ctx.draw.line({ id?, a, b, extend?, color?, width?, dash? })extend: "none" (mặc định), "left", "right", "both"; width mặc định 1; dash: "solid", "dash", "dot"{ id, remove() }
ctx.draw.box({ id?, a, b, color?, fill?, opacity?, text? })color viền, fill màu tô (mặc định theo color), opacity phần tô (mặc định 0,12), text chữ trong hộp{ id, remove() }
ctx.draw.label({ id?, at, text, style?, color?, textColor?, size?, tooltip? })style: "up", "down" (mặc định), "left", "right", "none"; size: "s", "m", "l"{ id, remove() }
ctx.draw.polyline({ id?, points, closed?, curved?, color?, width?, dash?, fill?, opacity? })Đường gấp khúc qua nhiều điểm — xem bên dưới{ id, remove(), setPoints(points) }
ctx.draw.remove(id)Xoá một đối tượng
ctx.draw.clear(loai?)Xoá tất cả, hoặc một loại: "line", "box", "label", "polyline"
  • Có id: gọi lại cùng id thì thay đối tượng cũ (di chuyển / sửa). Không id: mỗi lần gọi là một đối tượng mới.
  • Mỗi loại giữ tối đa 50 đối tượng (mới nhất); vượt thì bỏ cái cũ nhất. Đổi bằng limits: { lines, boxes, labels, polylines } (1–500; đường gấp khúc 1–100).
  • Màu mặc định là màu nhấn; chữ nhãn mặc định trắng.

Đường gấp khúc​

ctx.draw.polyline vẽ một đường nối lần lượt các điểm (như polyline.new của Pine): zigzag, đa giác bao quanh mẫu hình, kênh tự vẽ, hình tuỳ ý.

Tuỳ chọnNghĩa
pointsMảng điểm theo thứ tự nối, mỗi điểm viết như ở bảng Điểm trên. Tối đa 10.000 điểm mỗi đường
closedtrue: nối điểm cuối về điểm đầu thành hình khép kín. Mặc định false
curvedtrue: đường cong mượt đi qua đúng các điểm thay cho đoạn thẳng. Mặc định false
color, width, dashMàu nét (mặc định màu nhấn), độ dày (mặc định 1; 0 = không vẽ nét, chỉ tô), kiểu nét "solid" / "dash" / "dot"
fill, opacityMàu tô phần bên trong (chỉ tô khi có fill) và độ đục phần tô (mặc định 0,12)
idNhư các đối tượng khác: cùng id = thay đường cũ
  • setPoints(points) thay các điểm của đường (giữ màu, nét); remove() xoá đường.
  • Điểm có giá NaN (chưa có giá trị) bị bỏ qua, đường nối thẳng qua các điểm còn lại.
  • Hơn 10.000 điểm: lỗi ctx.draw.polyline: tối đa 10.000 điểm mỗi đường. Điểm viết sai (thiếu barsAgo / barsAhead / time): lỗi ctx.draw.polyline: điểm … phải là { barsAgo | barsAhead | time, price }.
  • Mỗi script giữ tối đa 50 đường (mới nhất), đổi bằng limits: { polylines } — tối đa 100.
  • Muốn nhớ các điểm qua nhiều nến (đỉnh / đáy của zigzag), lưu { time, price } trong state — mốc thời gian không đổi khi có nến mới.
  • Vẽ lại cả đường ở mỗi nến với cùng id là cách thường dùng; khi giá chạy trong phiên, đường của nến đang hình thành được vẽ lại theo giá mới như mọi đối tượng vẽ khác. Ẩn chỉ báo thì đường gấp khúc ẩn theo.
  • Chỉ có trong onBar (phần khai báo plot không có đối tượng vẽ).
indicator({
name: "Zigzag 5 %",
pane: "price",
params: { pct: param.num(5, { min: 1, max: 30, step: 0.5, label: "Đảo chiều (%)" }) },
limits: { polylines: 20 },
state: () => ({ huong: 0, cucTri: NaN, mocCucTri: 0, diem: [] }),
onBar: (ctx, s, p) => {
const k = p.pct / 100;
if (s.huong === 0) {
s.huong = 1;
s.cucTri = ctx.high;
s.mocCucTri = ctx.time;
s.diem.push({ time: ctx.time, price: ctx.low });
}
if (s.huong > 0) {
if (ctx.high > s.cucTri) {
s.cucTri = ctx.high;
s.mocCucTri = ctx.time;
} else if (ctx.low < s.cucTri * (1 - k)) {
// Xác nhận đỉnh: tam giác đỏ nhạt từ đỉnh xuống nến này
s.diem.push({ time: s.mocCucTri, price: s.cucTri });
ctx.draw.polyline({
points: [{ time: s.mocCucTri, price: s.cucTri }, { barsAgo: 0, price: ctx.low }, { barsAgo: 0, price: s.cucTri }],
closed: true, color: "down", width: 0, fill: "down", opacity: 0.15,
});
s.huong = -1;
s.cucTri = ctx.low;
s.mocCucTri = ctx.time;
}
} else if (ctx.low < s.cucTri) {
s.cucTri = ctx.low;
s.mocCucTri = ctx.time;
} else if (ctx.high > s.cucTri * (1 + k)) {
s.diem.push({ time: s.mocCucTri, price: s.cucTri });
s.huong = 1;
s.cucTri = ctx.high;
s.mocCucTri = ctx.time;
}
if (s.diem.length > 500) s.diem.shift();
// Cả zigzag là một đường (id "zigzag"), điểm cuối là cực trị đang chạy
ctx.draw.polyline({ id: "zigzag", points: [...s.diem, { time: s.mocCucTri, price: s.cucTri }], color: "accent", width: 2 });
},
plot: () => ({}),
});

ctx.table: bảng​

ctx.table(ten, { position?, header?, rows }) — đặt bảng tại một trong 9 vị trí: "top-left", "top-center", "top-right" (mặc định), "middle-left", "middle-center", "middle-right", "bottom-left", "bottom-center", "bottom-right". Mỗi vị trí một bảng: gọi lại cùng vị trí thì thay. ctx.table(ten, null) xoá bảng tên đó.

  • rows: mảng dòng, mỗi dòng mảng ô. Ô là chữ, số (hiện theo kiểu Việt Nam, số NaN hiện —), hoặc { text, format?, color?, bg?, tooltip? }.
  • header: true: dòng đầu là tiêu đề.
  • format của ô: "percent" (0,12 → 12 %), "integer", "volume" / "compact", "price", "number".
  • Tối đa 100 ô mỗi bảng (thừa thì cắt theo dòng).

ctx.log​

ctx.log(...giaTri) ghi một dòng vào thẻ Nhật ký của khung script, gắn với nến đang tính — bấm nút ngày của dòng để biểu đồ nhảy tới nến đó. Giữ 1.000 dòng gần nhất. Đối tượng được in dạng JSON. Đây là cách ghi nhật ký trên biểu đồ (log() / console.log ở ngoài onBar chỉ ghi khi dựng script, không hiện ở Nhật ký của biểu đồ).

ctx.alert​

ctx.alert(ten, loiNhan?) đánh dấu một sự kiện cảnh báo tên ten ở nến này. Nếu khai báo alertNames: ["…"], ten phải nằm trong danh sách (không thì lỗi ctx.alert: "…" chưa khai báo trong alertNames).

Cảnh báo theo script trên web hiện nhận các điều kiện trong alerts. Để cảnh báo theo logic của onBar, trả điều kiện thành một đầu ra rồi khai báo trong alerts qua o — xem Bài 5.

Trạng thái và quay lui​

  • state() trả dữ liệu thuần: số, chữ, đúng / sai, null, mảng, đối tượng thường, mảng số kiểu (Float64Array…). Có hàm / Date / lớp tự viết: lỗi state-not-json (state.f: trạng thái onBar chỉ chứa dữ liệu …, không chứa hàm).
  • Tối đa 256 KB (vượt: Trạng thái onBar vượt 256 KB (nến …) — giữ ít dữ liệu hơn trong state).
  • Trên lịch sử mỗi nến chạy một lần. Với nến đang chạy, mỗi giá mới chạy lại onBar trên bản sao trạng thái đã chốt ở nến trước; log, đối tượng vẽ, bảng, ctx.track của lần chạy bị thay được bỏ — kết quả realtime trùng kết quả tính lại cả lịch sử.
  • Trong onBar, Math.random() cho cùng dãy số ở mọi lần chạy cùng nến; Date.now() và new Date() trả mốc của nến (không phải giờ máy) — để kết quả lặp lại được.
  • Chiến lược chỉ chạy onBar ở nến đã đóng.

Ví dụ tổng hợp​

indicator({
name: "Mọi thứ của ctx",
pane: "price",
limits: { lines: 10, boxes: 10, labels: 30, track: 1000 },
alertNames: ["Đảo chiều"],
series: (d) => ({ ema: ta.ema(d.close, 20), dinh: d.high.highest(20) }),
state: () => ({ xuHuong: 0, dem: 0 }),
onBar: (ctx, s, p) => {
const t = ctx.ta;
const sma = t.sma(ctx.close, 20);
const ema = t.ema(ctx.close, 20);
const rma = t.rma(ctx.close, 14);
const wma = t.wma(ctx.close, 20);
const rsi = t.rsi(ctx.close, 14);
const sd = t.stdev(ctx.close, 20);
const tong = t.sum(ctx.volume, 5);
const doc = t.slope(ctx.close, 20);
const cao = t.highest(ctx.high, 20);
const thap = t.lowest(ctx.low, 20);
const truoc = t.prev(ctx.close, 1);
const doi = t.change(ctx.close, 1);
const luyKe = t.cum(ctx.volume);
const tuCat = t.barsSince(ctx.close > ctx.v.ema);
const catLen = t.crossAbove(ctx.close, ema);
const catXuong = t.crossBelow(ctx.close, ema);
const cat = t.cross(ctx.close, sma);
const giaKhiCat = t.valueWhen(ctx.close, catLen, 0);
const atr = t.atr(14);
const lich = ctx.track("c", ctx.close);
const dem = ctx.persist("lanChay", () => ({ n: 0 }));
dem.n += 1;
if (catLen) s.xuHuong = 1;
if (catXuong) s.xuHuong = -1;
if (catLen || catXuong) {
s.dem += 1;
ctx.alert("Đảo chiều", catLen ? "Giá cắt lên EMA 20" : "Giá cắt xuống EMA 20");
ctx.draw.label({ at: { barsAgo: 0, price: catLen ? ctx.low : ctx.high }, text: catLen ? "▲" : "▼", style: catLen ? "up" : "down", color: catLen ? "up" : "down", size: "s" });
if (s.dem % 50 === 0) ctx.log("Lần đảo chiều thứ", s.dem, { gia: ctx.close, nen: ctx.i, dong: ctx.closed, rt: ctx.realtime, kd: ctx.warmup });
}
ctx.draw.line({ id: "dinh", a: { barsAgo: 19, price: ctx.v.dinh }, b: { barsAhead: 5, price: ctx.v.dinh }, color: "down", dash: "dash" });
ctx.draw.box({ id: "kenh", a: { barsAgo: 19, price: cao }, b: { barsAgo: 0, price: thap }, color: "accent", opacity: 0.05 });
ctx.table("Trạng thái", {
position: "bottom-right",
header: true,
rows: [
["Mục", "Giá trị"],
["Mã / khung", `${ctx.symbol} · ${ctx.resolution}`],
["RSI", { text: rsi, format: "number" }],
["ATR / giá", { text: atr / ctx.close, format: "percent" }],
["Xu hướng", { text: s.xuHuong > 0 ? "Tăng" : s.xuHuong < 0 ? "Giảm" : "—", color: s.xuHuong > 0 ? "up" : "down" }],
],
});
return {
sma, ema, rma, wma, rsi, sd, tong, doc, cao, thap, truoc, doi, luyKe, tuCat, cat, giaKhiCat, atr,
dong5: lich.at(5),
thanTruoc: ctx.bar(1).close - ctx.bar(1).open,
emaTruoc: ctx.prev("ema", 1),
lanChay: dem.n,
xh: s.xuHuong,
};
},
plot: (d, p, o) => ({
"Đóng cửa 5 nến trước": line(o.dong5, { color: "textMuted", dash: "dot" }),
"WMA 20 (ctx.ta)": line(o.wma, { color: "ai", hidden: true }),
"Giá lúc cắt lên gần nhất": line(o.giaKhiCat, { color: "up", step: true, hidden: true }),
"Xu hướng": bgcolor(o.xh.gt(0), { color: "up", opacity: 0.04 }),
}),
alerts: (d, p, o) => ({ "Xu hướng chuyển tăng": o.xh.crossAbove(0) }),
});