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

Khái niệm nền

Trang này giải thích những ý cần hiểu một lần để đọc được mọi script: nến, chuỗi, ngăn vẽ, tham số, hai cách tính, và đủ JavaScript để viết chỉ báo. Đọc khoảng 15 phút.

Nến và dữ liệu d​

Biểu đồ là một dãy nến. Mỗi nến của một khung (1 phút, 15 phút, 1 ngày, 1 tuần…) có năm con số:

Tên trong scriptNghĩa
d.openGiá mở cửa
d.highGiá cao nhất
d.lowGiá thấp nhất
d.closeGiá đóng cửa (nến đang chạy: giá khớp gần nhất)
d.volumeKhối lượng khớp

d là dữ liệu của biểu đồ mà script nhận được: đúng mã, đúng khung bạn đang xem. Đổi mã hay khung trên biểu đồ là script tự tính lại trên dữ liệu mới. Ngoài năm giá trị trên, d còn có d.hl2 ((cao + thấp) / 2), d.hlc3, d.ohlc4, d.time (thời điểm mở nến), d.index (số thứ tự nến) và nến của khung lớn hơn — đầy đủ ở Tham chiếu › Dữ liệu.

Đơn vị giá

Giá cổ phiếu Việt Nam trên biểu đồ tính theo nghìn đồng (FPT 98,5 = 98.500 đ). Khối lượng tính theo cổ phiếu. Chỉ số (VNINDEX) tính theo điểm.

Chuỗi: cả cột số theo nến​

d.close không phải một con số mà là cả cột giá đóng cửa của mọi nến — gọi là một chuỗi (Series). Mọi phép tính trên chuỗi cũng cho ra một chuỗi:

Nếnd.closeta.sma(d.close, 3)d.close.gt(ta.sma(d.close, 3))
110NaNNaN
211NaNNaN
312111
41111,330
513121
  • ta.sma(d.close, 3) là cả cột trung bình 3 nến. Hai nến đầu chưa đủ 3 giá trị nên là NaN — "chưa biết".
  • d.close.gt(…) ("lớn hơn") là một chuỗi điều kiện: 1 = đúng, 0 = sai, NaN = chưa biết.

Vì chuỗi là cả cột số, bạn không dùng được dấu >, +, && của JavaScript giữa hai chuỗi (JavaScript không biết cộng hai cột). Thay vào đó mỗi phép là một phương thức viết sau dấu chấm:

Bạn muốn viếtViết trong FireAnt Script
close > smad.close.gt(sma)
close >= sma, <, <=, ==, !=.gte(sma), .lt(…), .lte(…), .eq(…), .neq(…)
a + b, a - b, a * b, a / ba.add(b), a.sub(b), a.mul(b), a.div(b)
cond1 && cond2, cond1 || cond2, !condcond1.and(cond2), cond1.or(cond2), cond.not()
close[1] (nến trước)d.close.shift(1)
"cắt lên", "cắt xuống"a.crossAbove(b), a.crossBelow(b)

Vế phải có thể là chuỗi hoặc số: ta.rsi(d.close, 14).gt(70), d.close.mul(1.05). Các phép nối tiếp nhau đọc từ trái sang phải: d.high.sub(d.low).div(d.close).mul(100) = (cao − thấp) / đóng × 100.

indicator({
name: "Biên độ nến (%)",
plot: (d) => {
const bienDo = d.high.sub(d.low).div(d.close).mul(100);
return {
"Biên độ %": hist(bienDo, { color: "accent" }),
"TB 20": line(ta.sma(bienDo, 20), { color: "ref" }),
};
},
});
Viết > giữa hai chuỗi không báo lỗi khi chạy — nhưng sai

ta.rsi(d.close, 14) > 70 là so sánh hai đối tượng trong JavaScript, luôn ra false: dấu sẽ không bao giờ hiện. Trình soạn gạch đỏ và báo Không dùng toán tử '>' cho kiểu 'Series' và 'number' — với chuỗi dùng .add() / .sub() / .gt()…. Gặp gạch đỏ là sửa ngay, đừng bỏ qua.

