Chiến lược
- 🇻🇳 Tiếng Việt
- 🇬🇧 English
- 🇰🇷 한국어
- 🇨🇳 中文
Các mục của strategy({...}) quyết định khi nào mua / bán và kiểm thử khớp lệnh thế nào. Hướng dẫn từng bước: Bài 9, Bài 10, Bài 11.
Mọi cấu hình (risk, portfolio, execution, costs, rules) viết được dạng đối tượng hoặc hàm của tham số: risk: (p) => ({ stopLoss: p.sl }).
signals
signals: (d, p) => dieuKien | { entry, exit?, score?, limit?, stop?, short? }
| Khoá | Kiểu | Nghĩa |
|---|---|---|
entry | điều kiện, bắt buộc | Mua khi đang không giữ |
exit | điều kiện | Bán (đóng vị thế) khi đang giữ. Cùng nến có entry và exit thì không mua |
score | chuỗi | số | Ưu tiên khi nhiều tín hiệu hơn chỗ trống (rổ nhiều mã trong Studio); một mã trên biểu đồ không dùng |
limit | chuỗi | số (giá) | Lệnh vào thành lệnh giới hạn ở mức này cho nến kế tiếp (chỉ khớp nếu giá xuống tới mức) |
stop | chuỗi | số (giá) | Lệnh vào thành lệnh dừng ở mức này (chỉ khớp nếu giá vượt lên mức). Có cả limit và stop: lệnh dừng – giới hạn |
short | { entry, exit?, limit?, stop? } | Vị thế bán khống — chỉ mã cho phép (hợp đồng tương lai); đang mua mà có tín hiệu bán khống thì đảo chiều |
Trả thẳng một điều kiện thì đó là entry. Trên biểu đồ, tín hiệu hiện ▲ (vào) / ▼ (ra) theo cách: vào khi entry đúng lúc chưa giữ, ra khi exit đúng lúc đang giữ (thiếu exit: ra khi entry hết đúng — riêng bảng kiểm thử thì giữ tới khi risk đóng vị thế).
filter
filter: (d, p) => dieuKien — chỉ cho vào lệnh ở nến điều kiện đúng (như thêm .and(filter) vào entry). Trong Studio kết hợp với lọc thanh khoản của rổ.
risk
| Khoá | Nghĩa |
|---|---|
stopLoss | Dừng lỗ: bán khi giá giảm stopLoss (tỷ lệ) so với giá vốn |
takeProfit | Chốt lời: bán khi lãi takeProfit |
trailingStop | Dừng lỗ kéo theo: bán khi giá giảm trailingStop so với mức cao nhất từ lúc mua |
maxHoldBars | Giữ tối đa số nến (của khung đang tính) |
maxHoldDays | Tên cũ của Quant: số phiên |
stopMode | "close" (mặc định): xét ngưỡng theo giá đóng cửa, bán ở nến sau. "intraday": khớp tại ngưỡng trong nến (giá mở cửa đã vượt ngưỡng → khớp giá mở cửa) |
Lý do ra lệnh hiện ở thẻ Giao dịch: Tín hiệu bán, Dừng lỗ, Chốt lời, Dừng lỗ kéo theo, Hết thời gian…
portfolio
| Khoá | Mặc định | Nghĩa |
|---|---|---|
capital | 1.000.000.000 | Vốn ban đầu (đồng) |
sizing | "equal" | Mỗi lần mua: "equal" = toàn bộ vốn cho mã đang xem (rổ trong Studio: vốn ÷ số vị thế tối đa); số 0–1 = tỷ trọng vốn |
pyramiding | 1 | Số lần được vào cùng chiều (1 = không mua thêm khi đang giữ) |
maxPositions | — | Số mã giữ cùng lúc (rổ trong Studio) |
cashRate | 0 | Lãi suất năm cho tiền nhàn rỗi |
execution
| Khoá | Mặc định | Nghĩa |
|---|---|---|
at | "open" | Lệnh thị trường từ tín hiệu khớp giá mở cửa ("open") hoặc đóng cửa ("close") |
delay | 1 | Khớp ở nến thứ delay sau nến tín hiệu. at: "close", delay: 0 = khớp ngay giá đóng cửa nến tín hiệu (lạc quan) |
intrabar | "ohlc" | Đường đi giá trong nến khi xét lệnh giới hạn / dừng: O→H→L→C nếu cao gần mở hơn, ngược lại O→L→H→C. "1m": dùng nến 1 phút khi có |
validBars | 1 | Lệnh giới hạn / dừng từ signals.limit / stop còn hiệu lực bao nhiêu nến |
costs
| Khoá | Mặc định cổ phiếu | Mặc định phái sinh | Nghĩa |
|---|---|---|---|
fees | 0,0015 | 0 | Phí mỗi chiều (tỷ lệ giá trị) |
sellTax | 0,001 | 0 | Thuế bán |
slippage | 0,001 | 1 bước giá | Trượt giá mỗi lệnh |
impact | 0,01 | 0 | Trượt thêm = impact × √(KL lệnh ÷ KL TB 20) — lệnh càng lớn so với thanh khoản càng trượt |
feePerContract | — | 5.000 | Phái sinh: phí mỗi hợp đồng mỗi chiều (đồng) |
rules
Luật thị trường áp theo loại mã (market: "auto"): cổ phiếu Việt Nam ("vn-equity"), phái sinh ("vn-derivative"), hoặc "none" (không luật).
| Khoá | Mặc định cổ phiếu | Nghĩa |
|---|---|---|
market | "auto" | Bộ luật |
settlement | 2 | Số phiên thanh toán (T+2) |
sellFrom | "afternoon" | Hàng về bán được từ chiều T+2 (nến ngày: ATC T+2); "next-open": mở cửa T+3 |
lotSize | 100 | Lô giao dịch |
priceLimit | "exchange" | Biên độ theo sàn (HOSE 7 %, HNX 10 %, UPCoM 15 %); số tự đặt; null bỏ biên độ |
tickSize | "exchange" | Bước giá theo sàn; số tự đặt; null không làm tròn |
limitLock | true | Không mua được khi trắng bên bán ở giá trần, không bán được khi trắng bên mua ở giá sàn |
retryBlockedBuys | false | Lệnh mua bị chặn (trần, thiếu tiền…) có thử lại ở nến sau không |
maxAdvPct | 0,1 | Mỗi lệnh tối đa tỷ lệ này của KL trung bình 20 phiên trước (0 = không giới hạn) |
allowShort | false (cổ phiếu) | Cho bán khống |
flatAt | — | Phái sinh: đóng hết vị thế từ giờ này ("14:25") |
multiplier, marginRate | — | Phái sinh: đồng mỗi điểm, tỷ lệ ký quỹ |
strategy({
name: "Chiến lược khai báo đủ cấu hình",
params: {
n: param.int(20, { min: 10, max: 60, label: "Đỉnh n nến" }),
sl: param.pct(0.06, { min: 0.02, max: 0.15, label: "Dừng lỗ" }),
},
filter: (d) => d.close.gt(ta.sma(d.close, 100)),
signals: (d, p) => ({
entry: d.close.gt(d.high.highest(p.n).shift(1)),
exit: d.close.lt(ta.ema(d.close, 20)),
limit: d.close.mul(1.01),
}),
risk: (p) => ({ stopLoss: p.sl, takeProfit: 0.3, trailingStop: 0.12, maxHoldBars: 60, stopMode: "close" }),
portfolio: { capital: 500000000, sizing: 0.8, pyramiding: 1, cashRate: 0.02 },
execution: { at: "open", delay: 1, intrabar: "ohlc", validBars: 2 },
costs: { fees: 0.0015, sellTax: 0.001, slippage: 0.0015, impact: 0.01 },
rules: { market: "auto", settlement: 2, sellFrom: "afternoon", lotSize: 100, priceLimit: "exchange", tickSize: "exchange", limitLock: true, retryBlockedBuys: false, maxAdvPct: 0.1 },
plot: (d, p) => ({ [`Đỉnh ${p.n} nến trước`]: line(d.high.highest(p.n).shift(1), { color: "up", step: true, dash: "dash" }) }),
});
Cách khớp lệnh khi kiểm thử
- Tín hiệu tính ở nến đã đóng
t(không nhìn trước). - Lệnh thị trường khớp theo
execution(mặc định giá mở cửa nếnt + 1), cộng trượt giá; lệnh giới hạn / dừng xét ở nếnt + 1theo đường đi giá trong nến. - Khối lượng: theo
sizing, làm tròn xuống lô, không quámaxAdvPctkhối lượng trung bình; thiếu tiền → mua ít hơn. - Luật: không mua ở trần khi trắng bên bán, không bán ở sàn khi trắng bên mua; hàng chưa về (T+2) chưa bán được — lệnh bán chờ.
riskxét mỗi nến sau khi mua.- Trong một nến, thứ tự chạm giá mặc định theo đường đi O–H–L–C ước lượng (lệnh khớp kiểu này được đánh dấu "ước lượng"). Ô Khớp trong nến › Kính lúp nến 1 phút của bảng kiểm thử (hoặc
execution.intrabar: "1m") khớp theo nến 1 phút thật bên trong; nến cũ hơn dữ liệu 1 phút lùi về đường đi ước lượng. Ô Trễ lệnh (1–3 nến) đổi nến khớp của lệnh thị trường cho lần chạy đó, không sửa mã.
Kết quả kiểm thử
| Chỉ số | Nghĩa |
|---|---|
| Lợi nhuận | Lãi / lỗ cả kỳ ÷ vốn ban đầu, sau phí, thuế, trượt giá |
| CAGR | Lợi nhuận kép năm |
| Sharpe | Lợi nhuận vượt lãi suất phi rủi ro (3 %/năm) ÷ độ biến động, quy năm |
| Sụt giảm tối đa | Mức giảm sâu nhất của vốn từ đỉnh |
| Số giao dịch, Tỷ lệ thắng | Số lần mua – bán đã đóng; phần trăm có lãi |
| Profit factor | Tổng lãi ÷ tổng lỗ |
| Thời gian có vị thế | Phần trăm thời gian đang giữ |
| MAE, MFE (mỗi giao dịch) | Lỗ / lãi tạm lớn nhất trong lúc giữ |
| Mua và giữ | Đường vốn nếu mua ở đầu kỳ và giữ tới cu ối — để so |
Mục dành cho Quant AI Studio
Trên biểu đồ chiến lược luôn chạy trên mã đang xem; các mục dưới được bỏ qua và dùng khi Mở trong Quant AI Studio.
| Mục | Nghĩa |
|---|---|
universe | Rổ mã: universe("VN30"), universe("HOSE").liquid(5e9), universe(["FPT", "HPG"]), universe({ industry: "Ngân hàng" }), universe({ watchlist: "Tên" }). Nối thêm .exchange("HOSE"), .industry(…), .exclude([...]), .include([...]), .minPrice(10), .top(50), .where((m) => …). Chi tiết: Quant AI Studio › Rổ mã |
period | { from: "2018-01-01", to? } — kỳ kiểm thử |
benchmark | Chỉ số so sánh (mặc định VNINDEX; null = không) |
warmup | Số phiên khởi động (mặc định 250) |
rank + select, weights, rebalance | Chiến lược xoay vòng / tỷ trọng theo rổ Studio — Quant AI Studio › Chiến lược |
optimize | { objective?: "sharpe" | "cagr" | "calmar" | "sortino", grid?: { tham_số: [giá trị…] }, maxCombos?, tests?: ["oos", "walkforward", "montecarlo", "costs", "delay", "regimes"], walkForward?: { folds, anchored, trainPct }, oos?: { splitPct } } — tối ưu và kiểm định độ vững trong Studio. Thẻ Tối ưu trên biểu đồ dùng min / max của tham số |
Lệnh trong onBar (lệnh giấy)
Thay cho signals, chiến lược viết được bằng lệnh giấy trong onBar (như strategy.entry / strategy.exit của Pine) — khi logic vào / ra cần trạng thái theo từng nến:
| Lệnh | Nghĩa |
|---|---|
ctx.buy(qty?, opts?), ctx.sell(qty?, opts?) | Lệnh mua / bán; opts như ctx.order (type, price, stopPrice, tif…). Thiếu qty: theo portfolio.sizing |
ctx.order({ side, type, qty, price, stopPrice, tif, oco, reduceOnly, comment }) | Đặt / sửa lệnh; trả id. type: thị trường, giới hạn, dừng, dừng – giới hạn; tif: "bar" (một nến), "day" (hết phiên), "gtc" (tới khi huỷ); oco: nhóm huỷ lẫn nhau |
ctx.exit({ id?, from?, qty?, limit?, stop?, trailPct?, trailPoints? }) | Lệnh thoát gắn với vị thế: chốt lời limit, cắt lỗ stop, dừng k éo theo — cùng một nhóm OCO, chỉ giảm vị thế. Gọi lại cùng id = sửa |
ctx.closePosition({ qty? }) | Đóng vị thế (hoặc một phần) bằng lệnh thị trường |
ctx.cancel(id), ctx.cancelAll() | Huỷ lệnh chờ |
ctx.position | side ("flat", "long", "short"), qty, sellable (bán được ngay — cổ phiếu đã qua T+2), avgPrice, openPnl, openPnlPct, bars, entryTime |
ctx.orders, ctx.equity, ctx.cash | Lệnh đang có (status, lý do bị từ chối: lot, price-limit, t2, short, cash…), vốn, tiền mặt |
Lệnh đặt ở nến t xét khớp từ nến t + 1 theo cùng luật thị trường và execution như signals; nến khởi động (ctx.warmup) không đặt lệnh. Bảng Kiểm thử chiến lược chạy kiểu này khi chiến lược đã Thêm vào biểu đồ (cả Tối ưu, Walk-forward). Chiến lược onBar chưa dùng được cho Lọc bằng script, và không có dấu ▲ / ▼ tín hiệu như signals — lệnh khớp hiện trên biểu đồ sau khi chạy kiểm thử.
// Chiến lược onBar: mua khi vượt đỉnh 20 phiên, thoát bằng cắt lỗ 2 × ATR dưới giá vốn hoặc dừng kéo theo 10 %
strategy({
name: "Bứt phá đỉnh 20 phiên, cắt lỗ theo ATR",
series: (d) => ({ dinh: d.high.highest(20).shift(1), atr: ta.atr(d, 14) }),
onBar: (ctx) => {
if (ctx.position.side === "flat" && ctx.close > ctx.v.dinh) ctx.buy();
if (ctx.position.side === "long") ctx.exit({ id: "thoat", stop: ctx.position.avgPrice - 2 * ctx.v.atr, trailPct: 0.1 });
},
});
The entries of strategy({...}) decide when to buy / sell and how the test fills orders. Step-by-step guides: Lesson 9, Lesson 10, Lesson 11.
Every configuration (risk, portfolio, execution, costs, rules) can be written as an object or as a function of the parameters: risk: (p) => ({ stopLoss: p.sl }).
signals
signals: (d, p) => dieuKien | { entry, exit?, score?, limit?, stop?, short? }
| Key | Type | Meaning |
|---|---|---|
entry | condition, required | Buy when not holding |
exit | condition | Sell (close the position) when holding. If entry and exit are both true on the same bar, no buy |
score | series | number | Priority when there are more signals than open slots (multi-ticker baskets in Studio); not used for a single ticker on the chart |
limit | series | number (price) | The entry becomes a limit order at this level for the next bar (fills only if price falls to the level) |
stop | series | number (price) | The entry becomes a stop order at this level (fills only if price rises to the level). With both limit and stop: a stop-limit order |
short | { entry, exit?, limit?, stop? } | Short position — only for tickers that allow it (futures); a short signal while long reverses the position |
Returning a single condition directly makes it the entry. On the chart, signals show ▲ (entry) / ▼ (exit) like this: entry when entry is true while not holding, exit when exit is true while holding (without exit: exit when entry stops being true — except in the tester panel, which holds until risk closes the position).
filter
filter: (d, p) => dieuKien — only allows entries on bars where the condition is true (like adding .and(filter) to entry). In Studio it combines with the basket's liquidity filter.
risk
| Key | Meaning |
|---|---|
stopLoss | Stop loss: sell when price falls by stopLoss (ratio) from the cost price |
takeProfit | Take profit: sell when the gain reaches takeProfit |
trailingStop | Trailing stop: sell when price falls by trailingStop from the highest level since buying |
maxHoldBars | Maximum holding period in bars (of the current timeframe) |
maxHoldDays | Quant's old name: number of sessions |
stopMode | "close" (default): the threshold is checked against the close, selling on the next bar. "intraday": fills at the threshold within the bar (if the open has already crossed the threshold → fills at the open) |
The exit reason appears in the Giao dịch (Trades) tab: Tín hiệu bán (Sell signal), Dừng lỗ (Stop loss), Chốt lời (Take profit), Dừng lỗ kéo theo (Trailing stop), Hết thời gian (Time expired)…
portfolio
| Key | Default | Meaning |
|---|---|---|
capital | 1,000,000,000 | Starting capital (VND) |
sizing | "equal" | Each buy: "equal" = all capital into the ticker being viewed (baskets in Studio: capital ÷ maximum positions); a number 0–1 = fraction of capital |
pyramiding | 1 | Number of entries allowed in the same direction (1 = no adding while holding) |
maxPositions | — | Number of tickers held at once (baskets in Studio) |
cashRate | 0 | Annual interest rate on idle cash |
execution
| Key | Default | Meaning |
|---|---|---|
at | "open" | Market orders from signals fill at the open ("open") or the close ("close") |
delay | 1 | Fill on the delay-th bar after the signal bar. at: "close", delay: 0 = fill right at the signal bar's close (optimistic) |
intrabar | "ohlc" | The price path inside a bar when checking limit / stop orders: O→H→L→C if the high is closer to the open, otherwise O→L→H→C. "1m": use 1-minute bars when available |
validBars | 1 | How many bars limit / stop orders from signals.limit / stop remain valid |
costs
| Key | Stock default | Derivatives default | Meaning |
|---|---|---|---|
fees | 0.0015 | 0 | Fee per side (ratio of value) |
sellTax | 0.001 | 0 | Sales tax |
slippage | 0.001 | 1 tick | Slippage per order |
impact | 0.01 | 0 | Extra slippage = impact × √(order volume ÷ 20-day average volume) — the larger the order relative to liquidity, the more slippage |
feePerContract | — | 5,000 | Derivatives: fee per contract per side (VND) |
rules
Market rules applied by ticker type (market: "auto"): Vietnamese stocks ("vn-equity"), derivatives ("vn-derivative"), or "none" (no rules).
| Key | Stock default | Meaning |
|---|---|---|
market | "auto" | Rule set |
settlement | 2 | Settlement sessions (T+2) |
sellFrom | "afternoon" | Shares become sellable from the afternoon of T+2 (daily bars: ATC of T+2); "next-open": the open of T+3 |
lotSize | 100 | Trading lot |
priceLimit | "exchange" | Price limit by exchange (HOSE 7%, HNX 10%, UPCoM 15%); a custom number; null removes the limit |
tickSize | "exchange" | Tick size by exchange; a custom number; null for no rounding |
limitLock | true | Cannot buy when there are no sellers at the ceiling price, cannot sell when there are no buyers at the floor price |
retryBlockedBuys | false | Whether a blocked buy order (ceiling, insufficient cash…) is retried on the next bar |
maxAdvPct | 0.1 | Each order is at most this fraction of the average volume of the previous 20 sessions (0 = no limit) |
allowShort | false (stocks) | Allow short selling |
flatAt | — | Derivatives: close all positions from this time ("14:25") |
multiplier, marginRate | — | Derivatives: VND per point, margin rate |
strategy({
name: "Chiến lược khai báo đủ cấu hình",
params: {
n: param.int(20, { min: 10, max: 60, label: "Đỉnh n nến" }),
sl: param.pct(0.06, { min: 0.02, max: 0.15, label: "Dừng lỗ" }),
},
filter: (d) => d.close.gt(ta.sma(d.close, 100)),
signals: (d, p) => ({
entry: d.close.gt(d.high.highest(p.n).shift(1)),
exit: d.close.lt(ta.ema(d.close, 20)),
limit: d.close.mul(1.01),
}),
risk: (p) => ({ stopLoss: p.sl, takeProfit: 0.3, trailingStop: 0.12, maxHoldBars: 60, stopMode: "close" }),
portfolio: { capital: 500000000, sizing: 0.8, pyramiding: 1, cashRate: 0.02 },
execution: { at: "open", delay: 1, intrabar: "ohlc", validBars: 2 },
costs: { fees: 0.0015, sellTax: 0.001, slippage: 0.0015, impact: 0.01 },
rules: { market: "auto", settlement: 2, sellFrom: "afternoon", lotSize: 100, priceLimit: "exchange", tickSize: "exchange", limitLock: true, retryBlockedBuys: false, maxAdvPct: 0.1 },
plot: (d, p) => ({ [`Đỉnh ${p.n} nến trước`]: line(d.high.highest(p.n).shift(1), { color: "up", step: true, dash: "dash" }) }),
});
How orders are filled in a test
- Signals are computed on the closed bar
t(no look-ahead). - Market orders fill according to
execution(by default at the open of bart + 1), plus slippage; limit / stop orders are checked on bart + 1along the intrabar price path. - Quantity: based on
sizing, rounded down to the lot, no more thanmaxAdvPctof average volume; insufficient cash → buy less. - Rules: no buying at the ceiling when there are no sellers, no selling at the floor when there are no buyers; shares not yet settled (T+2) cannot be sold — the sell order waits.
riskis checked on every bar after buying.- Within a bar, the order in which prices are touched follows an estimated O–H–L–C path by default (orders filled this way are marked "estimated"). The tester panel's Khớp trong nến › Kính lúp nến 1 phút (Intrabar fill › 1-minute bar magnifier) option (or
execution.intrabar: "1m") fills using the real 1-minute bars inside; bars older than the 1-minute data fall back to the estimated path. The Trễ lệnh (Order delay) box (1–3 bars) changes the fill bar of market orders for that run, without editing the code.
Test results
| Metric | Meaning |
|---|---|
| Lợi nhuận (Return) | Profit / loss over the whole period ÷ starting capital, after fees, tax and slippage |
| CAGR | Compound annual return |
| Sharpe | Return above the risk-free rate (3%/year) ÷ volatility, annualized |
| Sụt giảm tối đa (Maximum drawdown) | The deepest decline in capital from a peak |
| Số giao dịch (Number of trades), Tỷ lệ thắng (Win rate) | Number of closed buy–sell round trips; percentage that were profitable |
| Profit factor | Total profit ÷ total loss |
| Thời gian có vị thế (Time in market) | Percentage of time holding a position |
| MAE, MFE (per trade) | Largest open loss / profit while holding |
| Mua và giữ (Buy and hold) | The equity curve if you bought at the start of the period and held to the end — for comparison |
Entries for Quant AI Studio
On the chart, a strategy always runs on the ticker being viewed; the entries below are ignored there and used when you choose Mở trong Quant AI Studio (Open in Quant AI Studio).
| Entry | Meaning |
|---|---|
universe | Ticker basket: universe("VN30"), universe("HOSE").liquid(5e9), universe(["FPT", "HPG"]), universe({ industry: "Ngân hàng" }), universe({ watchlist: "Tên" }). Chain .exchange("HOSE"), .industry(…), .exclude([...]), .include([...]), .minPrice(10), .top(50), .where((m) => …). Details: Quant AI Studio › Ticker baskets |
period | { from: "2018-01-01", to? } — the test period |
benchmark | Benchmark index (VNINDEX by default; null = none) |
warmup | Number of warm-up sessions (250 by default) |
rank + select, weights, rebalance | Rotation / weighting strategies on Studio baskets — Quant AI Studio › Strategies |
optimize | { objective?: "sharpe" | "cagr" | "calmar" | "sortino", grid?: { tham_số: [giá trị…] }, maxCombos?, tests?: ["oos", "walkforward", "montecarlo", "costs", "delay", "regimes"], walkForward?: { folds, anchored, trainPct }, oos?: { splitPct } } — optimization and robustness validation in Studio. The Tối ưu (Optimization) tab on the chart uses the parameters' min / max |
Orders in onBar (paper orders)
Instead of signals, a strategy can be written with paper orders in onBar (like Pine's strategy.entry / strategy.exit) — when entry / exit logic needs per-bar state:
| Order | Meaning |
|---|---|
ctx.buy(qty?, opts?), ctx.sell(qty?, opts?) | Buy / sell orders; opts as in ctx.order (type, price, stopPrice, tif…). Without qty: follows portfolio.sizing |
ctx.order({ side, type, qty, price, stopPrice, tif, oco, reduceOnly, comment }) | Places / modifies an order; returns an id. type: market, limit, stop, stop-limit; tif: "bar" (one bar), "day" (until end of session), "gtc" (until cancelled); oco: a one-cancels-other group |
ctx.exit({ id?, from?, qty?, limit?, stop?, trailPct?, trailPoints? }) | Exit orders attached to the position: take profit limit, stop loss stop, trailing stop — in one OCO group, reduce-only. Calling again with the same id = modify |
ctx.closePosition({ qty? }) | Closes the position (or part of it) with a market order |
ctx.cancel(id), ctx.cancelAll() | Cancels pending orders |
ctx.position | side ("flat", "long", "short"), qty, sellable (sellable now — shares past T+2), avgPrice, openPnl, openPnlPct, bars, entryTime |
ctx.orders, ctx.equity, ctx.cash | Current orders (status, rejection reason: lot, price-limit, t2, short, cash…), equity, cash |
Orders placed on bar t are checked for fills from bar t + 1 under the same market rules and execution as signals; no orders are placed on warm-up bars (ctx.warmup). The Kiểm thử chiến lược (Strategy tester) panel runs this way once the strategy has been added with Thêm vào biểu đồ (Add to chart) (including Tối ưu (Optimization) and Walk-forward). onBar strategies cannot yet be used for Lọc bằng script (Screen by script), and they have no ▲ / ▼ signal markers like signals — filled orders appear on the chart after the test runs.
// Chiến lược onBar: mua khi vượt đỉnh 20 phiên, thoát bằng cắt lỗ 2 × ATR dưới giá vốn hoặc dừng kéo theo 10 %
strategy({
name: "Bứt phá đỉnh 20 phiên, cắt lỗ theo ATR",
series: (d) => ({ dinh: d.high.highest(20).shift(1), atr: ta.atr(d, 14) }),
onBar: (ctx) => {
if (ctx.position.side === "flat" && ctx.close > ctx.v.dinh) ctx.buy();
if (ctx.position.side === "long") ctx.exit({ id: "thoat", stop: ctx.position.avgPrice - 2 * ctx.v.atr, trailPct: 0.1 });
},
});
strategy({...})의 항목들은 언제 매수 / 매도할지, 그리고 테스트에서 주문을 어떻게 체결할지를 정합니다. 단계별 안내: 9강, 10강, 11강.
모든 설정(risk, portfolio, execution, costs, rules)은 객체로 쓰거나 파라미터를 받는 함수로 쓸 수 있습니다: risk: (p) => ({ stopLoss: p.sl }).
signals
signals: (d, p) => dieuKien | { entry, exit?, score?, limit?, stop?, short? }
| 키 | 타입 | 의미 |
|---|---|---|
entry | 조건, 필수 | 보유하지 않을 때 매수 |
exit | 조건 | 보유 중일 때 매도(포지션 청산). 같은 봉에 entry와 exit가 모두 있으면 매수하지 않음 |
score | 시리즈 | 숫자 | 빈자리보다 신호가 많을 때의 우선순위(Studio의 다종목 바스켓). 차트의 단일 종목에서는 사용하지 않음 |
limit | 시리즈 | 숫자(가격) | 진입 주문을 다음 봉에 대해 이 수준의 지정가 주문으로 바꿈(가격이 이 수준까지 내려와야 체결) |
stop | 시리즈 | 숫자(가격) | 진입 주문을 이 수준의 스톱 주문으로 바꿈(가격이 이 수준을 넘어야 체결). limit과 stop이 모두 있으면 스톱 지정가 주문 |
short | { entry, exit?, limit?, stop? } | 공매도 포지션 — 허용된 종목(선물)만 가능. 매수 보유 중 공매도 신호가 나오면 방향을 전환 |
조건 하나만 반환하면 그것이 entry입니다. 차트에서 신호는 ▲(진입) / ▼(청산)으로 표시되며, 보유하지 않을 때 entry가 참이면 진입, 보유 중일 때 exit가 참이면 청산합니다(exit가 없으면 entry가 거짓이 될 때 청산 — 단, 테스트 패널에서는 risk가 포지션을 닫을 때까지 보유).
filter
filter: (d, p) => dieuKien — 조건이 참인 봉에서만 진입을 허용합니다(entry에 .and(filter)를 붙인 것과 같음). Studio에서는 바스켓의 유동성 필터와 결합됩니다.
risk
| 키 | 의미 |
|---|---|
stopLoss | 손절: 가격이 매입가 대비 stopLoss(비율)만큼 하락하면 매도 |
takeProfit | 익절: 수익이 takeProfit에 도달하면 매도 |
trailingStop | 트레일링 스톱: 매수 이후 최고가 대비 trailingStop만큼 하락하면 매도 |
maxHoldBars | 최대 보유 봉 수(계산 중인 주기 기준) |
maxHoldDays | Quant의 옛 이름: 세션 수 |
stopMode | "close"(기본값): 종가로 임계값을 판단하고 다음 봉에 매도. "intraday": 봉 안에서 임계값에 체결(시가가 이미 임계값을 넘었으면 시가에 체결) |
주문 사유는 Giao dịch(거래) 탭에 표시됩니다: Tín hiệu bán(매도 신호), Dừng lỗ(손절), Chốt lời(익절), Dừng lỗ kéo theo(트레일링 스톱), Hết thời gian(보유 기간 만료)…
portfolio
| 키 | 기본값 | 의미 |
|---|---|---|
capital | 1,000,000,000 | 초기 자본(동) |
sizing | "equal" | 매수할 때마다: "equal" = 보고 있는 종목에 자본 전액(Studio 바스켓: 자본 ÷ 최대 포지션 수); 0–1 숫자 = 자본 비중 |
pyramiding | 1 | 같은 방향으로 진입할 수 있는 횟수(1 = 보유 중 추가 매수 안 함) |
maxPositions | — | 동시에 보유하는 종목 수(Studio 바스켓) |
cashRate | 0 | 유휴 현금의 연 이자율 |
execution
| 키 | 기본값 | 의미 |
|---|---|---|
at | "open" | 신호로 나온 시장가 주문을 시가("open") 또는 종가("close")에 체결 |
delay | 1 | 신호 봉 이후 delay번째 봉에서 체결. at: "close", delay: 0 = 신호 봉의 종가에 즉시 체결(낙관적) |
intrabar | "ohlc" | 지정가 / 스톱 주문을 판단할 때의 봉 내 가격 경로: 고가가 시가에 더 가까우면 O→H→L→C, 아니면 O→L→H→C. "1m": 가능하면 1분봉 사용 |
validBars | 1 | signals.limit / stop에서 나온 지정가 / 스톱 주문의 유효 봉 수 |
costs
| 키 | 주식 기본값 | 파생상품 기본값 | 의미 |
|---|---|---|---|
fees | 0.0015 | 0 | 편도 수수료(거래 금액 대비 비율) |
sellTax | 0.001 | 0 | 매도세 |
slippage | 0.001 | 1 호가 단위 | 주문당 슬리피지 |
impact | 0.01 | 0 | 추가 슬리피지 = impact × √(주문 KL ÷ 20일 평균 KL) — 유동성 대비 주문이 클수록 슬리피지가 커짐 |
feePerContract | — | 5,000 | 파생상품: 계약당 편도 수수료(동) |
rules
시장 규칙은 종목 유형에 따라 적용됩니다(market: "auto"): 베트남 주식("vn-equity"), 파생상품("vn-derivative"), 또는 "none"(규칙 없음).
| 키 | 주식 기본값 | 의미 |
|---|---|---|
market | "auto" | 규칙 세트 |
settlement | 2 | 결제 세션 수(T+2) |
sellFrom | "afternoon" | 입고된 주식은 T+2 오후부터 매도 가능(일봉: T+2 ATC); "next-open": T+3 시가 |
lotSize | 100 | 거래 단위 |
priceLimit | "exchange" | 거래소별 가격 제한폭(HOSE 7 %, HNX 10 %, UPCoM 15 %); 숫자로 직접 지정; null이면 제한폭 없음 |
tickSize | "exchange" | 거래소별 호가 단위; 숫자로 직접 지정; null이면 반올림 안 함 |
limitLock | true | 상한가에서 매도 잔량이 없으면 매수 불가, 하한가에서 매수 잔량이 없으면 매도 불가 |
retryBlockedBuys | false | 막힌 매수 주문(상한가, 현금 부족…)을 다음 봉에 재시도할지 여부 |
maxAdvPct | 0.1 | 주문당 이전 20세션 평균 거래량의 최대 비율(0 = 제한 없음) |
allowShort | false(주식) | 공매도 허용 |
flatAt | — | 파생상품: 이 시각부터 모든 포지션 청산("14:25") |
multiplier, marginRate | — | 파생상품: 포인트당 금액(동), 증거금률 |
strategy({
name: "Chiến lược khai báo đủ cấu hình",
params: {
n: param.int(20, { min: 10, max: 60, label: "Đỉnh n nến" }),
sl: param.pct(0.06, { min: 0.02, max: 0.15, label: "Dừng lỗ" }),
},
filter: (d) => d.close.gt(ta.sma(d.close, 100)),
signals: (d, p) => ({
entry: d.close.gt(d.high.highest(p.n).shift(1)),
exit: d.close.lt(ta.ema(d.close, 20)),
limit: d.close.mul(1.01),
}),
risk: (p) => ({ stopLoss: p.sl, takeProfit: 0.3, trailingStop: 0.12, maxHoldBars: 60, stopMode: "close" }),
portfolio: { capital: 500000000, sizing: 0.8, pyramiding: 1, cashRate: 0.02 },
execution: { at: "open", delay: 1, intrabar: "ohlc", validBars: 2 },
costs: { fees: 0.0015, sellTax: 0.001, slippage: 0.0015, impact: 0.01 },
rules: { market: "auto", settlement: 2, sellFrom: "afternoon", lotSize: 100, priceLimit: "exchange", tickSize: "exchange", limitLock: true, retryBlockedBuys: false, maxAdvPct: 0.1 },
plot: (d, p) => ({ [`Đỉnh ${p.n} nến trước`]: line(d.high.highest(p.n).shift(1), { color: "up", step: true, dash: "dash" }) }),
});
테스트의 주문 체결 방식
- 신호는 마감된 봉
t에서 계산합니다(미래 참조 없음). - 시장가 주문은
execution에 따라 체결되고(기본값은 봉t + 1의 시가) 슬리피지가 더해집니다. 지정가 / 스톱 주문은 봉 내 가격 경로에 따라 봉t + 1에서 판단합니다. - 수 량:
sizing에 따르며, 거래 단위로 내림하고 평균 거래량의maxAdvPct를 넘지 않습니다. 현금이 부족하면 더 적게 매수합니다. - 규칙: 매도 잔량이 없으면 상한가에서 매수하지 않고, 매수 잔량이 없으면 하한가에서 매도하지 않습니다. 아직 입고되지 않은 주식(T+2)은 매도할 수 없으며 매도 주문은 대기합니다.
risk는 매수 후 매 봉마다 판단합니다.- 한 봉 안에서 가격에 닿는 순서는 기본적으로 추정된 O–H–L–C 경로를 따릅니다(이렇게 체결된 주문에는 "추정" 표시가 붙음). 테스트 패널의 Khớp trong nến › Kính lúp nến 1 phút(봉 내 체결 › 1분봉 돋보기) 옵션(또는
execution.intrabar: "1m")은 봉 안의 실제 1분봉으로 체결합니다. 1분 데이터보다 오래된 봉은 추정 경로로 돌아갑니다. Trễ lệnh(주문 지연) 옵션(1–3개 봉)은 코드를 고치지 않고 해당 실행에 한해 시장가 주문의 체결 봉을 바꿉니다.
테스트 결과
| 지표 | 의미 |
|---|---|
| Lợi nhuận(수익률) | 전체 기간 손익 ÷ 초기 자본(수수료, 세금, 슬리피지 반영 후) |
| CAGR | 연 복리 수익률 |
| Sharpe | 무위험 이자율(연 3 %)을 넘는 수익 ÷ 변동성, 연율화 |
| Sụt giảm tối đa(최대 낙폭) | 고점 대비 자본의 가장 깊은 하락 |
| Số giao dịch(거래 수), Tỷ lệ thắng(승률) | 청산된 매수–매도 횟수; 수익 거래의 비율 |
| Profit factor | 총이익 ÷ 총손실 |
| Thời gian có vị thế(포지션 보유 시간) | 보유 중인 시간의 비율 |
| MAE, MFE(거래별) | 보유 중 최대 평가 손실 / 이익 |
| Mua và giữ(매수 후 보유) | 기간 초에 매수해 끝까지 보유했을 때의 자본 곡선 — 비교용 |
Quant AI Studio용 항목
차트에서 전략은 항상 보고 있는 종목에서 실행됩니다. 아래 항목은 무시되며 Mở trong Quant AI Studio(Quant AI Studio에서 열기)를 할 때 사용됩니다.
| 항목 | 의미 |
|---|---|
universe | 종목 바스켓: universe("VN30"), universe("HOSE").liquid(5e9), universe(["FPT", "HPG"]), universe({ industry: "Ngân hàng" }), universe({ watchlist: "Tên" }). .exchange("HOSE"), .industry(…), .exclude([...]), .include([...]), .minPrice(10), .top(50), .where((m) => …)를 이어 붙일 수 있습니다. 자세히: Quant AI Studio › 종목 바스켓 |
period | { from: "2018-01-01", to? } — 테스트 기간 |
benchmark | 비교 지수(기본값 VNINDEX; null = 없음) |
warmup | 워밍업 세션 수(기본값 250) |
rank + select, weights, rebalance | Studio 바스켓 기반 로테이션 / 비중 전략 — Quant AI Studio › 전략 |
optimize | { objective?: "sharpe" | "cagr" | "calmar" | "sortino", grid?: { tham_số: [giá trị…] }, maxCombos?, tests?: ["oos", "walkforward", "montecarlo", "costs", "delay", "regimes"], walkForward?: { folds, anchored, trainPct }, oos?: { splitPct } } — Studio에서의 최적화와 견고성 검증. 차트의 Tối ưu(최적화) 탭은 파라미터의 min / max를 사용 |
onBar 안의 주문(페이퍼 주문)
signals 대신, 진입 / 청산 로직에 봉 단위 상태가 필요할 때는 onBar 안의 페이퍼 주문으로 전략을 작성할 수 있습니다(Pine의 strategy.entry / strategy.exit와 비슷함).
| 명령 | 의미 |
|---|---|
ctx.buy(qty?, opts?), ctx.sell(qty?, opts?) | 매수 / 매도 주문; opts는 ctx.order와 같음(type, price, stopPrice, tif…). qty가 없으면 portfolio.sizing을 따름 |
ctx.order({ side, type, qty, price, stopPrice, tif, oco, reduceOnly, comment }) | 주문 생성 / 수정; id 반환. type: 시장가, 지정가, 스톱, 스톱 지정가; tif: "bar"(한 봉), "day"(세션 종료까지), "gtc"(취소할 때까지); oco: 상호 취소 그룹 |
ctx.exit({ id?, from?, qty?, limit?, stop?, trailPct?, trailPoints? }) | 포지션에 연결된 청산 주문: 익절 limit, 손절 stop, 트레일링 스톱 — 같은 OCO 그룹이며 포지션 축소만 함. 같은 id로 다시 호출하면 수정 |
ctx.closePosition({ qty? }) | 시장가 주문으로 포지션(또는 일부) 청산 |
ctx.cancel(id), ctx.cancelAll() | 미체결 주문 취소 |
ctx.position | side("flat", "long", "short"), qty, sellable(즉시 매도 가능 — T+2가 지난 주식), avgPrice, openPnl, openPnlPct, bars, entryTime |
ctx.orders, ctx.equity, ctx.cash | 현재 주문(status, 거부 사유: lot, price-limit, t2, short, cash…), 자본, 현금 |
봉 t에서 낸 주문은 signals와 같은 시장 규칙과 execution에 따라 봉 t + 1부터 체결을 판단합니다. 워밍업 봉(ctx.warmup)에서는 주문을 내지 않습니다. Kiểm thử chiến lược(전략 테스트) 패널은 전략을 Thêm vào biểu đồ(차트에 추가)한 뒤 이 방식으로 실행됩니다(Tối ưu(최적화), Walk-forward 포함). onBar 전략은 아직 Lọc bằng script(스크립트로 필터)에 사용할 수 없고, signals처럼 ▲ / ▼ 신호 마커가 없습니다. 체결된 주문은 테스트를 실행한 뒤 차트에 표시됩니다.
// Chiến lược onBar: mua khi vượt đỉnh 20 phiên, thoát bằng cắt lỗ 2 × ATR dưới giá vốn hoặc dừng kéo theo 10 %
strategy({
name: "Bứt phá đỉnh 20 phiên, cắt lỗ theo ATR",
series: (d) => ({ dinh: d.high.highest(20).shift(1), atr: ta.atr(d, 14) }),
onBar: (ctx) => {
if (ctx.position.side === "flat" && ctx.close > ctx.v.dinh) ctx.buy();
if (ctx.position.side === "long") ctx.exit({ id: "thoat", stop: ctx.position.avgPrice - 2 * ctx.v.atr, trailPct: 0.1 });
},
});
strategy({...}) 的各项决定何时买入 / 卖出,以及测试时如何撮合成交。分步教程:第 9 课、第 10 课、第 11 课。
所有配置(risk、portfolio、execution、costs、rules)都可以写成对象,或写成参数的函数:risk: (p) => ({ stopLoss: p.sl })。
signals
signals: (d, p) => dieuKien | { entry, exit?, score?, limit?, stop?, short? }
| 键 | 类型 | 含义 |
|---|---|---|
entry | 条件,必填 | 未持仓时买入 |
exit | 条件 | 持仓时卖出(平仓)。同一根 K 线同时有 entry 和 exit 时不买入 |
score | 序列 | 数字 | 信号数量多于空余仓位时的优先级(Studio 中多股票的股票池);图表上的单只股票不使用 |
limit | 序列 | 数字(价格) | 入场单变为下一根 K 线在该价位的限价单(只有价格下探到该价位才成交) |
stop | 序列 | 数字(价格) | 入场单变为该价位的止损单(只有价格上破该价位才成交)。同时有 limit 和 stop:止损限价单 |
short | { entry, exit?, limit?, stop? } | 卖空仓位——仅限允许卖空的品种(期货);持有多头时出现卖空信号则反手 |
直接返回一个条件时,该条件即为 entry。在图表上,信号按以下方式显示 ▲(入场)/ ▼(离场):未持仓时 entry 成立则入场,持仓时 exit 成立则离场(没有 exit:entry 不再成立时离场——但测试面板会一直持有,直到 risk 平仓)。
filter
filter: (d, p) => dieuKien——只允许在条件成立的 K 线上入场(相当于在 entry 上加 .and(filter))。在 Studio 中与股票池的流动性筛选结合使用。
risk
| 键 | 含义 |
|---|---|
stopLoss | 止损:价格相对成本价下跌 stopLoss(比例)时卖出 |
takeProfit | 止盈:盈利达到 takeProfit 时卖出 |
trailingStop | 移动止损:价格相对买入以来最高价下跌 trailingStop 时卖出 |
maxHoldBars | 最多持有的 K 线数(按当前计算周期) |
maxHoldDays | Quant 的旧名称:交易时段数 |
stopMode | "close"(默认):按收盘价判断阈值,在下一根 K 线卖出。"intraday":在 K 线内触及阈值时成交(开盘价已越过阈值 → 以开盘价成交) |
离场原因显示在 Giao dịch(交易)标签页中:Tín hiệu bán(卖出信号)、Dừng lỗ(止损)、Chốt lời(止盈)、Dừng lỗ kéo theo(移动止损)、Hết thời gian(到期)…
portfolio
| 键 | 默认值 | 含义 |
|---|---|---|
capital | 1,000,000,000 | 初始资金(越南盾) |
sizing | "equal" | 每次买入:"equal" = 全部资金用于当前查看的股票(Studio 中的股票池:资金 ÷ 最大持仓数);0–1 的数字 = 资金占比 |
pyramiding | 1 | 同方向可入场的次数(1 = 持仓时不加仓) |
maxPositions | — | 同时持有的股票数(Studio 中的股票池) |
cashRate | 0 | 闲置资金的年利率 |