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

Bài 6 — Tính theo từng nến với onBar

Bạn sẽ học: viết logic chạy lần lượt qua từng nến, nhớ giá trị qua các nến (trạng thái), đưa kết quả lên biểu đồ, dùng chỉ báo trên số tự tính, đọc lại giá trị cũ của biến tự tính.

6.1. Khi nào cần onBar​

Phần lớn chỉ báo viết được bằng phép của chuỗi (Bài 1–5). Dùng onBar khi giá trị của nến này phụ thuộc kết quả của chính nó ở nến trước theo cách các phép có sẵn không diễn tả được:

  • đếm, cộng dồn có điều kiện, "kể từ khi…";
  • đường dừng lỗ kéo theo chỉ đi một chiều, trạng thái xu hướng tăng / giảm tự định nghĩa;
  • nến Heikin Ashi, Renko tự tính;
  • vẽ nhãn, hộp, đường, bảng (Bài 7).

6.2. Khung của một chỉ báo onBar​

indicator({
name: "Tổng KL phiên tăng 10 nến",
state: () => ({ ds: [] }),
onBar: (ctx, s) => {
s.ds.push(ctx.close > ctx.open ? ctx.volume : 0);
if (s.ds.length > 10) s.ds.shift();
let tong = 0;
for (const v of s.ds) tong += v;
return { tong };
},
plot: (d, p, o) => ({ "KL phiên tăng (10 nến)": columns(o.tong, { color: "up", format: "volume" }) }),
});
PhầnVai trò
state: () => ({ … })Trạng thái ban đầu — đối tượng chứa những gì cần nhớ qua các nến. Chỉ chứa dữ liệu: số, chữ, mảng, đối tượng thường
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. ctx là nến đang tính, s là trạng thái (sửa trực tiếp), p là tham số
return { tong }Giá trị của nến này cho từng đầu ra onBar ({ tong } là cách viết gọn của { tong: tong }). Thiếu khoá → NaN
plot: (d, p, o) => …o chứa các đầu ra onBar dưới dạng chuỗi: o.tong. Vẽ như mọi chuỗi khác
Giá trị onBar không tự vẽ

Giá trị onBar trả về chỉ hiện khi plot dùng nó qua o. Quên bước này, biểu đồ trống và trình soạn nhắc Script không vẽ gì. Chỉ trả plot: () => ({}) khi script vẽ toàn bộ bằng ctx.draw / ctx.table.

6.3. Những gì có trong ctx​

Thuộc tínhNghĩa
ctx.open, ctx.high, ctx.low, ctx.close, ctx.volume, ctx.timeGiá trị (số) của nến đang tính
ctx.iSố thứ tự nến (0 = nến đầu của dữ liệu đã nạp)
ctx.bar(k)OHLCV của nến k nến trước: ctx.bar(1).close là đóng cửa nến trước. k ≥ 0
ctx.closedNến đã đóng chưa (false ở nến đang chạy trong phiên)
ctx.warmupNến thuộc đoạn khởi động (không hiện, không xét cảnh báo)
ctx.realtimeĐang chạy theo giá realtime (không phải tính lại lịch sử)
ctx.v.ten, ctx.prev("ten", k)Giá trị của chuỗi khai báo trong series (6.4)
ctx.ta.*Chỉ báo trên số tự tính (6.5)
ctx.track(key, giaTri)Lịch sử của biến tự tính (6.6)
ctx.draw, ctx.table, ctx.log, ctx.alertVẽ đối tượng, bảng, ghi nhật ký, bắn cảnh báo — Bài 7, Tham chiếu › onBar

Trong onBar mọi thứ là số, nên dùng toán tử JavaScript bình thường: >, +, &&, Math.max(…).

6.4. Đưa chuỗi vào onBar​

Tính đường trung bình bằng tay trong onBar vừa dài vừa chậm. Thay vào đó khai báo chuỗi trong series, rồi đọc giá trị của nến đang tính bằng ctx.v.<tên>:

indicator({
name: "Số phiên liên tục trên EMA 20",
series: (d) => ({ ema: ta.ema(d.close, 20) }),
state: () => ({ dem: 0 }),
onBar: (ctx, s) => {
if (Number.isNaN(ctx.v.ema)) return;
s.dem = ctx.close > ctx.v.ema ? s.dem + 1 : 0;
return { dem: s.dem };
},
plot: (d, p, o) => ({ "Số phiên trên EMA 20": hist(o.dem, { color: "up" }) }),
});
  • series: (d, p) => ({ … }) khai báo chuỗi đầu vào (dựng như trong plot).
  • ctx.v.ema là giá trị EMA ở nến đang tính; ctx.prev("ema", 3) là giá trị 3 nến trước.
  • return; (không trả gì) — nến đó mọi đầu ra là NaN.

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

ctx.ta.ema(giaTri, n) tính EMA của một con số bạn tự tính ở từng nến — kết quả trùng ta.ema nếu áp trên cùng chuỗi. Có sma, ema, rma, wma, rsi, stdev, sum, slope, highest, lowest, prev, change, cum, barsSince, crossAbove, crossBelow, cross, valueWhen, atr.

indicator({
name: "Lực thân nến (EMA 10)",
onBar: (ctx) => {
const than = (ctx.close - ctx.open) / ctx.open * 100;
const luc = ctx.ta.ema(than, 10);
return { than, luc };
},
plot: (d, p, o) => ({
"Thân nến %": hist(o.than, { up: "up", down: "down", opacity: 0.4 }),
"EMA 10": line(o.luc, { color: "accent", width: 2 }),
}),
});
Gọi ctx.ta ở mọi nến, cùng thứ tự

Mỗi chỗ gọi ctx.ta.* là một bộ tính riêng, nhận ra theo thứ tự gọi trong một lượt onBar. Gọi trong if (nến gọi, nến không) làm lệch thứ tự và dừng với lỗi ctx.ta: lần gọi thứ … trước là …, nến này là … — gọi ctx.ta ở mọi nến theo cùng thứ tự, hoặc truyền key khi gọi trong nhánh. Cách sửa: gọi trước khi rẽ nhánh, hoặc truyền khoá riêng là đối số chữ cuối cùng: ctx.ta.ema(x, 10, "ema-tang").

indicator({
name: "EMA riêng cho nến tăng và nến giảm",
onBar: (ctx) => {
let tang = NaN;
let giam = NaN;
if (ctx.close >= ctx.open) tang = ctx.ta.ema(ctx.volume, 10, "kl-tang");
else giam = ctx.ta.ema(ctx.volume, 10, "kl-giam");
return { tang, giam };
},
plot: (d, p, o) => ({
"KL nến tăng (EMA)": line(o.tang.ffill(), { color: "up" }),
"KL nến giảm (EMA)": line(o.giam.ffill(), { color: "down" }),
}),
});

o.tang.ffill() lấp chỗ NaN bằng giá trị gần nhất trước đó để đường liền.

6.6. Đọc lại giá trị cũ của biến tự tính: ctx.track​

ctx.track("ten", giaTri) ghi giá trị của nến này và trả lịch sử của biến: .at(0) là nến này, .at(3) là 3 nến trước (chưa có → NaN). Đây là cách viết x[3] cho một biến bạn tự tính.

indicator({
name: "Động lượng của giá Heikin Ashi",
state: () => ({ o: NaN, c: NaN }),
onBar: (ctx, s) => {
const c = (ctx.open + ctx.high + ctx.low + ctx.close) / 4;
const o = Number.isNaN(s.o) ? (ctx.open + ctx.close) / 2 : (s.o + s.c) / 2;
s.o = o;
s.c = c;
const lichSu = ctx.track("haClose", c);
return { mom: c - lichSu.at(5) };
},
plot: (d, p, o) => ({
"Động lượng HA (5)": hist(o.mom, { up: "up", down: "down" }),
}),
});

ctx.track giữ 500 nến gần nhất cho mỗi biến (đủ cho mọi at(k) với k < 500).

6.7. Ví dụ hoàn chỉnh: dừng lỗ kéo theo ATR​

