Khái niệm nền
- 🇻🇳 Tiếng Việt
- 🇬🇧 English
- 🇰🇷 한국어
- 🇨🇳 中文
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 script | Nghĩa |
|---|---|
d.open | Giá mở cửa |
d.high | Giá cao nhất |
d.low | Giá thấp nhất |
d.close | Giá đóng cửa (nến đang chạy: giá khớp gần nhất) |
d.volume | Khố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.
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ến | d.close | ta.sma(d.close, 3) | d.close.gt(ta.sma(d.close, 3)) |
|---|---|---|---|
| 1 | 10 | NaN | NaN |
| 2 | 11 | NaN | NaN |
| 3 | 12 | 11 | 1 |
| 4 | 11 | 11,33 | 0 |
| 5 | 13 | 12 | 1 |
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ết | Viết trong FireAnt Script |
|---|---|
close > sma | d.close.gt(sma) |
close >= sma, <, <=, ==, != | .gte(sma), .lt(…), .lte(…), .eq(…), .neq(…) |
a + b, a - b, a * b, a / b | a.add(b), a.sub(b), a.mul(b), a.div(b) |
cond1 && cond2, cond1 || cond2, !cond | cond1.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" }),
};
},
});
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ằng | Phươ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ùng | Phần lớn chỉ báo và chiến lược | Cần nhớ qua các nến (đếm, trạng thái), vẽ nhãn / hộp / bảng |
| Nhanh, lọc cả rổ được | Có | 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ết | Nghĩa |
|---|---|
// chú thích | Ghi 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.close | Hà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
This page explains the ideas you need to understand once to read any script: candles, series, plot panes, parameters, the two ways of calculating, and just enough JavaScript to write indicators. About a 15-minute read.
Candles and the d data
A chart is a sequence of candles. Each candle of a timeframe (1 minute, 15 minutes, 1 day, 1 week…) has five numbers:
| Name in the script | Meaning |
|---|---|
d.open | Open price |
d.high | High price |
d.low | Low price |
d.close | Close price (for the live candle: the latest matched price) |
d.volume | Matched volume |
d is the chart data the script receives: exactly the ticker and timeframe you are viewing. Change the ticker or timeframe on the chart and the script automatically recalculates on the new data. Besides those five values, d also has d.hl2 ((high + low) / 2), d.hlc3, d.ohlc4, d.time (the candle's open time), d.index (the candle index) and candles of higher timeframes — the full list is in Reference › Data.
Vietnamese stock prices on the chart are in thousand VND (FPT 98.5 = VND 98,500). Volume is in shares. Indices (VNINDEX) are in points.
Series: a whole column of numbers by candle
d.close is not a single number but the whole column of closing prices for every candle — called a series (Series). Every calculation on a series also produces a series:
| Candle | d.close | ta.sma(d.close, 3) | d.close.gt(ta.sma(d.close, 3)) |
|---|---|---|---|
| 1 | 10 | NaN | NaN |
| 2 | 11 | NaN | NaN |
| 3 | 12 | 11 | 1 |
| 4 | 11 | 11.33 | 0 |
| 5 | 13 | 12 | 1 |
ta.sma(d.close, 3)is the whole column of 3-candle averages. The first two candles don't have 3 values yet, so they are NaN — "unknown".d.close.gt(…)("greater than") is a condition series: 1 = true, 0 = false, NaN = unknown.
Because a series is a whole column of numbers, you cannot use JavaScript's >, +, && between two series (JavaScript doesn't know how to add two columns). Instead, each operation is a method written after a dot:
| You want to write | Write in FireAnt Script |
|---|---|
close > sma | d.close.gt(sma) |
close >= sma, <, <=, ==, != | .gte(sma), .lt(…), .lte(…), .eq(…), .neq(…) |
a + b, a - b, a * b, a / b | a.add(b), a.sub(b), a.mul(b), a.div(b) |
cond1 && cond2, cond1 || cond2, !cond | cond1.and(cond2), cond1.or(cond2), cond.not() |
close[1] (previous candle) | d.close.shift(1) |
| "crosses above", "crosses below" | a.crossAbove(b), a.crossBelow(b) |
The right-hand side can be a series or a number: ta.rsi(d.close, 14).gt(70), d.close.mul(1.05). Chained operations read left to right: d.high.sub(d.low).div(d.close).mul(100) = (high − low) / close × 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" }),
};
},
});
ta.rsi(d.close, 14) > 70 compares two objects in JavaScript and always returns false: the marker will never appear. The editor underlines it in red and reports Không dùng toán tử '>' cho kiểu 'Series' và 'number' — với chuỗi dùng .add() / .sub() / .gt()… (Do not use the '>' operator on 'Series' and 'number' — for series use .add() / .sub() / .gt()…). When you see a red underline, fix it right away; don't ignore it.
NaN, warm-up and three-state logic
- NaN ("not a number") means unknown: the first candles of a moving average, division by 0, missing values. NaN propagates through calculations: NaN + 1 = NaN. On the chart, NaN points are not drawn.
- Warm-up: an indicator needs some initial candles to "warm up" (SMA 20 needs 20 candles; EMA and RSI need longer to stabilize). The chart automatically loads extra candles before the visible range and does not draw / evaluate alerts or orders during the warm-up.
- Conditions have three states: true (1), false (0), unknown (NaN). Markers, background shading, alerts and signals fire only when true; NaN counts as not true. To explicitly treat "unknown" as false, use
.fillna(0); to negate a condition safely, write.fillna(0).not().
No looking into the future
On each candle, the script only sees that candle and earlier ones — just like in live trading. d.close.shift(1) is the previous candle's close; shift(5) is 5 candles back. Negative numbers (shift(-1) — the next candle) are blocked:
// ✗ lỗi: lookahead
indicator({
name: "Nhìn trước (sai)",
plot: (d) => ({ "Ngày mai": line(d.close.shift(-1)) }),
});
This script stops with the error shift(n) với n âm dùng dữ liệu tương lai — không được phép (shift(n) with negative n uses future data — not allowed). As a result, the signals you see on history are signals that truly existed at that time — they don't "repaint" after price moves.
The live candle (session not yet closed) is different: price is still changing, so the marker / signal on the last candle can appear and then disappear. Strategies and alerts have options to evaluate only on closed candles (Lesson 5, Lesson 9).
Structure of a script
A script is one call to indicator({...}) (indicator) or strategy({...}) (strategy). Inside are fields; leave out any you don't need:
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),
}),
});
A strategy replaces plot with signals (when to buy, when to sell) and adds risk (stop loss, take profit):
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 %
});
The functions in the declaration — plot, alerts, signals — are called once with the whole data series, not once per candle. They "assemble" the calculation; the engine computes that assembly on every candle and then updates the last candle with each new price.
Two ways to calculate: whole series or per candle
Whole series (plot, signals, alerts) | Per candle (onBar) | |
|---|---|---|
| Written with | Series methods: .gt(), .add(), ta.* | Plain JavaScript on numbers: >, +, if, loops |
| When to use | Most indicators and strategies | When you need to remember across candles (counting, state), or draw labels / boxes / tables |
| Fast, can screen a whole basket | Yes | Slower; cannot screen a whole basket yet |
Start with the first approach; switch to onBar only when you really need it — Lesson 6 teaches how. For example, an indicator counting consecutive up candles must remember the previous candle's count, so it uses 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" }) }),
});
In onBar, ctx.close and ctx.open are numbers for the candle being calculated, so you use > as usual.
Plot panes and scales
pane: "price"— draws over the price candles, sharing the price scale (moving averages, Bollinger Bands, signal arrows).pane: "new"(default) — a separate pane below the chart with its own scale (RSI, MACD, volume).- A single output can be placed in a different pane from the rest of the indicator:
line(x, { pane: "price" })in a separate-pane indicator. scale: { range: [0, 100], levels: [30, 70] }fixes a separate pane's scale from 0 to 100 and draws levels at 30 and 70.
Parameters
"Hard-coded" numbers in your code (period 14, threshold 70) should become parameters, so they can be adjusted without editing code and so optimization can try many values. param.int(14, { min: 2, max: 100, label: "Chu kỳ" }) creates an integer field; its value is available as p.<tên> (name) in every function of the declaration. All parameter types are in Lesson 4.
Minimal JavaScript
FireAnt Script is JavaScript. You only need the parts below — enough for every example in this chapter.
| Code | Meaning |
|---|---|
// chú thích | A comment for readers; the engine ignores it |
const x = 5; | Names the value 5 as x (cannot be changed afterward) |
let n = 0; n = n + 1; | A variable that can change (used in onBar) |
"chữ", EMA ${n} | Text strings; inside backticks ````` the ${…} inserts a value |
{ a: 1, "Tên có dấu cách": 2 } | An object: name–value pairs. Names with spaces / Vietnamese diacritics go in double quotes |
[1, 2, 3] | An array (list) |
(d) => d.close | A short function that takes d and returns d.close |
(d, p) => ({ A: … }) | A function returning an object: wrap the object in parentheses ( … ) |
(d) => { const x = …; return { … }; } | A multi-line function: the body goes in { } and ends with return |
if (a > b) { … } else { … } | Branching (in onBar) |
a > b ? x : y | "If a > b then x, otherwise y" |
Math.max(a, b), Math.abs(x), Math.round(x) | JavaScript math functions (on numbers) |
Number.isNaN(x) | Whether x is NaN |
No import, no network calls, no file access: scripts run in a sandbox isolated from the page, with 2 seconds to calculate the whole history and 50 ms for each price update.
Next
이 페이지는 모든 스크립트를 읽기 위해 한 번은 이해해야 할 개념을 설명합니다. 캔들, 시계열, 그리기 패널, 매개변수, 두 가지 계산 방식, 그리고 지표를 작성하기에 충분한 JavaScript입니다. 읽는 데 약 15분 걸립니다.
캔들과 데이터 d
차트는 캔 들의 연속입니다. 한 시간 프레임(1분, 15분, 1일, 1주…)의 각 캔들에는 다섯 개의 숫자가 있습니다.
| 스크립트에서의 이름 | 의미 |
|---|---|
d.open | 시가 |
d.high | 고가 |
d.low | 저가 |
d.close | 종가(진행 중인 캔들: 최근 체결가) |
d.volume | 체결량 |
d는 스크립트가 받는 차트 데이터로, 지금 보고 있는 종목과 시간 프레임 그대로입니다. 차트에서 종목이나 시간 프레임을 바꾸면 스크립트가 새 데이터로 자동 재계산합니다. 위 다섯 값 외에도 d에는 d.hl2((고가 + 저가) / 2), d.hlc3, d.ohlc4, d.time(캔들 시작 시점), d.index(캔들 순번), 그리고 더 큰 시간 프레임의 캔들이 있습니다. 전체 목록은 레퍼런스 › 데이터에 있습니다.
차트의 베트남 주식 가격은 천 동 단위입니다(FPT 98.5 = 98,500동). 거래량은 주 단위입니다. 지수(VNINDEX)는 포인트 단위입니다.
시계열: 캔들별 숫자 열 전체
d.close는 숫자 하나가 아니라 모든 캔들의 종가 열 전체로, 시계열(Series)이라고 부릅니다. 시계열에 대한 모든 계산도 시계열을 만듭니다.
| 캔들 | d.close | ta.sma(d.close, 3) | d.close.gt(ta.sma(d.close, 3)) |
|---|---|---|---|
| 1 | 10 | NaN | NaN |
| 2 | 11 | NaN | NaN |
| 3 | 12 | 11 | 1 |
| 4 | 11 | 11.33 | 0 |
| 5 | 13 | 12 | 1 |
ta.sma(d.close, 3)은 3캔들 평균 열 전체입니다. 처음 두 캔들은 값이 3개가 되지 않으므로 NaN, 즉 "알 수 없음"입니다.d.close.gt(…)("보다 크다")는 조건 시계열입니다. 1 = 참, 0 = 거짓, NaN = 알 수 없음.
시계열은 숫자 열 전체이므로 두 시계열 사이에 JavaScript의 >, +, &&를 쓸 수 없습니다(JavaScript는 두 열을 더하는 법을 모릅니다). 대신 각 연산은 점 뒤에 쓰는 메서드입니다.
| 쓰고 싶은 것 | FireAnt Script에서 쓰는 법 |
|---|---|
close > sma | d.close.gt(sma) |
close >= sma, <, <=, ==, != | .gte(sma), .lt(…), .lte(…), .eq(…), .neq(…) |
a + b, a - b, a * b, a / b | a.add(b), a.sub(b), a.mul(b), a.div(b) |
cond1 && cond2, cond1 || cond2, !cond | cond1.and(cond2), cond1.or(cond2), cond.not() |
close[1](이전 캔들) | d.close.shift(1) |
| "상향 교차", "하향 교차" | a.crossAbove(b), a.crossBelow(b) |
오른쪽 항은 시계열이나 숫자일 수 있습니다: ta.rsi(d.close, 14).gt(70), d.close.mul(1.05). 이어지는 연산은 왼쪽에서 오른쪽으로 읽습니다: d.high.sub(d.low).div(d.close).mul(100) = (고가 − 저가) / 종가 × 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" }),
};
},
});
ta.rsi(d.close, 14) > 70은 JavaScript에서 두 객체를 비교하는 것이라 항상 false가 되어, 표시가 절대 나타나지 않습니다. 편집기가 빨간 밑줄을 긋고 Không dùng toán tử '>' cho kiểu 'Series' và 'number' — với chuỗi dùng .add() / .sub() / .gt()…('Series'와 'number' 타입에 '>' 연산자를 쓰지 마세요 — 시계열에는 .add() / .sub() / .gt()… 사용)라고 알립니다. 빨간 밑줄이 보이면 무시하지 말고 바로 고치세요.
NaN, 워밍업, 3상태 논리
- NaN("not a number")은 알 수 없음을 뜻합니다. 이동평균의 첫 캔들, 0으로 나누기, 값이 없는 경우입니다. NaN은 계산을 따라 퍼집니다: NaN + 1 = NaN. 차트에서 NaN인 곳은 그려지지 않습니다.
- 워밍업: 지표는 처음 몇 캔들 동안 "예열"이 필요합니다(SMA 20은 20캔들, EMA, RSI는 안정되기까지 더 오래 걸림). 차트는 보고 있는 구간 앞의 캔들을 자동으로 더 불러오며, 워밍업 구간에서는 그리지 않고 알림과 주문도 평가하지 않습니다.
- 조건에는 세 가지 상태가 있습니다: 참(1), 거짓(0), 알 수 없음(NaN). 표시, 배경 칠하기, 알림, 신호는 참일 때만 켜지며, NaN은 참이 아닌 것으로 취급됩니다. "알 수 없음"을 명확히 거짓으로 보려면
.fillna(0)을 쓰고, 조건을 안전하게 뒤집으려면.fillna(0).not()으로 씁니다.
미래를 미리 보지 않기
각 캔들에서 스크립트는 그 캔들과 이전 캔들만 봅니다. 실제 거래할 때와 똑같습니다. d.close.shift(1)은 이전 캔들의 종가이고, shift(5)는 5캔들 전입니다. 음수(shift(-1) — 다음 캔들)는 막혀 있습니다.
// ✗ lỗi: lookahead
indicator({
name: "Nhìn trước (sai)",
plot: (d) => ({ "Ngày mai": line(d.close.shift(-1)) }),
});
이 스크립트는 shift(n) với n âm dùng dữ liệu tương lai — không được phép(음수 n의 shift(n)은 미래 데이터를 사용 — 허용되지 않음) 오류로 멈춥니다. 덕분에 과거 데이터에서 보는 신호는 그 시점에 실제로 있었던 신호이며, 가격이 움직인 뒤에 "다시 그려지지" 않습니다.
진행 중인 캔들(아직 마감되지 않은 세션)은 다릅니다. 가격이 계속 바뀌므로 마지막 캔들의 표시 / 신호가 나타났다가 사라질 수 있습니다. 전략과 알림에는 마감된 캔들에서만 판단하는 옵션이 있습니다(5강, 9강).
스크립트의 구조
스크립트는 indicator({...})(지표) 또는 strategy({...})(전략) 한 번의 호출입니다. 안에는 여러 항목이 있으며, 필요 없는 항목은 생략합니다.
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),
}),
});
전략은 plot 대신 signals(언제 매수, 언제 매도)를 쓰고 risk(손절, 익절)를 추가합니다.
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 %
});
선언 안의 함수들 — plot, alerts, signals — 은 캔들마다 한 번이 아니라 데이터 시계열 전체로 한 번 호출됩니다. 이 함수들은 계산을 "조립"하고, 실행기가 그 조립을 모든 캔들에서 계산한 뒤 새 가격이 들어올 때마다 마지막 캔들을 갱신합니다.
두 가지 계산 방식: 시계열 전체 또는 캔들별
시계열 전체(plot, signals, alerts) | 캔들별(onBar) | |
|---|---|---|
| 작성 방법 | 시계열 메서드: .gt(), .add(), ta.* | 숫자에 대한 일반 JavaScript: >, +, if, 반복문 |
| 언제 쓰나 | 대부분의 지표와 전략 | 캔들을 넘어 기억해야 할 때(세기, 상태), 라벨 / 박스 / 표 그리기 |
| 빠르고 바스켓 전체 필터 가능 | 예 | 더 느림; 아직 바스켓 전체 필터 불가 |
첫 번째 방식으로 시작하고, 정말 필요할 때만 onBar로 바꾸세요. 사용법은 6강에서 다룹니다. 예를 들어 연속 상승 캔들 수를 세는 지표는 이전 캔들의 숫자를 기억해야 하므로 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" }) }),
});
onBar 안에서 ctx.close, ctx.open은 계산 중인 캔들의 숫자이므로 >를 평소처럼 씁니다.
그리기 패널과 스케일
pane: "price"— 가격 캔들 위에 겹쳐 그리고 가격 스케일을 공유합니다(이동평균선, 볼린저 밴드, 신호 화살표).pane: "new"(기본값) — 차트 아래의 별도 패널, 별도 스케일(RSI, MACD, 거래량).- 출력 하나를 지표 전체와 다른 패널에 둘 수 있습니다: 별도 패널 지표 안에서
line(x, { pane: "price" }). scale: { range: [0, 100], levels: [30, 70] }은 별도 패널의 스케일을 0에서 100으로 고정하고 30, 70 수준선을 긋습니다.
매개변수
코드 안의 "고정" 숫자(기간 14, 기준값 70)는 코드를 고치지 않고 조정할 수 있고 최적화에서 여러 값을 시험할 수 있도록 매개변수로 만드는 것이 좋습니다. param.int(14, { min: 2, max: 100, label: "Chu kỳ" })는 정수 칸을 만들며, 값은 선언의 모든 함수에서 p.<이름>에 있습니다. 매개변수 유형 전체는 4강에 있습니다.
최소한의 JavaScript
FireAnt Script는 JavaScript입니다. 아래 내용만 알면 이 장의 모든 예제에 충분합니다.
| 쓰기 | 의미 |
|---|---|
// chú thích | 읽는 사람을 위한 주석, 실행기는 무시 |
const x = 5; | 값 5에 이름 x를 붙임(이후 변경 불가) |
let n = 0; n = n + 1; | 변경 가능한 변수(onBar에서 사용) |
"chữ", EMA ${n} | 문자열; 백틱 ````` 안에서는 ${…}로 값을 삽입 |
{ a: 1, "Tên có dấu cách": 2 } | 객체: 이름 – 값 쌍. 이름에 공백 / 베트남어 성조가 있으면 큰따옴표로 감쌈 |
[1, 2, 3] | 배열(목록) |
(d) => d.close | d를 받아 d.close를 반환하는 짧은 함수 |
(d, p) => ({ A: … }) | 객체를 반환하는 함수: 객체를 소괄호 ( … )로 감쌈 |
(d) => { const x = …; return { … }; } | 여러 줄 함수: 본문은 { } 안, return으로 끝남 |
if (a > b) { … } else { … } | 분기(onBar 안에서) |
a > b ? x : y | "a > b이면 x, 아니면 y" |
Math.max(a, b), Math.abs(x), Math.round(x) | JavaScript 수학 함수(숫자에 대해) |
Number.isNaN(x) | x가 NaN인지 여부 |
import가 없고, 네트워크 호출도, 파일 읽기도 없습니다. 스크립트는 페이지와 분리된 샌드박스에서 실행되며, 전체 과거 데이터 계산에 2초, 가격 갱신마다 50 ms가 주어집니다.
다음 단계
本页解释读懂任何脚本都需要理解一次的概念:K 线、序列、绘图窗格、参数、两种计算方式,以及足以编写指标的 JavaScript 知识。阅读约需 15 分钟。
K 线与数据 d
图表是一串 K 线。某个周期(1 分钟、15 分钟、1 天、1 周…)的每根 K 线都有五个数字:
| 脚本中的名称 | 含义 |
|---|---|
d.open | 开盘价 |
d.high | 最高价 |
d.low | 最低价 |
d.close | 收盘价(正在形成的 K 线:最新成交价) |
d.volume | 成交量 |
d 是脚本接收到的图表数据:正是您所查看的代码和周期。在图表上更换代码或周期,脚本就会在新数据上自动重新计算。除上述五个值外,d 还有 d.hl2((最高 + 最低) / 2)、d.hlc3、d.ohlc4、d.time(K 线开盘时间)、d.index(K 线序号)以及更大周期的 K 线——完整内容见参考 › 数据。
图表上越南股票的价格以千越南盾计(FPT 98.5 = 98,500 越南盾)。成交量以股计。指数(VNINDEX)以点计。
序列:按 K 线排列的一整列数字
d.close 不是一个数字,而是所有 K 线收盘价组成的一整列——称为序列(Series)。对序列的任何计算结果也是序列:
| K 线 | d.close | ta.sma(d.close, 3) | d.close.gt(ta.sma(d.close, 3)) |
|---|---|---|---|
| 1 | 10 | NaN | NaN |
| 2 | 11 | NaN | NaN |
| 3 | 12 | 11 | 1 |
| 4 | 11 | 11.33 | 0 |
| 5 | 13 | 12 | 1 |
ta.sma(d.close, 3)是整列 3 根 K 线平均值。前两根 K 线还不够 3 个值,所以是 NaN——“未知”。d.close.gt(…)(“大于”)是一个条件序列:1 = 成立,0 = 不成立,NaN = 未知。
由于序列是一整列数字,您不能在两个序列之间使用 JavaScript 的 >、+、&&(JavaScript 不知道如何把两列相加)。取而代之,每种运算都是写在点号之后的方法:
| 您想写 | 在 FireAnt Script 中写作 |
|---|---|
close > sma | d.close.gt(sma) |
close >= sma, <, <=, ==, != | .gte(sma), .lt(…), .lte(…), .eq(…), .neq(…) |
a + b, a - b, a * b, a / b | a.add(b), a.sub(b), a.mul(b), a.div(b) |
cond1 && cond2, cond1 || cond2, !cond | cond1.and(cond2), cond1.or(cond2), cond.not() |
close[1](上一根 K 线) | d.close.shift(1) |
| “上穿”、“下穿” | a.crossAbove(b), a.crossBelow(b) |
右侧可以是序列或数字:ta.rsi(d.close, 14).gt(70)、d.close.mul(1.05)。连续的运算从左到右读:d.high.sub(d.low).div(d.close).mul(100) = (最高 − 最低) / 收盘 × 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" }),
};
},
});
ta.rsi(d.close, 14) > 70 在 JavaScript 中是比较两个对象,结果总是 false:标记永远不会出现。编辑器会画红线并提示 Không dùng toán tử '>' cho kiểu 'Series' và 'number' — với chuỗi dùng .add() / .sub() / .gt()…(不能对 'Series' 和 'number' 类型使用运算符 '>'——序列请用 .add() / .sub() / .gt()…)。看到红线就立即修改,不要忽略。
NaN、预热与三态逻辑
- NaN(“not a number”)表示未知:移动平均线的最初几根 K 线、除以 0、不存在的值。NaN 会随计算传播:NaN + 1 = NaN。在图表上,NaN 处不绘制。
- 预热:指标需要最初一些 K 线来“热身”(SMA 20 需要 20 根 K 线;EMA、RSI 需要更久才能稳定)。图表会自动加载可视区域之前的更多 K 线,并且在预热段内不绘制、不评估提醒和委托。
- 条件有三种状态:成立(1)、不成立(0)、未知(NaN)。标记、背景着色、提醒、信号只在成立时触发;NaN 视为不成立。想把“未知”明确当作不成立,使用
.fillna(0);想安全地对条件取反,写.fillna(0).not()。
不偷看未来
在每根 K 线上,脚本只能看到该 K 线及之前的 K 线——与真实交易完全一样。d.close.shift(1) 是上一根 K 线的收盘价;shift(5) 是 5 根之前。负数(shift(-1)——后面的 K 线)会被禁止:
// ✗ lỗi: lookahead
indicator({
name: "Nhìn trước (sai)",
plot: (d) => ({ "Ngày mai": line(d.close.shift(-1)) }),
});
该脚本会以错误 shift(n) với n âm dùng dữ liệu tương lai — không được phép(n 为负数的 shift(n) 使用未来数据——不允许)停止。因此,您在历史上看到的信号就是当时真实存在的信号——不会在价格走出来之后“重绘”。
正在形成的 K 线(交易时段尚未收盘)则不同:价格仍在变动,所以最后一根 K 线的标记 / 信号可能出现后又消失。策略和提醒都有只在已收盘 K 线上检查的选项(第 5 课、第 9 课)。
脚本的结构
一个脚本就是一次 indicator({...})(指标)或 strategy({...})(策略)调用。里面是各个项,不需要的项可以省略:
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),
}),
});
策略用 signals(何时买入、何时卖出)代替 plot,并加上 risk(止损、止盈):
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 %
});
声明中的函数——plot、alerts、signals——以整个数据序列为输入只调用一次,而不是每根 K 线调用一次。它们负责“组装”计算;运行器在所有 K 线上计算这个组装结果,然后随每个新价格更新最后一根 K 线。
两种计算方式:整个序列或逐根 K 线
整个序列(plot、signals、alerts) | 逐根 K 线(onBar) | |
|---|---|---|
| 写法 | 序列的方法:.gt()、.add()、ta.* | 对数字使用普通 JavaScript:>、+、if、循环 |
| 何时使用 | 大多数指标和策略 | 需要跨 K 线记忆(计数、状态),绘制标签 / 方框 / 表格 |
| 快速,可筛选整个篮子 | 是 | 较慢;暂不能筛选整个篮子 |
从第一种方式开始;只有真正需要时才改用 onBar——第 6 课讲解用法。例如统计连续阳线数量的指标必须记住上一根 K 线的数字,所以要用 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" }) }),
});
在 onBar 中,ctx.close、ctx.open 是当前计算 K 线的数字,所以可以照常使用 >。
绘图窗格与刻度
pane: "price"——叠加在价格 K 线上绘制,共用价格刻度(移动平均线、Bollinger 通道、信号箭头)。pane: "new"(默认)——图表下方的独立窗格,使用独立刻度(RSI、MACD、成交量)。- 单个输出可以放在与整个指标不同的窗格中:在独立窗格指标中写
line(x, { pane: "price" })。 scale: { range: [0, 100], levels: [30, 70] }将独立窗格的刻度固定为 0 到 100,并画出 30、70 水平线。
参数
代码中的“硬”数字(周期 14、阈值 70)应改成参数,这样无需改代码即可调整,也便于优化时尝试多个值。param.int(14, { min: 2, max: 100, label: "Chu kỳ" }) 创建一个整数框;其值在声明的所有函数中位于 p.<tên>。全部参数类型见第 4 课。
最基本的 JavaScript
FireAnt Script 就是 JavaScript。您只需要下面这些——足够应付本章所有示例。
| 写法 | 含义 |
|---|---|
// chú thích | 给读者看的注释,运行器会忽略 |
const x = 5; | 为值 5 起名为 x(之后不能再改) |
let n = 0; n = n + 1; | 可变的变量(在 onBar 中使用) |
"chữ", EMA ${n} | 文字字符串;在 ````` 中,${…} 会插入值 |
{ a: 1, "Tên có dấu cách": 2 } | 对象:名称–值对。名称含空格 / 越南语声调符号时要放在双引号中 |
[1, 2, 3] | 数组(列表) |
(d) => d.close | 接收 d、返回 d.close 的简短函数 |
(d, p) => ({ A: … }) | 返回对象的函数:把对象包在圆括号 ( … ) 中 |
(d) => { const x = …; return { … }; } | 多行函数:函数体在 { } 中,以 return 结束 |
if (a > b) { … } else { … } | 分支(在 onBar 中) |
a > b ? x : y | “如果 a > b 则 x,否则 y” |
Math.max(a, b), Math.abs(x), Math.round(x) | JavaScript 的数学函数(作用于数字) |
Number.isNaN(x) | x 是否为 NaN |
没有 import,不能访问网络,不能读取文件:脚本在与页面隔离的沙箱中运行,计算整个历史有 2 秒,每次价格更新有 50 ms。