NaN, khởi động và logic ba trạng thái​

  • NaN ("not a number") nghĩa là chưa biết: nến đầu của đường trung bình, chia cho 0, giá trị không có. NaN lan theo phép tính: NaN + 1 = NaN. Trên biểu đồ, chỗ NaN không vẽ.
  • Khởi động: chỉ báo cần một số nến đầu để "nóng máy" (SMA 20 cần 20 nến; EMA, RSI cần lâu hơn để ổn định). Biểu đồ tự nạp thêm nến trước vùng đang xem và không vẽ / không đánh giá cảnh báo, lệnh trong đoạn khởi động.
  • Điều kiện có ba trạng thái: đúng (1), sai (0), chưa biết (NaN). Dấu, tô nền, cảnh báo, tín hiệu chỉ bật khi đúng; NaN tính như không đúng. Muốn coi "chưa biết" là sai rõ ràng, dùng .fillna(0); muốn đảo điều kiện mà vẫn an toàn, viết .fillna(0).not().

Không nhìn trước tương lai​

Ở mỗi nến, script chỉ thấy nến đó và các nến trước — đúng như khi giao dịch thật. d.close.shift(1) là đóng cửa nến trước; shift(5) là 5 nến trước. Số âm (shift(-1) — nến sau) bị chặn:

// ✗ lỗi: lookahead
indicator({
name: "Nhìn trước (sai)",
plot: (d) => ({ "Ngày mai": line(d.close.shift(-1)) }),
});

Script này dừng với lỗi shift(n) với n âm dùng dữ liệu tương lai — không được phép. Nhờ vậy, tín hiệu bạn thấy trên lịch sử là tín hiệu thật sự đã có ở thời điểm đó — không "vẽ lại" sau khi giá chạy.

Nến đang chạy (phiên chưa đóng) thì khác: giá còn thay đổi, nên dấu / tín hiệu của nến cuối có thể hiện rồi mất. Chiến lược và cảnh báo có tuỳ chọn chỉ xét ở nến đã đóng (Bài 5, Bài 9).

Cấu trúc một script​

Một script là một lời gọi indicator({...}) (chỉ báo) hoặc strategy({...}) (chiến lược). Bên trong là các mục, mục nào không cần thì bỏ:

indicator({
name: "Tên chỉ báo", // bắt buộc: tên trên chú giải, thư viện
pane: "new", // "price" chồng lên giá, "new" ngăn riêng (mặc định)
params: { // tham số người dùng chỉnh được
len: param.int(14, { min: 2, max: 100, label: "Chu kỳ" }),
},
plot: (d, p) => ({ // bắt buộc: những gì vẽ lên biểu đồ
RSI: line(ta.rsi(d.close, p.len)),
}),
alerts: (d, p) => ({ // điều kiện để đặt cảnh báo
"RSI vượt 70": ta.rsi(d.close, p.len).crossAbove(70),
}),
});

Chiến lược thay plot bằng signals (khi nào mua, khi nào bán) và thêm risk (dừng lỗ, chốt lời):

strategy({
name: "Tên chiến lược",
params: { len: param.int(20, { min: 5, max: 100, label: "Chu kỳ" }) },
signals: (d, p) => ({
entry: d.close.crossAbove(ta.sma(d.close, p.len)), // mua khi giá cắt lên MA
exit: d.close.crossBelow(ta.sma(d.close, p.len)), // bán khi giá cắt xuống MA
}),
risk: { stopLoss: 0.07 }, // dừng lỗ 7 %
});

Các hàm trong khai báo — plot, alerts, signals — được gọi một lần với cả chuỗi dữ liệu, không phải mỗi nến một lần. Chúng "lắp" phép tính; bộ chạy tính lắp ráp đó trên mọi nến rồi cập nhật nến cuối theo từng giá mới.