Một đường dừng lỗ đi theo giá: trong xu hướng tăng chỉ đi lên, nằm dưới giá mult × ATR; giá đóng cửa thủng đường thì đảo sang xu hướng giảm (đường nằm trên giá và chỉ đi xuống).

indicator({
name: "Dừng lỗ kéo theo ATR",
pane: "price",
params: {
n: param.int(14, { min: 2, max: 100, label: "Chu kỳ ATR" }),
mult: param.num(3, { min: 1, max: 6, step: 0.5, label: "Hệ số ATR" }),
},
state: () => ({ stop: NaN, dir: 1 }),
onBar: (ctx, s, p) => {
const atr = ctx.ta.atr(p.n);
if (Number.isNaN(atr)) return;
if (s.dir > 0) {
const moi = ctx.close - p.mult * atr;
s.stop = Number.isNaN(s.stop) ? moi : Math.max(s.stop, moi);
if (ctx.close < s.stop) {
s.dir = -1;
s.stop = ctx.close + p.mult * atr;
}
} else {
s.stop = Math.min(s.stop, ctx.close + p.mult * atr);
if (ctx.close > s.stop) {
s.dir = 1;
s.stop = ctx.close - p.mult * atr;
}
}
return { stop: s.stop, dir: s.dir };
},
plot: (d, p, o) => ({
"Dừng lỗ": line(o.stop, { width: 2, step: true, colors: [{ when: o.dir.gt(0), color: "up" }, { when: o.dir.lt(0), color: "down" }] }),
"Đảo lên": shape(o.dir.crossAbove(0), { at: "below", shape: "arrowUp", color: "up" }),
"Đảo xuống": shape(o.dir.crossBelow(0), { at: "above", shape: "arrowDown", color: "down" }),
}),
});
  • ctx.ta.atr(n) là ATR (làm trơn Wilder) của khung đang tính, gọi ở mọi nến — đặt ở đầu onBar trước mọi if.
  • Đầu ra onBar (o.dir) là chuỗi, nên dùng được crossAbove, colors như mọi chuỗi.

Dừng lỗ kéo theo ATR đổi màu theo xu hướng, mũi tên khi đảo chiều

6.8. Nến đang chạy, quay lui và trạng thái​

Trên lịch sử, onBar chạy đúng một lần mỗi nến. Trong phiên, mỗi giá mới làm nến cuối được tính lại: onBar chạy lại trên bản sao trạng thái đã chốt ở nến trước — các thay đổi của lần chạy trước trên cùng nến bị bỏ. Nhờ vậy trạng thái không bị cộng dồn sai theo số lần khớp lệnh, và kết quả realtime trùng với kết quả tính lại cả lịch sử.

  • Muốn một giá trị không bị quay lui (đếm số lần giá đổi trong nến, đo thời gian trong nến), dùng ctx.persist("khoa", () => ({ … })) — đối tượng tạo một lần, giữ qua mọi lần chạy.
  • ctx.closed cho biết lần chạy này là của nến đã đóng hay đang chạy.

Bài tập​

Viết chỉ báo đếm số phiên kể từ đỉnh 52 tuần gần nhất (đỉnh = giá cao nhất 250 phiên), vẽ dạng cột.

Lời giải (onBar)
indicator({
name: "Số phiên từ đỉnh 52 tuần",
series: (d) => ({ dinh: d.high.highest(250) }),
state: () => ({ tu: NaN }),
onBar: (ctx, s) => {
if (ctx.high >= ctx.v.dinh) s.tu = 0;
else if (!Number.isNaN(s.tu)) s.tu += 1;
return { tu: s.tu };
},
plot: (d, p, o) => ({ "Phiên từ đỉnh": columns(o.tu, { color: "accent" }) }),
});
Lời giải (không cần onBar)
indicator({
name: "Số phiên từ đỉnh 52 tuần (chuỗi)",
plot: (d) => ({
"Phiên từ đỉnh": columns(d.high.gte(d.high.highest(250)).barsSince(), { color: "accent" }),
}),
});

cond.barsSince() đếm số nến từ lần cond đúng gần nhất — thường có sẵn một phép chuỗi làm được việc tưởng phải dùng onBar.

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