Thực Hành Với MetaTrader 5 Và TradingView

Mục tiêu buổi học
- Kết nối MT5 với Python để mở/đóng lệnh mô phỏng
- Viết chiến lược Pine Script xác định xu hướng
- Nhận tín hiệu TradingView qua webhook về n8n
- Vận hành bot bằng vòng polling TP/SL
Hình minh hoạ tổng quan

4
Hình minh hoạ từng phần quan trọng
1. Pine Script trên TradingView


2. MT5 chạy tài khoản Demo


3. n8n Webhook nhận tín hiệu từ Alert

4. Monitoring dashboard bot giao dịch


Ứng dụng đa khung (MTF)


Script mẫu trong buổi học (giữ nguyên, không emoji)
//@version=5
indicator("Fin3B – AMA Trend Demo", overlay=true)
ama6 = ta.hma(close, 6)
ama10 = ta.hma(close, 10)
ama20 = ta.hma(close, 20)
plot(ama6)
plot(ama10)
plot(ama20)
trendUp = ama6 > ama10 and ama10 > ama20
bgcolor(trendUp ? color.new(color.green, 85) : na)
import MetaTrader5 as mt5
import time
mt5.initialize()
symbol = "XAUUSD"
lot = 0.01
tp = 2700
sl = 2600
request = {
"action": mt5.TRADE_ACTION_DEAL,
"symbol": symbol,
"volume": lot,
"type": mt5.ORDER_TYPE_BUY,
"price": mt5.symbol_info_tick(symbol).ask,
"sl": sl,
"tp": tp,
}
order = mt5.order_send(request)
print("Entry demo:", order)
while True:
positions = mt5.positions_get(symbol=symbol)
if not positions:
print("Không còn position mở – dừng polling")
break
price = mt5.symbol_info_tick(symbol).bid
print("Giá hiện tại:", price)
if price >= tp:
print("→ Đạt TP, đóng lệnh demo")
mt5.Close(symbol)
break
if price <= sl:
print("→ Chạm SL, đóng lệnh demo")
mt5.Close(symbol)
break
time.sleep(10)
Kết luận buổi 20
Fin3B ở buổi này giúp bạn hiểu:
- Xu hướng khung lớn mới là bản đồ
- Entry khung nhỏ chỉ là trigger
- TP/SL có thể bot tự xử lý bằng polling nếu API không hỗ trợ
- Không giao dịch khi AMA chồng chéo
- Luôn giám sát bot để tránh gãy hệ thống
Phần mở rộng: Làm chủ MT5 + TradingView bằng Python từ A đến Z
Phần trên là phần thực hành trong buổi học. Phần dưới đây đi sâu vào từng mảnh ghép, giải thích tại sao và làm thế nào để bạn tự xây dựng hệ thống tương tự từ đầu.
1. Vì sao chọn MT5 thay vì chỉ dùng API sàn?
Hầu hết sàn crypto có REST API tốt. Nhưng với forex, vàng, chỉ số, hàng hoá, con đường phổ biến nhất vẫn là MetaTrader. Lý do rất cụ thể:
| Tiêu chí | MT5 + Python | REST API sàn |
|---|---|---|
| Thị trường hỗ trợ | Forex, vàng, chỉ số, CFD, crypto | Chủ yếu crypto |
| Broker phổ biến | Hầu hết broker forex | Sàn crypto |
| Đòn bẩy | Có (forex) | Thường không |
| Backtest tích hợp | Có (Strategy Tester) | Không |
| Dữ liệu lịch sử | Sẵn theo broker | Tuỳ sàn |
| Hạn chế | Phải chạy trên Windows/VPS có MT5 | Chạy mọi nơi |
Điểm yếu lớn nhất của MT5 là phải chạy trên Windows. Điều này nghĩa là bot của bạn luôn cần một máy Windows hoặc VPS Windows — không deploy lên Vercel/Lambda được.
💡 Lựa chọn thực tế: nếu bạn chỉ giao dịch crypto, hãy dùng API sàn (Binance, Bitget) — đơn giản hơn nhiều. Nếu bạn giao dịch forex/vàng, MT5 là lựa chọn gần như bắt buộc.
2. Cài đặt và kết nối an toàn
pip install MetaTrader5 pandas numpy python-dotenv
Bạn phải cài MetaTrader 5 terminal trên cùng máy. Thư viện MetaTrader5 không kết nối qua mạng — nó nói chuyện trực tiếp với terminal đang chạy qua IPC.
import os
from dotenv import load_dotenv
import MetaTrader5 as mt5
load_dotenv()
def ket_noi_mt5() -> bool:
"""Kết nối tới MT5 terminal. Trả False nếu thất bại."""
if not mt5.initialize(
path=os.getenv("MT5_PATH"), # đường dẫn terminal64.exe
login=int(os.getenv("MT5_LOGIN", "0")),
password=os.getenv("MT5_PASSWORD"),
server=os.getenv("MT5_SERVER"),
timeout=10_000,
portable=False,
):
ma_loi, mo_ta = mt5.last_error()
print(f"❌ Khởi tạo thất bại: [{ma_loi}] {mo_ta}")
return False
thong_tin = mt5.terminal_info()
tai_khoan = mt5.account_info()
print(f"✅ MT5 {thong_tin.build} — kết nối: {thong_tin.connected}")
print(f" Tài khoản: {tai_khoan.login} | "
f"{tai_khoan.server} | "
f"{'DEMO' if tai_khoan.trade_mode == mt5.ACCOUNT_TRADE_MODE_DEMO else '⚠️ THẬT'}")
print(f" Số dư: {tai_khoan.balance:.2f} {tai_khoan.currency}")
return True
def ngat_ket_noi():
mt5.shutdown()
if __name__ == "__main__":
if ket_noi_mt5():
ngat_ket_noi()
File .env (không bao giờ commit):
MT5_PATH=C:\Program Files\MetaTrader 5\terminal64.exe
MT5_LOGIN=12345678
MT5_PASSWORD=mat_khau_cua_ban
MT5_SERVER=Broker-Demo
⚠️ Ba nguyên tắc an toàn với MT5:
- Luôn bắt đầu bằng tài khoản DEMO. Không có ngoại lệ.
- Không lưu mật khẩu trong code. Dùng
.env+.gitignore.- Giới hạn số lệnh tối đa trong bot để một lỗi logic không mở 1000 lệnh.
3. Các hàm MT5 bạn sẽ dùng 90% thời gian
| Hàm | Công dụng |
|---|---|
mt5.initialize() / mt5.shutdown() | Kết nối / ngắt kết nối |
mt5.symbol_info(symbol) | Thông số symbol (lot step, spread, digits) |
mt5.symbol_info_tick(symbol) | Giá bid/ask hiện tại |
mt5.copy_rates_from_pos() | Lấy nến lịch sử → DataFrame |
mt5.order_send(request) | Gửi lệnh (mở/đóng) |
mt5.positions_get() | Danh sách vị thế đang mở |
mt5.orders_get() | Danh sách lệnh chờ |
mt5.history_deals_get() | Lịch sử giao dịch |
mt5.account_info() | Số dư, equity, margin |
Lấy dữ liệu nến — nền tảng của mọi chiến lược:
import pandas as pd
import MetaTrader5 as mt5
KHUNG_THOI_GIAN = {
"M1": mt5.TIMEFRAME_M1,
"M5": mt5.TIMEFRAME_M5,
"M15": mt5.TIMEFRAME_M15,
"H1": mt5.TIMEFRAME_H1,
"H4": mt5.TIMEFRAME_H4,
"D1": mt5.TIMEFRAME_D1,
}
def lay_nen(symbol: str, khung: str = "H1", so_nen: int = 500) -> pd.DataFrame:
"""Lấy nến từ MT5 và trả về DataFrame đã chuẩn hoá."""
tf = KHUNG_THOI_GIAN.get(khung)
if tf is None:
raise ValueError(f"Khung không hợp lệ: {khung}. Dùng: {list(KHUNG_THOI_GIAN)}")
# Đảm bảo symbol đã được chọn trong Market Watch
if not mt5.symbol_select(symbol, True):
raise RuntimeError(f"Không chọn được symbol {symbol}")
rates = mt5.copy_rates_from_pos(symbol, tf, 0, so_nen)
if rates is None or len(rates) == 0:
raise RuntimeError(f"Không lấy được dữ liệu {symbol} {khung}: {mt5.last_error()}")
df = pd.DataFrame(rates)
df["time"] = pd.to_datetime(df["time"], unit="s")
df = df.set_index("time")
df = df.rename(columns={
"open": "open", "high": "high", "low": "low",
"close": "close", "tick_volume": "volume",
})
return df[["open", "high", "low", "close", "volume"]]
df = lay_nen("XAUUSD", "H1", 500)
print(df.tail())
4. Chỉ báo AMA/HMA dùng trong buổi học — hiểu để dùng đúng
Pine Script trong buổi học dùng ta.hma(close, n) — Hull Moving Average. Đây là biến thể làm mượt tốt hơn MA truyền thống và giảm độ trễ đáng kể.
Công thức HMA:
HMA(n) = WMA( 2 × WMA(price, n/2) − WMA(price, n), √n )
Cài đặt bằng Python (khớp với Pine Script):
import numpy as np
import pandas as pd
def wma(chuoi: pd.Series, chu_ky: int) -> pd.Series:
"""Weighted Moving Average — trọng số tuyến tính, mới nhất nặng nhất."""
trong_so = np.arange(1, chu_ky + 1)
return chuoi.rolling(chu_ky).apply(
lambda x: np.dot(x, trong_so) / trong_so.sum(), raw=True
)
def hma(chuoi: pd.Series, chu_ky: int) -> pd.Series:
"""Hull Moving Average — khớp với ta.hma() trong Pine Script."""
nua = max(1, chu_ky // 2)
can = max(1, int(np.sqrt(chu_ky)))
return wma(2 * wma(chuoi, nua) - wma(chuoi, chu_ky), can)
def them_tin_hieu_hma(df: pd.DataFrame, n6=6, n10=10, n20=20) -> pd.DataFrame:
df["hma6"] = hma(df["close"], n6)
df["hma10"] = hma(df["close"], n10)
df["hma20"] = hma(df["close"], n20)
# Điều kiện "xu hướng" giống hệt Pine Script
df["xu_huong_tang"] = (df["hma6"] > df["hma10"]) & (df["hma10"] > df["hma20"])
df["xu_huong_giam"] = (df["hma6"] < df["hma10"]) & (df["hma10"] < df["hma20"])
# Phát hiện lúc xu hướng ĐỔI (điểm vào lệnh tiềm năng)
df["doi_sang_tang"] = df["xu_huong_tang"] & ~df["xu_huong_tang"].shift(1).fillna(False)
df["doi_sang_giam"] = df["xu_huong_giam"] & ~df["xu_huong_giam"].shift(1).fillna(False)
# Đo mức "chồng chéo" — khi ba đường bó sát, không nên giao dịch
df["do_nen_chat"] = (df[["hma6", "hma10", "hma20"]].max(axis=1)
- df[["hma6", "hma10", "hma20"]].min(axis=1))
# Ngưỡng chồng chéo: nhỏ hơn 0.15% giá là quá chặt
df["chong_cheo"] = df["do_nen_chat"] < df["close"] * 0.0015
return df
df = them_tin_hieu_hma(lay_nen("XAUUSD", "H1", 500))
print(df[["close", "hma6", "hma10", "hma20", "xu_huong_tang", "chong_cheo"]].tail(10))
Ba đường HMA bó sát nhau nghĩa là thị trường đang tích luỹ, không có xu hướng. Vào lệnh lúc này gần như chắc chắn bị quét stop loss. Điều kiện chong_cheo chính là bộ lọc mà buổi học nhấn mạnh: "không giao dịch khi AMA chồng chéo".
5. Pine Script đầy đủ — có Alert để gửi webhook
Script trong buổi học chỉ vẽ HMA. Để dùng cho bot, cần thêm alert có payload JSON:
//@version=5
indicator("Fin3B - HMA Trend + Alert", overlay=true)
// --- Tham số ---
n6 = input.int(6, "HMA nhanh")
n10 = input.int(10, "HMA trung")
n20 = input.int(20, "HMA cham")
nguong_chong_cheo = input.float(0.15, "Nguong chong cheo (% gia)") / 100
// --- Tính HMA ---
hma6 = ta.hma(close, n6)
hma10 = ta.hma(close, n10)
hma20 = ta.hma(close, n20)
plot(hma6, "HMA6", color=color.new(color.yellow, 0))
plot(hma10, "HMA10", color=color.new(color.orange, 0))
plot(hma20, "HMA20", color=color.new(color.red, 0))
// --- Xu hướng ---
tang = hma6 > hma10 and hma10 > hma20
giam = hma6 < hma10 and hma10 < hma20
// --- Do chong cheo ---
do_nen_chat = math.max(hma6, hma10, hma20) - math.min(hma6, hma10, hma20)
chong_cheo = do_nen_chat < close * nguong_chong_cheo
bgcolor(tang and not chong_cheo ? color.new(color.green, 85) : na)
bgcolor(giam and not chong_cheo ? color.new(color.red, 85) : na)
// --- Tin hieu doi xu huong (chi khi khong chong cheo) ---
doi_tang = tang and not tang[1] and not chong_cheo
doi_giam = giam and not giam[1] and not chong_cheo
plotshape(doi_tang, "MUA", shape.labelup, location.belowbar, color.green, text="BUY")
plotshape(doi_giam, "BAN", shape.labeldown, location.abovebar, color.red, text="SELL")
// --- Alert gui webhook dang JSON ---
if doi_tang
alert('{"hanh_dong":"MUA","symbol":"' + syminfo.ticker +
'","gia":' + str.tostring(close, "#.##") +
',"khung":"' + timeframe.period +
'","thoi_gian":"' + str.tostring(time, "yyyy-MM-dd HH:mm:ss") + '"}',
alert.freq_once_per_bar_close)
if doi_giam
alert('{"hanh_dong":"BAN","symbol":"' + syminfo.ticker +
'","gia":' + str.tostring(close, "#.##") +
',"khung":"' + timeframe.period +
'","thoi_gian":"' + str.tostring(time, "yyyy-MM-dd HH:mm:ss") + '"}',
alert.freq_once_per_bar_close)
Bốn chi tiết quan trọng trong script này:
| Chi tiết | Vì sao quan trọng |
|---|---|
alert.freq_once_per_bar_close | Nến chưa đóng mà báo tín hiệu → tín hiệu có thể biến mất. Chỉ báo khi nến đã đóng. |
| Payload là JSON | n8n/API parse được ngay, không phải xử lý chuỗi tự do |
Có symbol, khung, thoi_gian | Bot cần biết tín hiệu đến từ đâu, khung nào |
Lọc chong_cheo ngay trong script | Giảm tín hiệu rác trước khi tới bot |
6. Webhook TradingView → n8n → MT5
Kiến trúc đầy đủ:
flowchart LR
A[TradingView Alert] -->|POST JSON| B[n8n Webhook]
B --> C{Loc tin hieu}
C -->|Hop le| D[Goi API bot]
C -->|Khong hop le| E[Log & bo qua]
D --> F[MT5 order_send]
F --> G[Thong bao Telegram]
Bước 1 — Tạo webhook receiver trong n8n:
- Thêm node Webhook, method
POST, pathtradingview-signal. - Thêm node IF kiểm tra
{{ $json.hanh_dong }}làMUAhoặcBAN. - Thêm node HTTP Request gọi API bot của bạn với header xác thực.
- Thêm node Telegram gửi thông báo.
⚠️ TradingView chỉ cho phép webhook tới URL công khai có HTTPS. Và URL webhook chính là mật khẩu — ai biết URL đều có thể gửi tín hiệu giả để bot của bạn vào lệnh. Hãy:
- Dùng URL có chuỗi ngẫu nhiên dài (n8n tự sinh).
- Kiểm tra thêm một
secrettrong body.- Đặt rate limit cho endpoint.
Bước 2 — Webhook receiver viết bằng Python (nếu không dùng n8n):
import hmac
import hashlib
import os
import logging
from flask import Flask, request, jsonify
app = Flask(__name__)
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)
SECRET = os.environ["WEBHOOK_SECRET"]
def kiem_tra_secret(du_lieu: dict) -> bool:
"""So sánh secret theo cách chống timing attack."""
nhan_duoc = str(du_lieu.get("secret", ""))
return hmac.compare_digest(nhan_duoc, SECRET) # KHÔNG dùng ==
@app.post("/webhook/tradingview")
def nhan_tin_hieu():
du_lieu = request.get_json(silent=True)
if not du_lieu:
return jsonify({"loi": "Body không phải JSON hợp lệ"}), 400
if not kiem_tra_secret(du_lieu):
logger.warning("Có request với secret sai từ %s", request.remote_addr)
return jsonify({"loi": "Xác thực thất bại"}), 401
hanh_dong = du_lieu.get("hanh_dong")
symbol = du_lieu.get("symbol")
if hanh_dong not in ("MUA", "BAN"):
return jsonify({"loi": f"hanh_dong không hợp lệ: {hanh_dong}"}), 422
if not symbol:
return jsonify({"loi": "Thiếu symbol"}), 422
logger.info("📩 Tín hiệu: %s %s @ %s", hanh_dong, symbol, du_lieu.get("gia"))
ket_qua = xu_ly_tin_hieu(hanh_dong, symbol, du_lieu)
return jsonify(ket_qua), 200
Thư viện hmac.compare_digest quan trọng hơn vẻ ngoài của nó: so sánh chuỗi bằng == dừng ngay khi gặp ký tự khác nhau, và thời gian dừng tiết lộ thông tin về secret đúng.
7. Đặt lệnh MT5 đúng chuẩn
Code trong buổi học gửi order_send trực tiếp. Trong thực tế cần kiểm tra kết quả, chuẩn hoá giá và khối lượng:
import MetaTrader5 as mt5
def chuan_hoa_khoi_luong(symbol: str, khoi_luong: float) -> float:
"""Làm tròn khối lượng theo bước (volume step) và giới hạn của broker."""
info = mt5.symbol_info(symbol)
if info is None:
raise RuntimeError(f"Không có thông tin symbol {symbol}")
buoc = info.volume_step
khoi_luong = round(khoi_luong / buoc) * buoc
khoi_luong = max(info.volume_min, min(info.volume_max, khoi_luong))
return round(khoi_luong, 8)
def chuan_hoa_gia(symbol: str, gia: float) -> float:
"""Làm tròn giá theo số chữ số thập phân mà broker yêu cầu."""
info = mt5.symbol_info(symbol)
return round(gia, info.digits)
def tinh_khoi_luong_theo_rui_ro(
symbol: str, so_du: float, rui_ro_pct: float, khoang_cach_sl: float
) -> float:
"""Khối lượng để khi chạm SL chỉ mất đúng rui_ro_pct % số dư."""
info = mt5.symbol_info(symbol)
so_tien_rui_ro = so_du * rui_ro_pct / 100
# Giá trị 1 điểm (point) cho 1 lot, quy đổi theo tick_value
gia_tri_moi_point = info.trade_tick_value / info.trade_tick_size
so_point_sl = khoang_cach_sl / info.point
if so_point_sl <= 0 or gia_tri_moi_point <= 0:
raise ValueError("Không tính được khối lượng — kiểm tra khoảng cách SL")
khoi_luong = so_tien_rui_ro / (so_point_sl * gia_tri_moi_point)
return chuan_hoa_khoi_luong(symbol, khoi_luong)
def mo_lenh(
symbol: str,
huong: str, # "MUA" hoặc "BAN"
khoi_luong: float,
sl: float | None = None,
tp: float | None = None,
magic: int = 20260101,
ghi_chu: str = "Fin3B bot",
) -> dict | None:
"""Mở lệnh thị trường với đầy đủ kiểm tra."""
info = mt5.symbol_info(symbol)
if info is None:
raise RuntimeError(f"Symbol không tồn tại: {symbol}")
if not info.visible:
mt5.symbol_select(symbol, True)
tick = mt5.symbol_info_tick(symbol)
if tick is None:
raise RuntimeError(f"Không lấy được giá {symbol}")
if huong == "MUA":
loai_lenh, gia = mt5.ORDER_TYPE_BUY, tick.ask
elif huong == "BAN":
loai_lenh, gia = mt5.ORDER_TYPE_SELL, tick.bid
else:
raise ValueError(f"Huong không hợp lệ: {huong}")
# Kiểm tra khoảng cách stop tối thiểu của broker
khoang_cach_min = info.trade_stops_level * info.point
if sl and abs(gia - sl) < khoang_cach_min:
raise ValueError(f"SL quá gần giá ({abs(gia - sl):.5f} < {khoang_cach_min:.5f})")
if tp and abs(gia - tp) < khoang_cach_min:
raise ValueError(f"TP quá gần giá ({abs(gia - tp):.5f} < {khoang_cach_min:.5f})")
request = {
"action": mt5.TRADE_ACTION_DEAL,
"symbol": symbol,
"volume": chuan_hoa_khoi_luong(symbol, khoi_luong),
"type": loai_lenh,
"price": chuan_hoa_gia(symbol, gia),
"sl": chuan_hoa_gia(symbol, sl) if sl else 0.0,
"tp": chuan_hoa_gia(symbol, tp) if tp else 0.0,
"deviation": 20, # trượt giá tối đa (point)
"magic": magic, # định danh bot — rất quan trọng
"comment": ghi_chu,
"type_time": mt5.ORDER_TIME_GTC,
"type_filling": mt5.ORDER_FILLING_IOC,
}
ket_qua = mt5.order_send(request)
if ket_qua is None:
raise RuntimeError(f"order_send trả None: {mt5.last_error()}")
if ket_qua.retcode != mt5.TRADE_RETCODE_DONE:
# Ví dụ: không đủ margin, thị trường đóng, giá lệch quá deviation
raise RuntimeError(
f"Lệnh thất bại [{ket_qua.retcode}] {ket_qua.comment}"
)
return {
"order": ket_qua.order,
"deal": ket_qua.deal,
"gia_khop": ket_qua.price,
"khoi_luong": ket_qua.volume,
"retcode": ket_qua.retcode,
}
magic là trường bắt buộc trong mọi bot thực tế. Nếu bạn chạy nhiều bot hoặc giao dịch tay trên cùng tài khoản, magic giúp bot chỉ quản lý lệnh của chính nó, không đóng nhầm lệnh tay của bạn.
Bảng mã lỗi MT5 hay gặp:
retcode | Ý nghĩa | Cách xử lý |
|---|---|---|
10009 | Thành công | — |
10004 | Requote | Tăng deviation, thử lại |
10006 | Bị từ chối | Kiểm tra thị trường có mở không |
10013 | Request không hợp lệ | Kiểm tra filling mode, stops_level |
10014 | Khối lượng không hợp lệ | Chạy chuan_hoa_khoi_luong |
10016 | Stop không hợp lệ | SL/TP quá gần giá |
10019 | Không đủ tiền | Giảm khối lượng |
10027 | AutoTrading bị tắt | Bật nút AutoTrading trong terminal |
8. Vòng polling quản lý TP/SL — viết lại cho đúng
Đoạn code trong buổi học có một vấn đề nghiêm trọng: nó tự đóng lệnh khi giá chạm TP/SL thay vì để broker tự làm. Thực tế bạn nên đặt SL/TP vào lệnh và để broker xử lý. Polling chỉ dùng khi API không hỗ trợ, hoặc để làm việc khác (trailing stop, báo cáo).
import time
import logging
from datetime import datetime
import MetaTrader5 as mt5
logger = logging.getLogger(__name__)
def lay_vi_the_cua_bot(symbol: str, magic: int) -> list:
"""CHỈ lấy vị thế do bot này mở — lọc theo magic."""
tat_ca = mt5.positions_get(symbol=symbol) or []
return [p for p in tat_ca if p.magic == magic]
def quản_ly_trailing_stop(symbol: str, magic: int, he_so_atr: float = 2.0):
"""Dời SL theo ATR khi giá đi đúng hướng — khoá lợi nhuận dần."""
df = lay_nen(symbol, "H1", 100)
atr = (df["high"] - df["low"]).rolling(14).mean().iloc[-1]
for vt in lay_vi_the_cua_bot(symbol, magic):
tick = mt5.symbol_info_tick(symbol)
info = mt5.symbol_info(symbol)
if vt.type == mt5.POSITION_TYPE_BUY:
gia_hien_tai = tick.bid
sl_moi = gia_hien_tai - atr * he_so_atr
# Chỉ dời LÊN, không bao giờ dời xuống
if sl_moi <= vt.sl:
continue
if sl_moi <= vt.price_open:
continue # chưa tới điểm hoà vốn
else:
gia_hien_tai = tick.ask
sl_moi = gia_hien_tai + atr * he_so_atr
if vt.sl != 0 and sl_moi >= vt.sl:
continue
if sl_moi >= vt.price_open:
continue
request = {
"action": mt5.TRADE_ACTION_SLTP,
"symbol": symbol,
"position": vt.ticket,
"sl": chuan_hoa_gia(symbol, sl_moi),
"tp": vt.tp,
"magic": magic,
}
ket_qua = mt5.order_send(request)
if ket_qua and ket_qua.retcode == mt5.TRADE_RETCODE_DONE:
logger.info("📈 Dời SL vé %s → %.5f", vt.ticket, sl_moi)
else:
logger.warning("Dời SL thất bại vé %s: %s", vt.ticket,
ket_qua.comment if ket_qua else mt5.last_error())
def vong_lap_bot(symbol: str, khung: str = "H1", magic: int = 20260101,
poll_giay: int = 30, khoi_luong: float = 0.01):
"""Vòng lặp bot hoàn chỉnh."""
da_xu_ly_nen = None
while True:
try:
df = them_tin_hieu_hma(lay_nen(symbol, khung, 300))
nen_cuoi = df.index[-1]
# Chỉ xử lý một lần cho mỗi nến đã đóng
if nen_cuoi == da_xu_ly_nen:
quản_ly_trailing_stop(symbol, magic)
time.sleep(poll_giay)
continue
da_xu_ly_nen = nen_cuoi
dong = df.iloc[-1]
vi_the = lay_vi_the_cua_bot(symbol, magic)
co_lenh_mua = any(p.type == mt5.POSITION_TYPE_BUY for p in vi_the)
co_lenh_ban = any(p.type == mt5.POSITION_TYPE_SELL for p in vi_the)
tick = mt5.symbol_info_tick(symbol)
atr = (df["high"] - df["low"]).rolling(14).mean().iloc[-1]
logger.info("Nến %s | giá %.2f | tăng=%s | giảm=%s | chồng chéo=%s | vị thế=%d",
nen_cuoi, dong["close"], dong["xu_huong_tang"],
dong["xu_huong_giam"], dong["chong_cheo"], len(vi_the))
# Bộ lọc: không giao dịch khi HMA chồng chéo
if dong["chong_cheo"]:
logger.info("⏸ Bỏ qua — HMA chồng chéo")
time.sleep(poll_giay)
continue
# Giới hạn: tối đa 1 vị thế mỗi hướng
if dong["doi_sang_tang"] and not co_lenh_mua:
if co_lenh_ban:
dong_tat_ca(symbol, magic)
mo_lenh(symbol, "MUA", khoi_luong,
sl=tick.ask - atr * 2, tp=tick.ask + atr * 4,
magic=magic, ghi_chu="HMA tang")
elif dong["doi_sang_giam"] and not co_lenh_ban:
if co_lenh_mua:
dong_tat_ca(symbol, magic)
mo_lenh(symbol, "BAN", khoi_luong,
sl=tick.bid + atr * 2, tp=tick.bid - atr * 4,
magic=magic, ghi_chu="HMA giam")
except Exception:
logger.exception("Lỗi trong vòng lặp bot — tiếp tục chạy")
time.sleep(poll_giay)
Ba cải tiến so với code buổi học:
- Lọc theo
magic— bot chỉ đụng vào lệnh của chính nó. - Chỉ xử lý mỗi nến một lần (
da_xu_ly_nen) — không đặt lệnh lặp lại trong cùng một nến. - Ghi log có cấu trúc — biết bot đang nghĩ gì ở mỗi nến.
9. Backtest trước khi chạy tiền thật
Không có backtest thì không có niềm tin. Dù đơn giản, một backtest vector hoá vẫn cho bạn con số đáng tham khảo:
import numpy as np
import pandas as pd
def backtest_hma(df: pd.DataFrame, phi_moi_lenh: float = 0.0002) -> dict:
"""Backtest đơn giản: vào lệnh khi xu hướng đổi, đóng khi đảo chiều."""
vi_the = 0 # 0: không, 1: mua, -1: bán
gia_vao = 0.0
von = 1.0
so_lenh = 0
thang = 0
duong_von = []
for i in range(1, len(df)):
dong = df.iloc[i]
gia = dong["close"]
if vi_the == 0:
if dong["doi_sang_tang"] and not dong["chong_cheo"]:
vi_the, gia_vao = 1, gia
so_lenh += 1
elif dong["doi_sang_giam"] and not dong["chong_cheo"]:
vi_the, gia_vao = -1, gia
so_lenh += 1
else:
doi_chieu = ((vi_the == 1 and dong["doi_sang_giam"]) or
(vi_the == -1 and dong["doi_sang_tang"]))
if doi_chieu:
loi_nhuan = (gia / gia_vao - 1) * vi_the - phi_moi_lenh
von *= (1 + loi_nhuan)
if loi_nhuan > 0:
thang += 1
vi_the = 0
duong_von.append(von)
duong_von = pd.Series(duong_von)
dinh = duong_von.cummax()
drawdown = (duong_von / dinh - 1)
return {
"so_lenh": so_lenh,
"ty_le_thang": round(thang / so_lenh * 100, 1) if so_lenh else 0,
"loi_nhuan_tong_pct": round((von - 1) * 100, 2),
"max_drawdown_pct": round(drawdown.min() * 100, 2),
"he_so_profit_factor": round(von, 4),
}
df = them_tin_hieu_hma(lay_nen("XAUUSD", "H1", 5000))
print(backtest_hma(df))
⚠️ Giới hạn của backtest này — phải nói rõ: nó bỏ qua spread, slippage, swap qua đêm, và giả định khớp lệnh ở giá đóng nến. Kết quả thực tế luôn xấu hơn. Hãy dùng nó để loại bỏ chiến lược tệ, không phải để tin vào con số đẹp.
10. Vận hành trên VPS
MT5 yêu cầu Windows, nên bot cần VPS Windows:
| Yêu cầu | Khuyến nghị |
|---|---|
| RAM | 4 GB (MT5 + Python) |
| CPU | 2 vCPU |
| Ổ đĩa | 60 GB SSD |
| Hệ điều hành | Windows Server 2019/2022 |
| Vị trí | Gần server broker để giảm độ trễ |
| Giá tham khảo | 10–25 USD/tháng |
Bốn việc phải làm khi lên VPS:
- Chạy bot như Windows Service (dùng
nssm) để tự khởi động lại sau khi VPS restart. - Bật AutoTrading trong MT5 và không đóng cửa sổ terminal.
- Log ra file có xoay vòng — không để log làm đầy đĩa.
- Giám sát từ xa — gửi tin nhắn Telegram mỗi khi bot mở/đóng lệnh hoặc gặp lỗi.
import requests
def thong_bao_telegram(noi_dung: str):
"""Gửi thông báo — cách đơn giản nhất để biết bot còn sống."""
token = os.environ.get("TELEGRAM_BOT_TOKEN")
chat_id = os.environ.get("TELEGRAM_CHAT_ID")
if not token or not chat_id:
return
try:
requests.post(
f"https://api.telegram.org/bot{token}/sendMessage",
json={"chat_id": chat_id, "text": noi_dung, "parse_mode": "HTML"},
timeout=10,
)
except Exception as e:
logger.warning("Không gửi được Telegram: %s", e)
💡 Mẹo vận hành: hãy để bot gửi tin nhắn "còn sống" mỗi 6 giờ. Nếu bạn không nhận được tin nhắn, bot đã chết — và bạn biết trước khi mất tiền.
11. Sai lầm phổ biến khi xây bot MT5
| Sai lầm | Hậu quả | Cách tránh |
|---|---|---|
Không dùng magic | Bot đóng nhầm lệnh tay | Luôn đặt magic và lọc theo nó |
| Không chuẩn hoá khối lượng | retcode 10014, lệnh bị từ chối | Dùng volume_step từ symbol_info |
| Đặt SL/TP quá gần | retcode 10016 | Kiểm tra trade_stops_level |
| Chạy trên tài khoản thật ngay | Mất tiền do bug logic | Demo tối thiểu 4 tuần |
| Không giới hạn số lệnh | Một lỗi vòng lặp mở 1000 lệnh | Đếm và chặn theo magic |
| Polling quá nhanh | Tốn CPU, bị broker giới hạn | sleep ≥ 10 giây |
Bỏ qua retcode | Tưởng đã vào lệnh nhưng thất bại | Luôn kiểm tra retcode == 10009 |
| Không log | Không biết vì sao bot mất tiền | Log mọi quyết định |
12. Câu hỏi thường gặp
MT5 có chạy được trên Linux/macOS không?
Thư viện MetaTrader5 chính thức chỉ hỗ trợ Windows. Trên Linux cần Wine — chạy được nhưng không ổn định và không khuyến nghị cho tiền thật.
Có cần VPS không hay chạy máy nhà được? Máy nhà được nếu bạn không tắt máy và mạng ổn định. Nhưng một lần mất điện đúng lúc có vị thế mở có thể khiến bạn mất kiểm soát SL/TP. VPS đáng tiền.
TradingView có giới hạn số alert không? Có. Gói miễn phí giới hạn số alert và không cho webhook. Gói trả phí cho nhiều alert hơn. Đây là chi phí thật cần tính vào.
Bot có cần máy học không? Không, và phần lớn bot thành công không dùng ML. Quản lý rủi ro, kỷ luật thực thi, và chiến lược đơn giản nhưng được kiểm chứng quan trọng hơn nhiều so với model phức tạp.
Làm sao biết bot đang hoạt động đúng? Ba dấu hiệu: (1) log ghi đều mỗi nến, (2) tin nhắn Telegram "còn sống" đều đặn, (3) số vị thế mở khớp với kỳ vọng của chiến lược.
Nên dùng khung thời gian nào? Khung càng lớn, tín hiệu càng ít nhưng nhiễu càng thấp. Với người mới: H1 hoặc H4. Khung M1/M5 cần hạ tầng tốt hơn và chi phí giao dịch cao hơn nhiều.
13. Checklist trước khi chạy tiền thật
- Đã chạy demo tối thiểu 4 tuần liên tục
- Đã backtest tối thiểu 2 năm dữ liệu, có tính spread và phí
- Đã kiểm tra hành vi khi mất mạng và khi MT5 bị đóng
- Có giới hạn số lệnh tối đa mỗi ngày
- Có giới hạn drawdown — bot tự dừng nếu lỗ quá X%
- SL luôn được đặt, không bao giờ có lệnh không SL
- Rủi ro mỗi lệnh ≤ 1% số dư
- Có thông báo Telegram cho mọi lệnh mở/đóng
- Có log và biết cách đọc log
- Đã tính chi phí: VPS + TradingView + spread + swap
- Đã chuẩn bị tâm lý cho chuỗi thua 5–7 lệnh liên tiếp (chuyện bình thường)
⚖️ Miễn trừ trách nhiệm: Nội dung này chỉ nhằm mục đích giáo dục về lập trình và kỹ thuật. Giao dịch forex, CFD và tài sản số có rủi ro rất cao — phần lớn nhà đầu tư cá nhân thua lỗ. Không có chiến lược nào đảm bảo lợi nhuận. Hãy tự nghiên cứu và chỉ dùng số tiền bạn có thể chấp nhận mất hoàn toàn.
14. Tóm tắt
Trong phần mở rộng này bạn đã đi qua:
✅ Kiến trúc MT5 + Python + TradingView + n8n
✅ Cài đặt và kết nối an toàn với .env
✅ HMA/AMA — cài đặt đúng để khớp Pine Script
✅ Pine Script có alert gửi JSON qua webhook
✅ Webhook receiver bảo mật bằng HMAC
✅ Đặt lệnh MT5 có chuẩn hoá khối lượng/giá và kiểm tra retcode
✅ Vòng polling đúng cách, lọc theo magic
✅ Backtest và giới hạn của nó
✅ Vận hành trên VPS có giám sát
Điều quan trọng nhất không nằm ở code: đó là hiểu rằng không giao dịch khi HMA chồng chéo, và luôn giới hạn rủi ro mỗi lệnh. Code chỉ thực thi những quyết định đó.