Hai cách tính: cả chuỗi hay từng nến​

Cả chuỗi (plot, signals, alerts)Từng nến (onBar)
Viết bằngPhương thức của chuỗi: .gt(), .add(), ta.*JavaScript thường trên số: >, +, if, vòng lặp
Khi nào dùngPhần lớn chỉ báo và chiến lượcCần nhớ qua các nến (đếm, trạng thái), vẽ nhãn / hộp / bảng
Nhanh, lọc cả rổ đượcCóChậm hơn; chưa lọc cả rổ được

Bắt đầu bằng cách thứ nhất; chỉ chuyển sang onBar khi thật cần — Bài 6 dạy cách dùng. Ví dụ một chỉ báo đếm số nến tăng liên tiếp phải nhớ con số của nến trước, nên dùng onBar:

indicator({
name: "Nến tăng liên tiếp",
state: () => ({ dem: 0 }),
onBar: (ctx, s) => {
s.dem = ctx.close > ctx.open ? s.dem + 1 : 0;
return { dem: s.dem };
},
plot: (d, p, o) => ({ "Số nến tăng": hist(o.dem, { color: "up" }) }),
});

Trong onBar, ctx.close, ctx.open là con số của nến đang tính, nên dùng > bình thường.

Ngăn vẽ và thang​

  • pane: "price" — vẽ chồng lên nến giá, dùng chung thang giá (đường trung bình, dải Bollinger, mũi tên tín hiệu).
  • pane: "new" (mặc định) — một ngăn riêng dưới biểu đồ, thang riêng (RSI, MACD, khối lượng).
  • Một đầu ra có thể đặt ngăn khác với cả chỉ báo: line(x, { pane: "price" }) trong chỉ báo ngăn riêng.
  • scale: { range: [0, 100], levels: [30, 70] } cố định thang của ngăn riêng từ 0 đến 100 và kẻ mức 30, 70.

Tham số​

Số "cứng" trong mã (chu kỳ 14, ngưỡng 70) nên đưa thành tham số để chỉnh mà không sửa mã, và để tối ưu thử nhiều giá trị. param.int(14, { min: 2, max: 100, label: "Chu kỳ" }) tạo ô số nguyên; giá trị nằm ở p.<tên> trong mọi hàm của khai báo. Đủ các loại tham số ở Bài 4.

JavaScript tối thiểu​

FireAnt Script là JavaScript. Bạn chỉ cần những phần dưới — đủ cho mọi ví dụ trong chương.

ViếtNghĩa
// chú thíchGhi chú cho người đọc, bộ chạy bỏ qua
const x = 5;Đặt tên x cho giá trị 5 (không đổi được nữa)
let n = 0; n = n + 1;Biến đổi được (dùng trong onBar)
"chữ", EMA ${n}Chuỗi chữ; trong dấu ````` thì ${…} chèn giá trị
{ a: 1, "Tên có dấu cách": 2 }Đối tượng: các cặp tên – giá trị. Tên có dấu cách / dấu tiếng Việt thì để trong ngoặc kép
[1, 2, 3]Mảng (danh sách)
(d) => d.closeHàm ngắn nhận d, trả d.close
(d, p) => ({ A: … })Hàm trả một đối tượng: bọc đối tượng trong ngoặc tròn ( … )
(d) => { const x = …; return { … }; }Hàm nhiều dòng: thân trong { }, kết thúc bằng return
if (a > b) { … } else { … }Rẽ nhánh (trong onBar)
a > b ? x : y"Nếu a > b thì x, không thì y"
Math.max(a, b), Math.abs(x), Math.round(x)Hàm toán của JavaScript (trên số)
Number.isNaN(x)x có phải NaN không

Không có import, không gọi mạng, không đọc tệp: script chạy trong sandbox tách khỏi trang, có 2 giây để tính cả lịch sử và 50 ms cho mỗi lần cập nhật giá.

Tiếp theo​

Bài 1 — Đường trung bình trên giá