Python trong giao dịch – phân tích dữ liệu, tự động hóa và nghiên cứu

Python giúp tổ chức dữ liệu thị trường, phân tích nhật ký và lặp lại phép tính mà không sao chép công thức thủ công. Nó đặc biệt hữu ích khi bảng tính quá nhỏ hoặc khó duy trì. Bài này dẫn từ thiết lập môi trường tới đoạn mã tải dữ liệu, kiểm tra chất lượng và lưu kết quả. Đồng thời giải thích nghiên cứu trong notebook khác phần mềm kết nối tài khoản giao dịch ra sao và việc tích hợp cần những biện pháp bảo vệ nào.

Python hỗ trợ phân tích dữ liệu lặp lại được và kiểm tra kết quả; mã không thay thế chất lượng dữ liệu hay phương pháp kiểm thử đúng.
Điểm chính trong 60 giây
  • Python trong nghiên cứu và Python trong thực thi có mức trách nhiệm khác nhau – notebook tính thống kê có thể dừng lỗi mà không gây hậu quả. Mã tác động tới tài khoản phải có giới hạn, nhật ký, thời hạn chờ, chế độ thử nghiệm và cơ chế dừng có kiểm soát
  • Môi trường phải tái lập được – phiên bản Python, môi trường ảo cô lập, phiên bản thư viện cố định, nguồn dữ liệu, múi giờ và khoảng ngày. Thiếu các yếu tố ấy, “chạy được trên máy tôi” là giai thoại, không phải kết quả
  • pandas và numpy thay bảng tính trong phân tích OHLCV – lấy mẫu lại từ M1 sang H4, trung bình trượt, sụt giảm vốn, tương quan giữa các cặp. Bảng tính dừng ở 1.048.576 dòng; dữ liệu tick vượt xa mức đó
  • Không bao giờ đặt khóa và mật khẩu trong mã – chỉ dùng biến môi trường hoặc tệp cấu hình ngoài kho mã. Khóa nhập vào mã sẽ đi vào lịch sử Git và vẫn còn sau mỗi lần “xóa”
  • API không đồng nghĩa với truy cập không giới hạn – có xác thực, giới hạn tần suất yêu cầu và mã lỗi; môi trường thử nghiệm đôi khi, nhưng không phải luôn luôn, nằm ở máy chủ riêng. Mã không xử lý HTTP 429 và không có thời hạn chờ sớm muộn sẽ bị ngắt đúng lúc bất lợi nhất
  • Thiên lệch nhìn trước chỉ cách một thao tác dịch trong mã – vị thế quyết định khi nến đóng chỉ tác động lên lợi nhuận của nến tiếp theo. Nhân tín hiệu với lợi nhuận cùng dòng tạo ra kết quả thị trường chưa từng cung cấp
  • Mã chưa chạy là mã chưa được kiểm tra – biên dịch được và “trông đúng” không đồng nghĩa với chạy trên dữ liệu. Sáu khối mã thực thi đã được kiểm tra ngoại tuyến bằng dữ liệu và mô phỏng được công bố. Các khối đầu ra là kết quả thực tế của kiểm tra ấy; lệnh cài đặt mô tả thiết lập môi trường, còn phần gửi lệnh vẫn được ghi rõ là phác thảo cấu trúc không thể thực thi

1. Python hữu ích ở đâu, và ở đâu chỉ có vẻ là giải pháp

Bảng tính xử lý được nhật ký giao dịch chừng nào còn nằm trong giới hạn kiên nhẫn của bạn — ranh giới đó tùy máy, số công thức và cấu trúc bảng, nên không thể mô tả bằng một số giao dịch tròn trịa. Tuy nhiên, có một giới hạn cứng, không phụ thuộc gì khác vì được ghi trong đặc tả định dạng: mỗi trang tính Excel chứa 1.048.576 dòng và 16.384 cột [16]. Một năm dữ liệu tick của một cặp tiền đã lớn gấp nhiều lần — không thủ thuật nào vượt được giới hạn ấy.

Python không phải phép màu. Nó giải quyết bốn vấn đề cụ thể mà bảng tính không giải quyết được, hoặc phải tốn hàng giờ:

  • Quy mô dữ liệu – giới hạn bởi bộ nhớ máy và định dạng lưu trữ, không bởi số dòng của trang tính
  • Khả năng lặp lại – cùng mã chạy ngày mai trên cùng dữ liệu cho cùng kết quả. Bảng tính kéo công thức thủ công không bảo đảm điều đó
  • Đổi khoảng thời gian không cần làm thủ công – tổng hợp M1 thành H4 mà giữ OHLCV chỉ là một lần gọi resample, không phải một cột công thức phụ
  • Tự động lấy và làm sạch dữ liệu – tập lệnh theo lịch tải, kiểm tra và lưu dữ liệu khi bạn không có mặt

Chỉ vậy thôi. Python không tạo lợi thế thị trường, không nâng chất lượng chiến lược và không đổi điều kiện thực thi tại nhà môi giới. Nó rút ngắn khoảng cách giữa câu hỏi và câu trả lời — chính vì thế, nó cho bạn thấy ý tưởng không hiệu quả nhanh hơn.

Bảng so sánh bảng tính và Python theo năm tiêu chí công việc
Chọn theo quy mô công việc và khả năng lặp lại; đánh giá riêng chất lượng chiến lược.

Python không làm được gì. Nó không phù hợp giao dịch có độ trễ cực thấp — ở đó từng micro giây đều quan trọng, và ngôn ngữ thông dịch có bộ thu gom bộ nhớ là lựa chọn kém. Nhưng câu “Python quá chậm để giao dịch” cũng sai: với chiến lược vào đầu nến H1 tiếp theo, thời gian tính Python không đáng kể so với thời gian lệnh đi qua mạng và máy chủ nhà môi giới.

Tăng tốc nhờ vector hóa có cơ chế cụ thể: thao tác cả cột chạy trong mã numpy đã biên dịch thay vì vòng lặp thông dịch. Mức chênh lệch tùy dữ liệu, loại thao tác và phần cứng — muốn nêu con số cụ thể, hãy đo trên máy mình bằng mô-đun timeit của thư viện chuẩn [4] và ghi kèm phiên bản thư viện. Dẫn chuẩn đo hiệu năng của người khác mà thiếu môi trường không phải phép đo.

So sánh công cụ phân tích của người giao dịch:

Đặc điểmExcelPythonPine ScriptMQL4/MQL5
Giới hạn dữ liệu1.048.576 dòng mỗi trang tínhGiới hạn bởi bộ nhớ máyTùy gói TradingViewLịch sử do nhà môi giới cung cấp
Nơi mã chạyCục bộCục bộ, trên máy chủ hoặc đám mâyTrong môi trường TradingViewTrong phần mềm MetaTrader
Truy cập tài khoảnKhôngChỉ qua API nhà môi giới hoặc gói trung gianKhông — cảnh báo có thể kích hoạt chuỗi xử lý tiếp theoTích hợp trong phần mềm
Nguồn dữ liệuBất cứ dữ liệu nào bạn tải vàoBất kỳ, nhưng phải nêu rõNguồn dữ liệu TradingViewNguồn dữ liệu nhà môi giới
Khả năng tái lậpThấp (công thức thủ công)Cao nếu cố định phiên bảnTrung bình (phiên bản Pine và dữ liệu nền tảng)Trung bình (bản dựng phần mềm và dữ liệu nhà môi giới)
Độ khó họcThấpTrung bìnhThấp đến trung bìnhTrung bình đến cao

Phân biệt ở dòng cuối quan trọng hơn vẻ ngoài. Pine Script chạy trong TradingView, MQL trong phần mềm MetaTrader, còn Python ở bất cứ nơi nào bạn chạy nó. Sự tự do ấy có nghĩa bạn chịu trách nhiệm về môi trường: phiên bản thư viện, múi giờ hệ thống, thời gian hoạt động và cả việc mã có được phép tác động tài khoản hay không.

Phạm vi bài viết. Đây không phải khóa học Python hay khóa học backtest. Phương pháp kiểm thử — chia tập huấn luyện và xác thực, kiểm thử tiến, mô hình chi phí — thuộc bài Tài khoản demo, backtest và kiểm thử tiến. Mã chạy trong nền tảng và duy trì nó trên máy chủ được trình bày trong Tự động hóa giao dịch: EA, tập lệnh và VPS. Mô hình ngôn ngữ và giới hạn của chúng nằm trong AI và LLM trong giao dịch. Ở đây chỉ xét Python như lớp dữ liệu, tính toán và tích hợp.
Bạn không cần bỏ bảng tính. Có thể xuất kết quả Python ra CSV rồi mở bằng bảng tính, hoặc ngược lại: giữ nhật ký giao dịch trong bảng tính rồi tải vào pandas để phân tích. Thư viện openpyxl cho phép đọc và ghi trực tiếp tệp .xlsx từ Python; bản phát hành mới nhất của nó có từ tháng 6 năm 2024 (3.1.5) [13] và trong trường hợp này, đó không phải dấu hiệu đáng ngại — định dạng .xlsx đã ổn định, thư viện không phụ thuộc NumPy hay pandas, nên quy tắc ở phần 2 không loại nó đi, chỉ yêu cầu bạn biết rõ tình trạng ấy [CẬP NHẬT: 2026-08-22]. Python mở rộng bảng tính tại nơi bảng tính dừng lại.

2. Môi trường tái lập: phiên bản, venv, requirements

Nguyên nhân rất thường gặp khiến mã của người khác “không chạy” không nằm trong mã. Tác giả đã chạy bằng phiên bản thư viện khác và không ghi lại. Trong phân tích dữ liệu thị trường, đây không chỉ là bất tiện — nó làm kết quả mất giá trị vì không thể tái lập.

Môi trường tái lập khi nêu được sáu điều: phiên bản Python, phương pháp cô lập, phiên bản phụ thuộc cố định, nguồn dữ liệu và khoảng ngày, múi giờ, cùng hạt giống ngẫu nhiên ở mọi nơi kết quả phụ thuộc ngẫu nhiên. Thiếu một yếu tố, kết quả là câu chuyện chứ không phải phép đo.

Môi trường ảo là bắt buộc. Cài thư viện “toàn cục” khiến cập nhật dự án này có thể làm hỏng dự án khác. Cô lập không tốn phí, chỉ mất khoảng chục giây và dựa vào mô-đun venv của thư viện chuẩn [4]:

# Windows, PowerShell
py -3.12 -m venv .venv
.venv\Scripts\Activate.ps1

# macOS / Linux
python3.12 -m venv .venv
source .venv/bin/activate

# dùng chung cho hai hệ thống
python -m pip install --upgrade pip
python -m pip install pandas numpy matplotlib requests python-dotenv
python -m pip freeze > requirements.txt

Dòng cuối thường bị bỏ qua nhất. pip freeze ghi lại chính xác phiên bản mọi thứ thực sự đã cài — kể cả phụ thuộc bạn không biết tới. Đưa tệp đó vào kho cạnh mã. Khi ấy tái tạo môi trường trên máy khác là python -m pip install -r requirements.txt, không phải đoán.

Môi trường dùng cho lần kiểm tra ngoại tuyến hiện tại của các ví dụ tính toán phần 4 và 9: Windows 11, Python 3.12.10, pandas 3.0.5 và NumPy 2.5.3, kiểm tra ngày 8 tháng 10 năm 2026. Danh sách dưới ghi các gói liên quan những phép tính ấy; đây là danh mục giới hạn, không phải bản xuất mọi gói cài trên máy[1][3].

# Phiên bản phụ thuộc dùng để xác minh tính toán cục bộ
numpy==2.5.3
pandas==3.0.5
python-dateutil==2.9.0.post0
six==1.17.0
tzdata==2026.4

# trình thông dịch
Python 3.12.10

Lưu ý pandas còn dùng các gói khác: mô tả môi trường không dừng ở một phiên bản thư viện. Phụ thuộc có thể gây khác biệt giữa các máy. Các lệnh cài ở trên minh họa quy trình thiết lập dự án; không phải hồ sơ cài đặt đã thực hiện trong lần kiểm tra này. Hãy tạo tệp requirements.txt đầy đủ trong chính môi trường của bạn bằng python -m pip freeze và giữ cùng mã.

Tệp này cũng không mô tả toàn bộ môi trường cho cả bài, mà chỉ cho các phần tính toán. Đoạn kiểm tra xử lý lỗi HTTP ở phần 7 đã chạy trong môi trường bổ sung requests 2.34.2, còn kiểm tra cổng an toàn ở phần 8 không cần gói ngoài thư viện chuẩn. Không nêu phiên bản matplotlib và python-dotenv vì chúng không được dùng trong ví dụ nào thực sự chạy — liệt kê “cho đầy đủ” sẽ thành ghi con số chưa ai kiểm tra. Nếu dựng môi trường cho toàn bộ nội dung, hãy coi lệnh cài ở trên là điểm khởi đầu và cố định pip freeze của riêng bạn; chỉ tệp phụ thuộc ấy mới nói được điều gì về kết quả của bạn.

Notebook nghiên cứu minh họa với dữ liệu UTC, kiểm tra giá trị thiếu và xuất kết quả, không gửi lệnh.
Notebook hỗ trợ phân tích dữ liệu tái lập. Đầu ra cần được xác thực trước khi dùng trong hệ thống thực thi riêng.

Notebook hay tệp .py? Cả hai, nhưng cho việc khác nhau. Jupyter hữu ích khi chưa biết đang tìm gì: chạy một ô, xem kết quả, đổi tham số. Tuy nhiên, một đặc tính khiến nó không phù hợp làm nơi chứa mã thực thi: trạng thái biến phụ thuộc thứ tự chạy ô, không phụ thuộc thứ tự ô trong tệp. Notebook chạy theo thứ tự 1–3–2 trông giống hệt notebook chạy tuần tự. Mã tác động tài khoản phải nằm trong tệp .py thông thường, chạy từ trên xuống, có kiểm thử và nhật ký.

Anaconda và Google Colab là lối tắt có chi phí riêng. Anaconda cài nhiều gói một lúc và có trình quản lý phụ thuộc riêng, giúp bắt đầu dễ hơn nhưng khó mô tả chính xác môi trường khi trộn conda với pip. Colab chạy trong trình duyệt không cần cài, phù hợp để học, nhưng mỗi phiên lại tạo môi trường từ đầu và phiên bản thư viện thay đổi ngoài sự kiểm soát của bạn. Với tài liệu cần tái lập sau một năm, cả hai đều đòi hỏi thêm kỷ luật.

Thư viện để bắt đầu – bốn mục, không phải bốn mươi (có năm gói vì mục đầu là một cặp):

  • pandas và numpy – cấu trúc dữ liệu và tính toán
  • matplotlib – biểu đồ
  • requests – giao tiếp HTTP với API
  • python-dotenv – tải cấu hình từ tệp ngoài kho mã

Chỉ thêm chỉ báo kỹ thuật khi thực sự cần. Một điểm cần sửa so với hướng dẫn cũ: TA-Lib phát hành gói nhị phân wheel chính thức kèm thư viện C trên PyPI; vì thế với tổ hợp hệ điều hành, kiến trúc và Python được hỗ trợ, lệnh pip install TA-Lib không cần tải bản không chính thức hay tự biên dịch[9]. Kiểm tra wheel cụ thể trước khi cài; đây không phải lời hứa chạy trên mọi máy. Siêu dữ liệu kiểm tra ngày 8 tháng 10 năm 2026 ghi TA-Lib 0.8.1 và pandas-ta vẫn ở 0.4.71b0, phát hành tháng 9 năm 2025[13]. Gói sau cố định numba==0.61.2, nên chỉ kiểm tra yêu cầu pandas là chưa đủ. Kiểm tra cài đặt lịch sử trên Python 3.14.3, ngày 22 tháng 8 năm 2026, thất bại tại phụ thuộc đó với thông báo only versions >=3.10,<3.14 are supported. Quan sát ấy không tự động áp dụng cho ví dụ hiện tại chạy Python 3.12.10. Lần kiểm tra hiện tại đọc siêu dữ liệu PyPI; không cài hay chạy TA-Lib hoặc pandas-ta. Vì thế quy tắc kiểm tra bên cạnh còn quá hẹp: phát hành gần đây và yêu cầu pandas tương thích không chứng minh toàn bộ phụ thuộc cài được và chạy đúng. Cũng phải kiểm tra thư viện biên dịch bị cố định, wheel cho trình thông dịch và phép thử với dữ liệu nhỏ. Siêu dữ liệu hai bản mới nhất không mô tả toàn bộ lịch sử dự án; hãy đánh giá các khoảng ngừng trước đó trong kho mã.

Trước khi thêm thư viện, kiểm tra ngày phát hành gần nhất. Dự án lâu không phát triển sớm muộn sẽ lệch khỏi NumPy hoặc pandas mới — khi đó phải cố định phiên bản cũ trong môi trường riêng hoặc viết lại mã. Ngày phát hành gần nhất hiện trên trang gói PyPI [13] và nói nhiều hơn số sao GitHub. Điều này đặc biệt đúng với các framework backtest cũ.

Hạt giống ngẫu nhiên. Ở mọi nơi kết quả phụ thuộc ngẫu nhiên — chia mẫu, bootstrap, mô phỏng, khởi tạo mô hình — phải đặt và ghi hạt giống. Trong numpy: rng = np.random.default_rng(20260822) [3], rồi chỉ dùng đối tượng ấy. Thiếu nó, hai lần chạy cùng mã cho hai kết quả khác nhau và không rõ bạn đang mô tả kết quả nào.

3. pandas và numpy: dữ liệu OHLCV, chỉ số thời gian, múi giờ

numpy xử lý tính toán trên mảng số, pandas xử lý cấu trúc: DataFrame là bảng có tên cột và chỉ số, thường là chỉ số thời gian. Với dữ liệu thị trường, chỉ số thời gian quan trọng hơn phép tính, vì nó quyết định bạn có thực sự tính đúng điều mình nghĩ hay không.

Tải dữ liệu – pd.read_csv() xử lý được tệp hàng triệu dòng, nhận biết dấu phân cách và tự phân tích ngày khi chỉ rõ cột. Tuy nhiên, mặc định nó không biết múi giờ; đó là điều đầu tiên phải xác lập.

Đổi khoảng thời gian – tổng hợp M1 thành H4 mà giữ OHLCV chỉ cần một lần gọi: df.resample('4h').agg({'open':'first', 'high':'max', 'low':'min', 'close':'last', 'tick_volume':'sum'}). Chú ý hai đối số thường bị bỏ qua nhưng quyết định cách diễn giải: label quyết định gắn nhãn đầu hay cuối cửa sổ, còn closed quyết định mép khoảng nào thuộc cửa sổ. Với phần lớn tần suất, mặc định gắn nhãn tại mép trái [1], tức dấu thời gian nến là lúc mở; trong pandas 3.0.5, resample('4h') trên tám nến giờ từ nửa đêm cho nhãn 00:00 và 04:00. Nếu quy ước tín hiệu giả định lúc đóng nhưng dữ liệu gắn nhãn lúc mở, bạn vô tình dịch toàn bộ kết quả một khoảng.

Thống kê trượt – df['close'].rolling(20).mean() cho trung bình động, .rolling(20).std() cho độ lệch dùng trong Dải Bollinger, .ewm(span=20).mean() cho trung bình hàm mũ [1]. Tất cả chỉ tính từ dữ liệu quá khứ so với dòng hiện tại — tính chất ấy là nền tảng cho tính đúng đắn của mọi bước còn lại.

Lọc dữ liệu – điều kiện kết hợp chỉ cần viết một lần: df[(df['sma20'] > df['sma50']) & (df['rsi'] < 30)]. Bắt buộc đặt ngoặc quanh từng điều kiện, vì & có độ ưu tiên cao hơn phép so sánh trong Python — bỏ ngoặc gây lỗi thay vì âm thầm cho đáp án sai; ở điểm này, đó là điều có lợi.

Bảng nhất quán gồm bốn nến OHLCV tổng hợp và kiểm tra dữ liệu
Định dạng DataFrame không xác lập chất lượng dữ liệu; kiểm tra giá trị, thời gian và định nghĩa khối lượng.

Múi giờ là quyết định, không phải chi tiết. Dữ liệu nhà môi giới thường mang dấu thời gian máy chủ, nguồn ngoài thường dùng UTC, còn hệ điều hành có thể dùng giờ địa phương với quy tắc đổi giờ của vùng đã chọn. Trộn ba đồng hồ này sẽ khiến phân tích “phiên London” nghiên cứu thứ khác với phiên London.

Quy tắc thực hành là xác định múi giờ nguồn trước, rồi tính bằng UTC. Với dấu thời gian có độ lệch rõ ràng hoặc Z, dùng pd.to_datetime(column, utc=True). Với thời gian máy chủ không có độ lệch, trước hết phân tích rồi gán múi giờ đã xác nhận bằng tz_localize(), xử lý các giờ nhập nhằng hoặc không tồn tại, sau đó dùng tz_convert("UTC"). Chỉ dùng utc=True sẽ coi giá trị không có múi giờ là UTC, không suy ra múi giờ nhà môi giới. Chỉ đổi sang Europe/London khi trình bày[1]. Chỉ số có múi giờ rõ ràng và nguồn dữ liệu được ghi lại giúp kết hợp các chuỗi nhất quán.

Giờ máy chủ nhà môi giới là một giá trị riêng. Nhiều nhà môi giới đặt máy chủ lệch UTC, đôi khi đổi độ lệch theo lịch đổi giờ Mỹ hoặc châu Âu. Đừng đoán theo trí nhớ: kiểm tra đặc tả nhà môi giới và ghi vào cấu hình dự án dưới dạng tham số, không phải số nhập cứng trong mã. [TÍNH NĂNG TÙY NHÀ MÔI GIỚI/TÀI KHOẢN]

Dữ liệu thiếu. Lịch sử OHLCV có khoảng trống: cuối tuần, ngày lễ, nghỉ kỹ thuật, công cụ không báo giá. pandas cung cấp df.ffill(), điền khoảng trống bằng giá trị cuối đã biết — với dữ liệu giá, đó thường là ý tưởng tệ, vì tạo nến không biến động và làm giảm độ lệch chuẩn. Tốt hơn nên loại các kỳ thiếu và đếm số lượng.

Điều chỉnh so với tài liệu cũ. Cú pháp df.fillna(method='ffill'), thường gặp trong hướng dẫn vài năm trước, đã bị rút bỏ: tham số method của fillna() được đánh dấu ngừng khuyến nghị ở pandas 2.1 [2] và bị loại bỏ trong pandas 3.0. Khác biệt rất thực tế: cú pháp cũ không còn chỉ cảnh báo mà kết thúc chương trình với thông báo TypeError: NDFrame.fillna() got an unexpected keyword argument 'method'. Cú pháp đúng là df.ffill() và df.bfill(). Chữ ký hàm trong tài liệu pandas 3.0.5 không còn tham số ấy — DataFrame.fillna(value, *, axis=None, inplace=False, limit=None) [1]. Chữ ký và nội dung thông báo đã được kiểm tra bằng cách chạy trên pandas 3.0.5, không phải chỉ đọc tài liệu. [CẬP NHẬT: 2026-08-22]
Khối lượng tick không phải khối lượng toàn thị trường. Trên thị trường tiền tệ phi tập trung, khách hàng cá nhân không nhận được doanh số hợp nhất — chỉ nhận số lần báo giá đổi trong nguồn của một nhà môi giới cụ thể. Có thể thận trọng dùng làm đại diện hoạt động, nhưng cột tên “volume” trong tệp không phải khối lượng thị trường và không thể so sánh giữa nhà môi giới.

Nhiều công cụ cùng lúc. Để phân tích danh mục tiền tệ, pandas có chỉ số phân cấp MultiIndex: một DataFrame chứa nhiều cặp, đánh chỉ số theo cặp và thời gian. Có thể tính tương quan trên trục thời gian chung, với điều kiện đã đưa mọi chuỗi về cùng múi giờ và cùng lưới thời gian. Nếu không, bạn đang tính tương quan giữa các dữ liệu lệch nhau.

Lưu kết quả. df.to_csv() dễ đọc và phổ dụng. df.to_parquet() tạo tệp nhỏ hơn, đọc nhanh hơn, đồng thời giữ kiểu cột và múi giờ chỉ số [1] – CSV ghi mọi thứ dưới dạng văn bản nên phải phân tích lại khi tải. Với dữ liệu làm việc lớn, Parquet thuận tiện hơn nhưng cần bộ ghi bổ sung, vì vậy hãy thêm vào requirements.txt.

Bối cảnh vĩ mô trong Python. Báo cáo vị thế và dữ liệu lịch có thể lấy bằng mã như giá — với cùng kỷ luật về múi giờ và thời điểm công bố. Tuy nhiên, diễn giải chúng thuộc phần khác của khóa học: Báo cáo COT và vị thế.

4. Ví dụ tối thiểu: CSV → UTC → lợi nhuận → kiểm tra khoảng trống → lưu

Tập lệnh đầu tiên không nên gửi lệnh hay thậm chí kết nối nhà môi giới. Nó nên làm việc hữu ích, kiểm tra được và hoàn toàn an toàn: tải tệp, xử lý thời gian, tính lợi nhuận, nói rõ dữ liệu có lỗi gì và lưu kết quả.

Giả định. Tệp CSV có các cột time,open,high,low,close,tick_volume. Cột time ở định dạng ISO 8601 kèm dấu múi giờ (Z nghĩa là UTC). Khoảng thời gian được khai báo là một giờ. Tệp chạy được cố ý cài ba lỗi thường có trong dữ liệu xuất: một dấu thời gian trùng, một giờ thiếu và một ô khối lượng trống. Sáu dòng được dán trực tiếp vào mã để bạn chạy và nhận đúng cùng kết quả mà không tải tệp — kết quả không thể tái lập không phải kết quả. Môi trường: Python 3.12.10, pandas 3.0.5, numpy 2.5.3, Windows 11, [CẬP NHẬT: 2026-10-08].

"""Ví dụ tối thiểu: CSV -> UTC -> lợi suất log -> kiểm tra khoảng trống -> lưu."""
import io
from pathlib import Path

import numpy as np
import pandas as pd

# Dữ liệu đặt ngay trong mã để tái tạo kết quả mà không tải tệp.
# Sáu hàng với ba lỗi có chủ đích: trùng dấu thời gian 01:00,
# thiếu giờ 03:00 và ô khối lượng trống lúc 02:00.
CSV_DATA = """time,open,high,low,close,tick_volume
2026-06-01T00:00:00Z,1.08140,1.08170,1.08131,1.08153,812
2026-06-01T01:00:00Z,1.08153,1.08190,1.08150,1.08176,640
2026-06-01T01:00:00Z,1.08153,1.08195,1.08149,1.08182,655
2026-06-01T02:00:00Z,1.08182,1.08188,1.08130,1.08141,
2026-06-01T04:00:00Z,1.08120,1.08125,1.08040,1.08050,903
2026-06-01T05:00:00Z,1.08050,1.08061,1.07980,1.07993,774
"""

OUTPUT = Path("EURUSD_H1_returns.csv")
INTERVAL = "1h"  # tần suất khai báo của dữ liệu đầu vào


def load_data(source) -> pd.DataFrame:
    """Nạp CSV và đặt chỉ mục có múi giờ theo UTC.

    Cố ý không sắp xếp: thứ tự hàng trong tệp cũng là thông tin
    về chất lượng xuất dữ liệu. Chỉ sắp xếp sau khi lập báo cáo.
    """
    df = pd.read_csv(source)
    # utc=True buộc dùng một múi giờ; thiếu nó pandas có thể trả chỉ mục không có múi giờ
    df["time"] = pd.to_datetime(df["time"], utc=True, format="ISO8601")
    return df.set_index("time")


def quality_report(df: pd.DataFrame, interval: str) -> dict:
    """Các bộ đếm cần xem trước khi tính bất kỳ giá trị nào."""
    expected = pd.date_range(df.index.min(), df.index.max(), freq=interval, tz="UTC")
    return {
        "rows": int(len(df)),
        "index_duplicates": int(df.index.duplicated().sum()),
        "missing_cells": int(df.isna().sum().sum()),
        "missing_timestamps": int(len(expected.difference(df.index))),
        # Đo TRƯỚC khi sắp xếp: sau sort_index() kiểm tra này luôn
        # đúng và không bao giờ phát hiện tệp có thứ tự thời gian xáo trộn.
        "file_chronology_ok": bool(df.index.is_monotonic_increasing),
    }


def add_returns(df: pd.DataFrame) -> pd.DataFrame:
    """Lợi suất log = ln(close_t / close_(t-1)). Đơn vị: logarit tự nhiên, không thứ nguyên."""
    df = df[~df.index.duplicated(keep="last")].copy()
    df["log_return"] = np.log(df["close"]).diff()
    return df


def main() -> None:
    df = load_data(io.StringIO(CSV_DATA))  # trong thực tế: load_data(Path("EURUSD_H1.csv"))
    print("kiểm tra chất lượng:", quality_report(df, INTERVAL))
    df = add_returns(df.sort_index())
    print("số nến sau loại trùng:", len(df))
    print("tổng lợi suất log:", round(float(df["log_return"].sum()), 6))
    print("độ lệch chuẩn lợi suất log (khoảng 1h và 2h):",
          round(float(df["log_return"].std()), 6))
    df.to_csv(OUTPUT)
    print("đã lưu:", OUTPUT)


if __name__ == "__main__":
    main()

Đầu ra của lần chạy trên dữ liệu dán trong mã và môi trường đã mô tả — bạn có thể tái lập chính xác:

kiểm tra chất lượng: {'rows': 6, 'index_duplicates': 1, 'missing_cells': 1, 'missing_timestamps': 1, 'file_chronology_ok': True}
số nến sau loại trùng: 5
tổng lợi suất log: -0.00148
độ lệch chuẩn lợi suất log (khoảng 1h và 2h): 0.000467
đã lưu: EURUSD_H1_returns.csv

Diễn giải. Mã phát hiện đủ ba lỗi được cài trước khi tính bất cứ gì — đó là toàn bộ giá trị của nó. Tổng lợi nhuận log là −0,00148; còn năm nến sau loại trùng, tức bốn khoảng giữa các giá quan sát liên tiếp, tương ứng tỷ giá giảm khoảng 0,15 phần trăm trong cửa sổ nghiên cứu. Sáu là số dòng tệp, không phải số nến: một dòng bị trùng. Độ lệch chuẩn 0,000467 là đại lượng cho từng nến H1, không phải theo năm hay ngày; so sánh mà không nêu khoảng thời gian là vô nghĩa. Lợi nhuận log không có đơn vị và cộng được theo thời gian — vì thế cộng chứ không nhân như lợi nhuận đơn.

Giới hạn. Năm nến chỉ minh họa cơ chế, không phải mẫu thống kê — không thể kết luận gì về thị trường từ số quan sát ấy. Mã không mô hình hóa spread, hoa hồng hay trượt giá, vì đây không phải backtest; nó tính lợi nhuận giá, không phải kết quả tài khoản. Phát hiện dấu thời gian thiếu giả định dữ liệu đúng tần suất giờ khai báo — nếu qua cuối tuần, nhiều dấu “thiếu” là bình thường, nên phải đặt ngưỡng cảnh báo theo lịch giao dịch. Một bẫy riêng nằm ở thước đo: mã phát hiện khoảng trống nhưng không điền, nên lợi nhuận bắc qua giờ thiếu bao phủ hai khoảng, không phải một. Trong ví dụ, đó là một trong bốn lợi nhuận, dù cả tập vẫn được gọi là đại lượng “mỗi nến H1” — đây là giản lược, không phải đo chính xác. Cách trung thực có hai hướng: đặt lại chỉ số theo lưới đủ từng giờ và giữ giá trị trống tại khoảng thiếu, hoặc công bố việc trộn thời hạn như giới hạn của phép đo.

Vì sao kiểm tra thứ tự thời gian trước khi sắp xếp. Nếu load_data() kết thúc bằng lần gọi sort_index(), thì kết quả file_chronology_ok sẽ luôn đúng — kiểm tra quảng bá trong báo cáo không thể phát hiện gì, và tệp sai thứ tự sẽ vượt qua mà không để dấu. Kiểm tra không thể thất bại còn tệ hơn không kiểm tra vì tạo cảm giác đã xác thực. Vì thế phải đo thứ tự trên dữ liệu vừa tải từ tệp, chỉ sắp xếp sau khi lập báo cáo.

Tình trạng xác minh: đoạn này đã chạy trong môi trường nêu trên, đầu ra phía trên là đầu ra thật của chương trình. Ở phiên bản pandas khác, định dạng từ điển hoặc cách làm tròn có thể khác.

5. Trực quan hóa: mỗi biểu đồ một câu hỏi

Một con số tóm tắt che giấu điều quan trọng nhất: khi nào chiến lược lỗ, kéo dài bao lâu, và toàn bộ kết quả có dựa vào ba giao dịch không. Biểu đồ không phải vật trang trí báo cáo — nó đặt câu hỏi với dữ liệu mà bảng không trả lời.

matplotlib là thư viện biểu đồ cơ bản của Python: tĩnh và cú pháp khá vụng. plotly cho biểu đồ tương tác — rê vào điểm để xem ngày và giá trị, phóng to một đoạn, xuất HTML. Với tập rất lớn, nó có thể chậm hơn và cần cài riêng. Để khám phá trong notebook, plotly tiện hơn; để lưu biểu đồ vào báo cáo, dùng matplotlib.

Bốn biểu đồ nên biết vẽ vì mỗi loại trả lời một câu hỏi:

  1. Đường vốn – kết quả lũy kế theo thời gian. Trả lời “có tăng và tăng khi nào”, không trả lời “tôi sẽ kiếm bao nhiêu”
  2. Biểu đồ sụt giảm vốn – khoảng cách với đỉnh lũy kế tại từng thời điểm. Thể hiện sụt giảm sâu nhất và thời gian phục hồi. Luôn ghi rõ tính trên số dư hay vốn chủ sở hữu tức thời, bằng phần trăm hay bằng tiền
  3. Bản đồ nhiệt theo phiên – kết quả chia theo giờ và thứ trong tuần. Phải giải quyết múi giờ trước, nếu không nó mô tả những giờ không tồn tại
  4. Phân bố lợi nhuận – biểu đồ tần suất kết quả từng giao dịch. Thể hiện đuôi dày và bất đối xứng mà một chỉ số rủi ro không cho thấy
Biểu đồ vốn tổng hợp và đỉnh lũy kế có sụt giảm âm mười phần trăm
Sụt giảm được đo từ đỉnh lũy kế, với cách xử lý dòng vốn được nêu rõ.

Để vẽ nến trực tiếp từ DataFrame có mplfinance, lớp trên matplotlib — kèm lưu ý từ quy tắc phần 2: bản phát hành gần nhất có từ tháng 8 năm 2023 và vẫn mang số phiên bản tiền phát hành (0.12.10b0) [13] [CẬP NHẬT: 2026-10-08]. Điều ấy không tự động loại nó, nhưng trước khi thêm vào requirements.txt, hãy thử trên dữ liệu mình xem có chạy với phiên bản matplotlib và pandas đang dùng không — bản phát hành mới không kiểm tra thay bạn. Muốn biến tập lệnh thành bảng điều khiển đơn giản có thanh trượt và danh sách chọn, có streamlit, dự án được phát hành đều đặn [13], khởi chạy bằng streamlit run dashboard.py. Cả hai tiết kiệm thời gian và đều thêm phụ thuộc vào requirements.txt, nên hãy thêm có chủ ý.

Quy tắc thực hành: nếu không thể nói bằng một câu biểu đồ trả lời câu hỏi nào, bạn không cần nó. Biểu đồ không có câu hỏi là trang trí; trang trí trong phân tích kết quả có thể đắt giá vì gợi kết luận dữ liệu không chứa.

6. Backtest bằng Python: phần thuộc mã và phần thuộc phương pháp

Đây là ranh giới dễ bỏ qua. Python chịu trách nhiệm triển khai bài kiểm thử: tải dữ liệu, tính tín hiệu, gán kết quả đúng kỳ, tính chi phí và thước đo. Nó không chịu trách nhiệm phương pháp: chia mẫu, số biến thể đã thử, mô hình thực thi, đánh giá độ vững của kết quả. Phương pháp là chủ đề bài riêng — Tài khoản demo, backtest và kiểm thử tiến — không thư viện tốt hơn nào thay thế được.

Chỉ có pandas chưa phải backtest. Nhân cột tín hiệu với cột lợi nhuận tạo chuỗi kết quả, nhưng không mô hình hóa điều xảy ra giữa quyết định và thực thi: thời điểm vào, giá vào, spread, hoa hồng, swap, khớp một phần hay từ chối lệnh. Đây có thể là giản lược chấp nhận được khi sàng ý tưởng — miễn gọi đúng là giản lược, không gọi là kết quả.

Chi phí spread cố định so với biến đổi cho ba giao dịch tổng hợp
Theo giả định đã nêu, chi phí là 3,00 USD hoặc 8,80 USD; không phải nhận định về spread điển hình.

Nếu muốn đi xa hơn vòng lặp tự viết, hệ sinh thái Python có ba hướng công cụ sẵn. Bảng so sánh theo cách hoạt động, không theo lời hứa hiệu năng — thời gian cụ thể tùy dữ liệu, phần cứng và phiên bản, nên nếu cần hãy tự đo và ghi môi trường.

Đặc điểmHướng sự kiện đầy đủHướng vector hóaHướng sự kiện tối giản
Thư viện ví dụbacktradervectorbtbacktesting.py
Cách tínhTừng nến, có mô hình lệnh và danh mục riêngThao tác trên cả mảng cùng lúcTừng nến, mô hình giản lược
Lượng mã cho chiến lược đơn giảnNhiều (lớp, kế thừa)Ít nếu tư duy theo cộtÍt
Ứng dụng tự nhiênLogic danh mục, nhiều công cụ, lệnh phức tạpSàng hàng loạt biến thể và lọc ban đầuHọc và kiểm tra nhanh ý tưởng có hợp lý không
Chi phí chínhĐường học dốcKhó gỡ lỗi hơn; dễ âm thầm dịch sai dữ liệuHạn chế với chiến lược phức tạp
Cần kiểm tra trước khi dùngNgày phát hành PyPI gần nhất, tương thích với pandas và NumPy đang dùng, cách mô hình hóa chi phí và thời điểm khớp
Nhịp phát triển ba dự án này khác nhau rất nhiều. Đây không phải chi tiết cho người sưu tầm: framework không theo kịp pandas và NumPy sớm muộn buộc bạn cố định phiên bản cũ trong môi trường riêng. Trước khi chọn, mở trang PyPI, xem ngày phát hành gần nhất và phạm vi Python hỗ trợ. Khi biết cần tìm gì, khác biệt giữa ba dự án rõ hơn nhiều so với vẻ ngoài của một bảng chung: backtrader từng có khoảng ngừng phát hành nhiều năm, bản gần nhất có từ tháng 4 năm 2023 — bên cạnh đó có các bản fork cộng đồng với mức duy trì khác nhau; vectorbt có song song dự án mở và bản thương mại phát triển riêng (bản mở phát hành tháng 7 năm 2026), nên trước hết phải xác định tài liệu đang nói về bản nào; backtesting.py từng ngừng phát hành nhiều năm, nhưng đã kết thúc quãng ngừng và ra bản mới tháng 7 năm 2026 — thì quá khứ ở đây quan trọng. Cả ba ngày lấy từ kho đăng ký gói [13]. Không điều nào tự động loại thư viện — điều loại nó là không tương thích với phiên bản bạn dùng. [CẬP NHẬT: 2026-08-22]

Dòng cuối không phải thủ tục. Mỗi framework giả định một mức giá và thời điểm khớp — chính giả định đó, không phải logic chiến lược, thường quyết định kết quả. Trước khi tin thư viện, tìm trong tài liệu ba câu trả lời: khớp điểm vào ở giá nào, tính chi phí ra sao, xử lý khoảng nhảy giá giữa các nến thế nào.

Hai đường tổng hợp có ranh giới trong mẫu và ngoài mẫu rõ ràng
Tách chọn biến thể khỏi đánh giá trên dữ liệu sau; công bố mọi lần thử.

Tự do của mã đồng thời là rủi ro. Bộ kiểm thử tích hợp áp đặt một số quy tắc nên bảo vệ bạn khỏi một số lỗi. Mã tự viết không bảo vệ gì: nó không hỏi đã tách dữ liệu huấn luyện và xác thực chưa, không nhận ra tín hiệu dùng giá chưa tồn tại lúc đó, không đếm số biến thể đã thử. Toàn bộ trách nhiệm chuyển sang tác giả — vì thế các phần sau xét những thứ có thể hỏng âm thầm trong mã.

7. API nhà môi giới: xác thực, giới hạn, lỗi, môi trường thử nghiệm

Câu “nhà môi giới cung cấp API” đôi khi bị hiểu thành “tôi có quyền truy cập tài khoản không giới hạn bằng mã”. Thực tế khác: API là tập điểm cuối có xác thực, quyền hạn, giới hạn tần suất và tập mã lỗi xác định. Đôi khi, nhưng không phải luôn, nó tách môi trường thử nghiệm và sản xuất; cách tách phải đọc tài liệu, không được mặc định. Trước dòng mã đầu tiên, cần trả lời chín câu hỏi: xác thực ra sao, token có quyền gì, được gửi bao nhiêu yêu cầu mỗi giây, phân biệt thử nghiệm và sản xuất thế nào, trả về loại dữ liệu nào, định nghĩa mã lỗi nào, có hạn chế vùng không, chính sách duy trì kết nối ra sao, và token có quyền đặt lệnh hay không.

Kiến trúc MT5 IPC, TWS hoặc IB Gateway và REST API trực tiếp
Thư viện không loại bỏ nền tảng hay ứng dụng trung gian bắt buộc.

Giới hạn cụ thể và được công bố — nhưng phải đọc đó là giới hạn nào. Phân biệt thường mất trong hướng dẫn này quyết định cách thiết kế vòng lặp. Với OANDA v20, hai con số nằm ở hai tài liệu và có nghĩa khác nhau. Hướng dẫn nhà phát triển nêu mức được cưỡng chế: “120 yêu cầu mỗi giây. Vượt mức sẽ nhận HTTP 429” cho REST API, cùng giới hạn hai mươi luồng đang hoạt động và hai kết nối mới mỗi giây — đều tính theo địa chỉ IP gửi yêu cầu [15]. Ngược lại, tài liệu thực hành tốt nêu khuyến nghị: nhà cung cấp khuyên không quá một trăm yêu cầu mỗi giây trên kết nối đã thiết lập và hai kết nối mới mỗi giây [5]. [CẬP NHẬT: 2026-08-22]

Vậy một trăm mỗi giây là mức nhà cung cấp khuyên ở dưới, còn một trăm hai mươi là ngưỡng máy chủ trả lỗi 429. Mã giả định “100/s là tối đa” không gây hại, nhưng mã coi đó là giới hạn cứng rồi không xử lý 429 sẽ ngã ngay khi vượt — vì mức cưỡng chế nằm khác mức khuyến nghị. Đây là số của một nhà cung cấp, không phải quy luật thị trường; tại nhà môi giới khác chúng sẽ khác và [TÍNH NĂNG TÙY NHÀ MÔI GIỚI/TÀI KHOẢN]. Hãy kiểm tra tài liệu nhà cung cấp của mình trước khi viết vòng lặp hỏi dữ liệu.

Điều chỉnh so với hướng dẫn phổ biến — và bản trước của đoạn này. Nhiều tài liệu truyền con số “khoảng 30 yêu cầu mỗi giây” cho OANDA. Nó đã lỗi thời. Tuy nhiên, lỗi phổ biến và khó thấy hơn là dẫn mức khuyến nghị 100 yêu cầu mỗi giây như mức tối đa. Bài này từng làm vậy trước khi sửa. Khuyến nghị và giới hạn cưỡng chế là hai đại lượng khác nhau từ hai tài liệu của cùng nhà cung cấp [5] [15]; thiết kế mã khi nhầm giới hạn sẽ lãng phí năng lực hoặc bị chặn mà chưa có xử lý. Xác minh giới hạn trong tài liệu gốc, không trong hướng dẫn — và xem con số là lời khuyên hay ngưỡng cắt.

Môi trường thử nghiệm là máy chủ khác, không phải chế độ khác. Ở OANDA, sự tách biệt hiện ngay trong địa chỉ: api-fxpractice.oanda.com cho môi trường thực hành và api-fxtrade.oanda.com cho sản xuất, với cặp địa chỉ tương tự cho luồng dữ liệu [15] [CẬP NHẬT: 2026-08-22]. Đây là điều tốt vì có thể chặn nhầm lẫn trong mã: thêm điều kiện bảo vệ máy chủ sản xuất. Khó hơn khi nhà cung cấp dùng chung địa chỉ, chỉ phân biệt bằng token — khi ấy cấu hình là rào chắn duy nhất.

Bốn đường kết nối Python với tài khoản thường gặp — khác kiến trúc, không phải khác mức “cao cấp”:

Giải phápKết nối thực sự đi qua đâuĐiều kiệnGhi chú
OANDA v20 (REST + luồng dữ liệu)Mã → máy chủ nhà môi giới, trực tiếp qua HTTPTài khoản tại nhà môi giới đó; máy chủ thử nghiệm và sản xuất riêngTài liệu công khai, giới hạn nêu rõ. Đây là API của một nhà cung cấp, không phải chuẩn ngành
Interactive Brokers (TWS / IB Gateway)Mã → ứng dụng trung gian chạy cục bộ → máy chủ nhà môi giớiCần TWS hoặc IB Gateway đang chạy; mô hình bất đồng bộĐóng ứng dụng trung gian sẽ ngắt kết nối mã. Thư viện bọc giúp xử lý dễ hơn nhưng không xóa phụ thuộc đó. Chú ý tên: thư viện hay được dẫn ib_insync không có bản phát hành mới từ tháng 7 năm 2023 và dựa trên TWS API cũ; hướng kế tục đang được duy trì tích cực là ib_async [13] [CẬP NHẬT: 2026-10-08]
Gói MetaTrader5 chính thứcMã → phần mềm MT5 cài trên cùng máy → máy chủ nhà môi giớiPhải cài phần mềm MT5 cùng máy; lần gọi initialize() sẽ khởi động nếu chưa chạy, dùng thông tin đăng nhập lưu trong phần mềm hoặc truyền vào lời gọi [18]; tích hợp có từ bản dựng 2085 (2019) [7]Đây là giao tiếp với phần mềm nền tảng, không phải API nhà môi giới độc lập [6]. Phụ thuộc phần mềm là đặc tính, không phải lỗi. Gói PyPI chính thức chỉ cài trên Windows — macOS và Linux cần chạy cả nền tảng lẫn Python qua Wine, hoặc lớp trung gian riêng [TÍNH NĂNG TÙY PHIÊN BẢN/MÔI TRƯỜNG]
Cầu nối đám mây bên ngoài (ví dụ MetaApi)Mã → dịch vụ ngoài → phần mềm nền tảng trong đám mây nhà cung cấp → máy chủ nhà môi giớiCần giao quyền truy cập tài khoản cho bên thứ ba; giá và giới hạn tùy góiThêm mắt xích và điểm phải tin cậy. Cầu nối cần đặt lệnh phải có mật khẩu chính, không phải mật khẩu nhà đầu tư — tức quyền điều khiển tài khoản, không chỉ đọc. Kiểm tra giá với nhà cung cấp [TÍNH NĂNG TÙY GÓI]
Mật khẩu nhà đầu tư và mật khẩu chính là hai quyết định khác nhau. Tài khoản tại nhà môi giới MetaTrader có hai mật khẩu. Mật khẩu nhà đầu tư cho quyền chỉ đọc: xem vị thế và lịch sử, không được đặt hay sửa lệnh. Mật khẩu chính cho toàn quyền tài khoản. Với dịch vụ ngoài — nhật ký, xác minh kết quả, bảng thống kê — tối đa chỉ cấp mật khẩu nhà đầu tư. Cầu nối đám mây cần gửi lệnh theo định nghĩa không thể hoạt động bằng mật khẩu nhà đầu tư: nó cần mật khẩu chính, nên kết nối là quyết định khác về bản chất với kết nối nhật ký. Đọc tài liệu hiện hành của nhà cung cấp cụ thể để biết thực sự yêu cầu quyền gì. Nếu đã giao mật khẩu chính mà không muốn giữ quyền ấy: đổi trong phần mềm hoặc bảng quản lý nhà môi giới để vô hiệu quyền bên thứ ba, rà lịch sử lệnh tìm thao tác lạ, bật xác thực bổ sung ở nơi được hỗ trợ. Ngay cả quyền chỉ đọc cũng bộc lộ phong cách, giờ làm và quy mô vị thế — đó là quyết định về riêng tư, không phải thủ tục.

Ngoài thị trường tiền tệ cũng nên biết ccxt — thư viện thống nhất truy cập các sàn tiền mã hóa. Nó không dùng cho tài khoản FX/CFD ở nhà môi giới cá nhân nhưng đôi khi bị nhầm là giải pháp phổ dụng.

cTrader. Nếu dùng cTrader, thuật toán được viết trong môi trường cTrader Algo, hỗ trợ sẵn cả C# và Python — gồm cBot, chỉ báo và plugin [8] [CẬP NHẬT: 2026-08-22]. Đây là mô hình khác các cách trên: mã chạy bên trong nền tảng, không phải tiến trình riêng kết nối API. Kiểm tra phạm vi hàm và cách khởi chạy trong tài liệu hiện hành vì sản phẩm đang phát triển tích cực.

Khóa và mật khẩu: không bao giờ đặt trong mã

Khóa API gõ vào mã sẽ theo nó vào kho, bản sao lưu và lịch sử phiên bản — không biến mất khỏi lịch sử Git khi chỉ xóa dòng. Kho công khai bị quét tự động để tìm chuỗi như vậy. Bí mật chỉ nên ở biến môi trường, tệp cấu hình ngoài kho được liệt kê trong .gitignore, hoặc trình quản lý bí mật cung cấp giá trị qua một trong các cách đó.

"""Cấu hình và bí mật: không đặt thông tin nhạy cảm trong tệp mã."""
import os
from dataclasses import dataclass

HOSTS = {
    "practice": "https://api-fxpractice.oanda.com",
    "live": "https://api-fxtrade.oanda.com",
}


class MissingConfiguration(RuntimeError):
    """Phát sinh khi thiếu dữ liệu bắt buộc để được khởi chạy."""


@dataclass(frozen=True)
class Configuration:
    token: str
    account: str
    environment: str

    @property
    def host(self) -> str:
        return HOSTS[self.environment]


def _required(name: str) -> str:
    value = os.environ.get(name, "").strip()
    if not value:
        # Cố ý không có giá trị mặc định: token trống sẽ khiến yêu cầu được gửi
        # mà không xác thực thay vì dừng chương trình.
        raise MissingConfiguration(f"Thiếu biến môi trường {name}")
    return value


def load_config() -> Configuration:
    environment = os.environ.get("BROKER_ENV", "practice").strip().lower()
    if environment not in HOSTS:
        raise MissingConfiguration(f"BROKER_ENV phải thuộc {sorted(HOSTS)}")
    return Configuration(
        token=_required("BROKER_API_TOKEN"),
        account=_required("BROKER_ACCOUNT_ID"),
        environment=environment,
    )

Ba quyết định trong đoạn này là có chủ ý. Môi trường mặc định là thực hành, không phải sản xuất — lỗi cấu hình khi ấy dẫn tới demo thay vì tài khoản thật. Thiếu biến môi trường sẽ dừng chương trình thay vì thay bằng chuỗi rỗng. Token không có giá trị mặc định ở đâu, kể cả trong chú thích, vì bí mật trong chú thích vẫn là bí mật đã lộ.

Thư viện python-dotenv đã nêu không thay mã này — nó chạy trước. Gọi load_dotenv() (sau from dotenv import load_dotenv) sẽ tải tệp .env ngoài kho vào biến môi trường của tiến trình; sau đó load_config() mới đọc. Nhờ vậy hàm cấu hình không biết và không cần biết giá trị đến từ tệp, thiết lập hệ thống hay trình quản lý bí mật. Chính tệp .env được đưa vào .gitignore cùng toàn bộ thư mục cấu hình.

Lấy dữ liệu: thời hạn chờ, thử lại, mã phản hồi

Yêu cầu không giới hạn thời gian có thể treo vô hạn và đóng băng cả tiến trình. Thử lại vô hạn có thể cạn hạn mức và bị ngắt. Đoạn dưới là bộ bảo vệ tối thiểu khi đọc dữ liệu — kèm phân biệt quan trọng để bảo vệ tài khoản về sau: chỉ thử lại thao tác lũy đẳng, tức lặp lại không làm đổi trạng thái phía nhà môi giới.

import logging
import time

import requests

TIMEOUT_S = 10.0        # số giây chờ không hoạt động khi kết nối/đọc, không phải hạn toàn tác vụ
MAX_ATTEMPTS = 4            # số lần thử tính cả lần đầu
BACKOFF_S = 1.0         # cơ số thời gian chờ tăng theo hàm mũ, tính bằng giây

log = logging.getLogger("data")


class ApiError(RuntimeError):
    """Lỗi không thể sửa bằng cách thử lại."""


def fetch_candles(session: requests.Session, host: str, token: str,
                   instrument: str, parameters: dict) -> dict:
    """Chỉ thử lại GET - yêu cầu không thay đổi gì phía nhà môi giới."""
    url = f"{host}/v3/instruments/{instrument}/candles"
    headers = {"Authorization": f"Bearer {token}"}

    for attempt in range(1, MAX_ATTEMPTS + 1):
        delay = BACKOFF_S * 2 ** (attempt - 1)
        try:
            response = session.get(url, headers=headers, params=parameters, timeout=TIMEOUT_S)
        except requests.RequestException as error:
            if attempt == MAX_ATTEMPTS:
                raise ApiError(f"Không kết nối được sau {MAX_ATTEMPTS} lần thử") from error
            log.warning("lần thử %s: %s", attempt, error)
            time.sleep(delay)
            continue

        if response.status_code == 429:
            # Vượt giới hạn tần suất. Header Retry-After, nếu nhà môi giới trả về,
            # đáng tin cậy hơn tự phỏng đoán.
            try:
                wait = float(response.headers.get("Retry-After", delay))
            except (TypeError, ValueError):
                # Đặc tả cũng cho phép ngày HTTP thay vì số giây;
                # thiếu nhánh này, float() sẽ làm hỏng toàn bộ việc tải dữ liệu.
                wait = delay
            log.warning("giới hạn tần suất, chờ %.1f s", wait)
            time.sleep(wait)
            continue

        if 500 <= response.status_code < 600:
            log.warning("lỗi máy chủ %s, thử lại", response.status_code)
            time.sleep(delay)
            continue

        if response.status_code != 200:
            raise ApiError(f"HTTP {response.status_code}: {response.text[:200]}")

        try:
            data = response.json()
        except ValueError as error:
            raise ApiError("Phản hồi JSON không hợp lệ") from error
        if not isinstance(data, dict):
            raise ApiError("Phản hồi JSON phải là đối tượng")
        if not isinstance(data.get("candles"), list) or not data["candles"]:
            raise ApiError("Phản hồi không có nến - kiểm tra công cụ và khoảng ngày")
        return data

    raise ApiError(f"Không có phản hồi hợp lệ sau {MAX_ATTEMPTS} lần thử")

Tình trạng xác minh: kiểm thử ngoại tuyến, không kết nối nhà môi giới. Hàm fetch_candles() được công bố đã chạy với mô phỏng phiên HTTP trong bộ nhớ, thay vì máy chủ thật. Chín kịch bản bao gồm 429 với Retry-After tính bằng giây, tiếp đến là 503 rồi thành công trong cùng một kịch bản; danh sách nến rỗng, 404, tiêu đề ngày HTTP, hết lần thử, ngoại lệ kết nối, JSON không hợp lệ, phản hồi không phải đối tượng và kiểu candles không hợp lệ. Các lần gọi sleep() được thay bằng bản ghi độ trễ yêu cầu; kiểm thử không mở socket và không đo thời gian mạng. Môi trường: Python 3.12.10, requests 2.34.2, Windows 11, kiểm tra 2026-10-08. Báo cáo kịch bản thực tế:

A: 429 giây -> 503 -> 200: số lần gọi=3; chờ=[3.0, 2.0]; một nến
B: nến rỗng: số lần gọi=1; chờ=[]; lỗi dự kiến
C: HTTP 404: số lần gọi=1; chờ=[]; lỗi dự kiến
D: phương án dự phòng ngày HTTP: số lần gọi=2; chờ=[1.0]; một nến
E: giới hạn thử lại: số lần gọi=4; chờ=[0.0, 0.0, 0.0, 0.0]; lỗi dự kiến
F: lỗi truyền tải: số lần gọi=4; chờ=[1.0, 2.0, 4.0]; lỗi dự kiến
G: JSON không hợp lệ: số lần gọi=1; chờ=[]; lỗi dự kiến
H: JSON không phải đối tượng: số lần gọi=1; chờ=[]; lỗi dự kiến
I: sai kiểu dữ liệu nến: số lần gọi=1; chờ=[]; lỗi dự kiến

Kết quả này xác nhận và không xác nhận điều gì. Nó xác nhận hành vi kỳ vọng trong chín kịch bản chuẩn bị sẵn. A kiểm tra 429 chọn độ trễ ba giây từ máy chủ thay vì một giây trong mã; D kiểm tra phương án dự phòng khi float() không phân tích được ngày HTTP. F kiểm tra có truyền timeout=10, thực hiện bốn lần thử và kết thúc bằng ngoại lệ sau lỗi truyền tải mô phỏng. Nó không xác lập hạn chót cho kết nối thật: timeout của requests không phải hạn tổng cho tác vụ, và việc chờ ở đây được mô phỏng. B, C và G–I kiểm tra từ chối phản hồi rỗng hoặc không hợp lệ. Kiểm thử không xác minh tính khả dụng API, mọi trường dữ liệu hay hành vi mạng tại nhà môi giới cụ thể.

Tuy nhiên, nó không xác nhận gì về chính nhà môi giới. Đường dẫn điểm cuối, tên tham số, cấu trúc phản hồi và việc nhà cung cấp có trả Retry-After hay không là đặc tính của API cụ thể [TÍNH NĂNG TÙY NHÀ MÔI GIỚI/TÀI KHOẢN], cần đọc tài liệu và kiểm thử trên tài khoản thực hành. Mô phỏng chứng minh mã của bạn xử lý đúng, không chứng minh giả định về máy chủ của người khác đúng — đó là toàn bộ giá trị của phép thử.

Hỏi định kỳ so với luồng, tách thời điểm báo giá, yêu cầu và nhận
Nhận thông điệp nhanh không xác lập độ mới của báo giá hay thời điểm thực thi lệnh.

Phải đo độ trễ, không dẫn lại. Các khoảng truyền nhau trong hướng dẫn — “50–200 ms cho API này, 100–500 ms cho API kia” — vô giá trị vì không nêu vị trí máy khách, đường truyền, thời điểm hay cách đo. Thay vì chép số người khác, tách rõ thứ đang đo: thời gian phản hồi điểm cuối dữ liệu, từ gửi lệnh tới xác nhận nhận lệnh, tới xác nhận khớp và tuổi dữ liệu thị trường khi quyết định. Đó là bốn đại lượng khác nhau; chỉ một được gọi nôm na là “độ trễ”. Đo từng đại lượng trong chuỗi của mình, ghi nhật ký và dẫn kèm ngày, nhà môi giới, số lần thử. Ping ICMP không phải bất kỳ giá trị nào trong bốn giá trị đó.

Hỏi định kỳ hay luồng dữ liệu. Khi hỏi REST định kỳ, tuổi dữ liệu tùy khoảng giữa yêu cầu: hỏi mỗi năm giây nghĩa là trường hợp xấu nhất quyết định dựa trên giá năm giây trước; hỏi thường hơn tiêu hao hạn mức. Luồng — WebSocket hoặc HTTP streaming tùy API — đảo cách làm: máy chủ gửi khi có cập nhật. Nó hợp lý khi cần phản ứng trong nến hoặc hỏi định kỳ làm cạn hạn mức. Với chiến lược vào đầu nến H1 tiếp theo, hỏi định kỳ là đủ và dễ duy trì hơn. Luồng lại thêm ba vấn đề. Thứ nhất, khoảng trống sau kết nối lại: mất kết nối ba mươi giây thì socket không tự phát lại ba mươi giây ấy — phải lấy đoạn thiếu qua yêu cầu lịch sử riêng trước khi coi chuỗi là đầy đủ. Thứ hai, phát hiện kết nối chết: socket ngừng gửi trông giống hệt thị trường không có hoạt động, nên cần tín hiệu hiện diện như heartbeat hoặc keep-alive và bộ đếm im lặng riêng để tuyên bố kết nối hỏng rồi tạo lại. Thứ ba, thứ tự và bản ghi trùng: sau phục hồi có thể nhận lại thông điệp đã xử lý. Giống vấn đề lệnh, cách giải quyết là định danh sự kiện và kiểm tra đã ghi nhận chưa.

Giới hạn riêng cho dữ liệu và lệnh. API nghiêm túc tách luồng giá với luồng giao dịch; hạn mức thao tác đổi trạng thái tài khoản đôi khi thấp hơn nhiều so với đọc. Mã cố sửa dừng lỗ hàng chục lần mỗi giây khi thị trường căng có thể bị ngắt đúng đường giao dịch, để lại vị thế mở tạm thời không thể truy cập bằng chương trình. Vì vậy phải tự giới hạn tốc độ sửa, không trông chờ nhà môi giới bỏ qua.

Tự động hóa không đổi điều kiện thực thi. Mô hình khớp lệnh, cách xử lý luồng lệnh và chất lượng dữ liệu của nhà môi giới ảnh hưởng kết quả bất kể người hay mã gửi lệnh; với chiến lược nhạy chi phí vào, chúng có thể quyết định kết quả. Phần chọn nhà môi giới trình bày mô hình thực thi và chất lượng thực thi. Với mã chỉ cần một kết luận: API tăng tốc gửi yêu cầu thực thi, không tạo thanh khoản vốn không có. Điều này rõ nhất khi biến động mạnh. Ngày 15 tháng 1 năm 2015, Ngân hàng Quốc gia Thụy Sĩ bỏ tỷ giá tối thiểu 1,20 franc mỗi euro, đồng thời giảm lãi suất xuống −0,75 phần trăm [17]. Cơ chế khi ấy có tính tổng quát, không cần tin giai thoại: nếu phía đối ứng không có thanh khoản tại mức lệnh bảo vệ, lệnh không khớp giá ghi sẵn mà khớp mức đầu tiên có sẵn — xa bao nhiêu do sổ lệnh quyết định, không do kết nối. Quy mô tác động tại các nhà môi giới cụ thể được trình bày trong phần chọn nhà môi giới; với mã, chỉ cần hiểu tốc độ gửi không tạo ra mức giá mà không ai muốn giao dịch.

8. Từ notebook tới dịch vụ: mã thực thi khác tập lệnh ra sao

Đây là ranh giới thực sự của bài. Notebook tính thống kê nhật ký có thể hỏng giữa chừng mà không sao — chạy lại là được. Mã có quyền tác động tài khoản sau cùng lỗi ấy để lại trạng thái: vị thế mở, lệnh mới sửa một phần, vào trùng. Khác biệt không nằm ở chất lượng mã mà ở hậu quả không thể đảo ngược của một trong hai loại.

Notebook nghiên cứu ≠ chương trình thực thi. Trước khi mã được phép đặt lệnh, phải có đầy đủ: cấu hình tách khỏi mã · bí mật ngoài kho · nhật ký mọi quyết định và phản hồi máy chủ · kiểm thử logic chạy không cần mạng · thử lại có giới hạn và chỉ cho thao tác lũy đẳng · thời hạn chờ trên mọi lời gọi mạng · trạng thái tồn tại qua khởi động lại tiến trình · giám sát với tín hiệu hiện diện · công tắc dừng ngăn điểm vào mới · quy trình triển khai mô tả cách đưa phiên bản mới vào hoạt động. Thiếu dù một yếu tố, bạn có nguyên mẫu, không phải chương trình thực thi — phải coi đúng như vậy dù đường vốn kiểm thử đẹp đến đâu.

Ba mức tự động hóa – khác nhau ở người ra quyết định cuối, không ở công nghệ:

Mức 1: thông báo. Mã kiểm tra điều kiện rồi gửi thông báo. Không truy cập tài khoản; không cần token đặt lệnh. Con người quyết định và thực thi. Chỉ ở mức này, lỗi mã không tốn gì ngoài bực bội.

Mức 2: chuẩn bị và chờ xác nhận. Mã dựng lệnh — công cụ, chiều, quy mô — rồi chờ người vận hành xác nhận rõ ràng, sau đó mới gửi. Mức này bắt lỗi tính toán trước khi thành vị thế và giữ con người trong vòng quyết định. Với người giao dịch cá nhân không định duy trì phần mềm, đây thường là đích đến chứ không phải bước chuyển — nhưng đó là nhận định biên tập, không phải kết quả nghiên cứu.

Mức 3: chạy không cần xác nhận. Mã tự mở và đóng vị thế. Cần mọi yếu tố trong khung trên, thử trên tài khoản thực hành rồi tài khoản có quy mô tối thiểu, và giám sát giai đoạn đầu. Bao nhiêu giao dịch mới đủ để chuyển tiếp là câu hỏi về cỡ mẫu và lực kiểm định, không phải một số tròn; nó thuộc bài về kiểm thử.

Nhánh phân tích ngoại tuyến tách khỏi các cổng và bộ chuyển tiếp thực thi
Xác lập phân tích đúng trước; gửi lệnh cần thiết kế, kiểm thử và xác nhận trạng thái riêng.

Lớp thực thi có các cổng an toàn

Nếu đưa mã đặt lệnh, phải đưa cả thứ ngăn nó. Mô-đun dưới chạy được trọn vẹn và không có lời gọi mạng nào: dựng lệnh, kiểm tra giới hạn rồi dừng ở các cổng. Phần gửi là đoạn riêng — được chủ ý trình bày ngoài dạng mã thực thi.

"""Lớp thực thi: xác thực, chế độ kiểm thử và cổng rõ ràng trước chế độ thực."""
import logging
import os
import sys
import uuid

ALLOWED_INSTRUMENTS = {"EUR_USD"}
MAX_UNITS = 1_000          # đơn vị đồng tiền cơ sở, không phải lot - quy ước tùy API

# Không thể đọc chấp thuận chế độ thực từ tệp này vì không nằm ở đây.
# Chương trình lấy từ biến môi trường và chấp nhận giá trị đúng là
# số tài khoản mà người vận hành chủ động muốn giao dịch - tự gõ trong shell
# trước khi khởi chạy. Phạm vi hàng rào được mô tả trong khung dưới đầu ra kiểm thử.
CONFIRMATION_VARIABLE = "BROKER_LIVE_CONFIRMATION"

# Thiếu cấu hình này sẽ không có mục log.info: mức root mặc định
# là WARNING và không có nơi ghi. Tệp để lại dấu vết sau sự cố, còn
# luồng hiển thị cùng thông tin trực tiếp. Trong chương trình lớn, cấu hình ghi nhật ký
# thuộc điểm vào chương trình, không thuộc mô-đun.
logging.basicConfig(
    level=logging.INFO,
    format="%(asctime)s %(levelname)s %(name)s %(message)s",
    handlers=[logging.FileHandler("execution.log", encoding="utf-8"),
              logging.StreamHandler(sys.stdout)],
)

log = logging.getLogger("execution")


class SafetyGateError(RuntimeError):
    """Chặn lệnh trước khi rời tiến trình."""


def new_order_id() -> str:
    """Gọi MỘT LẦN cho mỗi ý định đặt lệnh và lưu trước khi gửi.

    Mọi lần thử tiếp theo của cùng lệnh phải trả lại cùng giá trị -
    nếu không sẽ không có cơ sở nhận diện trùng lặp.
    """
    return str(uuid.uuid4())


def build_order(instrument: str, units: int, order_id: str) -> dict:
    if instrument not in ALLOWED_INSTRUMENTS:
        raise SafetyGateError(f"Công cụ không thuộc danh sách cho phép: {instrument}")
    # bool là lớp con của int nên isinstance(True, int) là đúng: nếu không loại bỏ rõ ràng,
    # giá trị logic sẽ qua xác thực và lọt vào payload.
    if isinstance(units, bool) or not isinstance(units, int) or units == 0:
        raise SafetyGateError("Quy mô phải là số nguyên khác không")
    if abs(units) > MAX_UNITS:
        raise SafetyGateError(
            f"Quy mô {abs(units)} vượt giới hạn {MAX_UNITS}")
    return {
        "order": {
            "type": "MARKET",
            "instrument": instrument,
            "units": str(units),
            # Mã định danh đến từ bên ngoài và không đổi giữa các lần thử.
            # Nếu tạo tại đây, mỗi lần thử lại sẽ mang giá trị khác.
            # Tên trường và các ràng buộc thuộc API cụ thể.
            "clientExtensions": {"id": order_id},
        }
    }


def _operator_confirmation() -> str:
    """Đọc giá trị từ bên ngoài tệp. Mã không biết và không lưu giá trị đó."""
    return os.environ.get(CONFIRMATION_VARIABLE, "").strip()


def submit_order(session, cfg, instrument: str, units: int, order_id: str,
                  test_mode: bool = True) -> dict | None:
    """Mặc định không gửi gì. Máy chủ thực yêu cầu hai quyết định."""
    order = build_order(instrument, units, order_id)

    if test_mode:
        log.info("CHẾ ĐỘ KIỂM THỬ [%s] đã tạo lệnh, chưa gửi gì: %s",
                 cfg.environment, order)
        return None

    if cfg.environment == "live" and _operator_confirmation() != cfg.account:
        raise SafetyGateError(
            "Máy chủ thực yêu cầu người vận hành xác nhận rõ ràng")

    # Các cổng kiểm soát đã qua - mô-đun kết thúc tại đây. Nó không gửi lệnh,
    # vì không có cách chạy trung thực; cấu trúc được mô tả trong phác thảo dưới.
    raise NotImplementedError(
        "Mô-đun không triển khai gửi lệnh - xem phác thảo cấu trúc")

Định danh lệnh được tạo một lần, không phải mỗi lần thử. Muốn nhận ra trùng lặp sau mất kết nối, giá trị phải được tạo khi hình thành ý định đặt lệnh, lưu bền vững trước khi gửi và dùng lại cho mọi lần thử của cùng lệnh ấy. Nếu uuid.uuid4() nằm trong bước dựng lệnh, mỗi lần thử lại sẽ gửi giá trị khác, không còn cơ sở coi yêu cầu thứ hai là lặp yêu cầu đầu. Vì thế build_order() nhận định danh qua đối số, không tự tạo.

Định danh ấy không giải quyết điều gì. Trường clientExtensions trong dữ liệu gửi trên là giải pháp của một nhà cung cấp, không phải chuẩn ngành, cũng không phải bảo đảm lũy đẳng đã được tài liệu hóa. Tài liệu OANDA mô tả đó là dữ liệu tùy chỉnh không bắt buộc gắn với lệnh và cảnh báo riêng: “Không đặt, sửa hay xóa tradeClientExtensions nếu tài khoản liên kết MT4” [14] (kiểm tra 2026-08-22). Không có lời hứa lệnh thị trường lặp cùng định danh sẽ bị từ chối là trùng — lệnh thị trường khớp ngay nên cửa sổ nhận ra trùng rất nhỏ. Nhận biết trùng do đó dựa vào hai điều phía bạn: sổ ý định bền vững ghi trước khi gửi, và đối soát với danh sách lệnh, giao dịch lấy từ nhà môi giới sau sự cố. Kiểm tra tên trường và ràng buộc trong tài liệu API của mình — nhà cung cấp khác đặt tên khác hoặc không có trường đó.

Gửi lệnh: phác thảo cấu trúc

Dưới đây không có hàm để sao chép. Đây là phác thảo cấu trúc: thứ tự bước và tên thao tác đủ để chuyển vào mã riêng, nhưng không thể dán và chạy. Hình thức này là lựa chọn có chủ ý. Ban biên tập không chạy nhánh này và không định chạy: gửi lệnh cần tài khoản tại nhà môi giới; kiểm tra mã giao dịch trên tài khoản người khác không phải thử nghiệm mà là rủi ro của họ. Đưa hàm hoàn chỉnh sẽ tạo hiện vật chưa ai chạy, chủ yếu dạy người đọc tin mã chưa xác minh. Chúng tôi chọn tương tự cho phác thảo MQL trong bài 11.5 và phác thảo Pine Script trong bài 11.2.

PHÁC THẢO CẤU TRÚC - gửi lệnh và xử lý khi không nhận phản hồi
Ký hiệu định hướng cho lớp HTTP. Đây không phải mã hoàn chỉnh có thể chạy.

khai báo: StanNieznany - phản hồi chưa đến nên chưa biết
          lệnh có tồn tại không. Đây là kiểu riêng, không phải lớp con của lỗi mạng:
          lỗi mạng có thể thử lại, trạng thái này tuyệt đối không được thử lại.

đầu vào: lệnh (đã tạo và xác thực), order_id, cấu hình, phiên HTTP
đầu ra:  phản hồi nhà môi giới hoặc StanNieznany - không bao giờ im lặng báo thành công

1. SỔ GHI BỀN VỮNG - trước mọi việc khác
   ghi bên ngoài bộ nhớ tiến trình: order_id, dấu thời gian UTC,
   công cụ, units, môi trường, status := INTENT
   buộc ghi xuống đĩa trước khi tiếp tục
   thiếu bước này, khởi động lại tiến trình sẽ mất thông tin về ý định,
   rồi chương trình không biết mình đã thử gì trước đó

2. NHẬT KÝ - cũng trước khi gửi
   ghi nhật ký: order_id + môi trường + nội dung lệnh
   không bao giờ đưa token vào nhật ký
   che số tài khoản, chỉ để bốn ký tự cuối
   con người đọc nhật ký sau sự kiện; chương trình đọc sổ ghi ở bước 1

3. MỘT YÊU CẦU, KHÔNG THỬ LẠI
   POST đến đường dẫn đặt lệnh theo tài liệu API của bạn
   header xác thực chứa token lấy từ cấu hình, tuyệt đối không lấy từ mã
   bắt buộc có timeout - thiếu nó, lời gọi có thể treo mãi

4. KHÔNG NHẬN ĐƯỢC PHẢN HỒI (timeout hoặc lỗi truyền tải)
   KHÔNG thử lại - POST không có tính lũy đẳng
   trạng thái trong sổ ghi := UNKNOWN
   phát sinh StanNieznany
   ĐỐI SOÁT TRẠNG THÁI: hỏi nhà môi giới về lệnh và giao dịch khớp
   trong vài phút gần nhất, tìm order_id trong đó; chỉ sau khi xác định
   lệnh có tồn tại không mới đưa ra quyết định mới
   (phác thảo không có quy trình này - xem đoạn bên dưới)

5. ĐÃ NHẬN PHẢN HỒI
   ghi nhật ký: mã HTTP + phần nội dung phản hồi đã rút gọn
   nếu mã ngoài (200, 201) -> status := REJECTED -> phát sinh lỗi
   nếu nội dung không phải JSON -> status := UNKNOWN -> phát sinh StanNieznany
   nếu không                   -> status := ACCEPTED -> trả về phản hồi

những gì phác thảo này không có và không thể có:
   không có hàm làm sẵn có thể dán vào và chạy với tài khoản,
   cũng không nêu tên trường mức bảo vệ, vì điều đó đến từ tài liệu
   API cụ thể chứ không phải ví dụ của người khác.

Phác thảo này không làm gì – điều đó quan trọng ngang phần nó mô tả. Nó không thêm mức bảo vệ: cả dừng lỗ lẫn chốt lời đều thiếu. Đây không phải bỏ sót — quy ước biểu diễn khác giữa API: trường riêng, khoảng cách hay giá tuyệt đối, thời hạn hiệu lực riêng. Đoạn đó phải lấy từ tài liệu nhà môi giới của bạn, không từ ví dụ người khác. Hệ quả thực tế liên quan chính bước 4: sau StanNieznany, bạn có thể có vị thế mở không được bảo vệ mà còn không chắc nó tồn tại. Trong mã được phép gửi lệnh, mức bảo vệ phải đi cùng yêu cầu vào lệnh — nếu không sẽ có khoảng thời gian vị thế sống mà chưa có dừng lỗ.

Không chỉ thiếu mức bảo vệ. Từ danh sách “Notebook nghiên cứu ≠ chương trình thực thi”, cả mô-đun chạy được ở trên và phác thảo này đều chưa có giám sát, công tắc dừng hay quy trình triển khai; trạng thái tồn tại qua khởi động lại chỉ xuất hiện ở bước 1 của phác thảo dưới dạng yêu cầu, chưa được triển khai. Phần nhật ký, trạng thái và kiểm thử bàn về các yếu tố ấy — chỉ bàn, không thể hiện bằng mã. Kết luận vẫn như trong khung: đây là tài liệu học kiến trúc, không phải chương trình thực thi.

Các cổng an toàn đã được kiểm tra ngoại tuyến – không mạng, không nhà môi giới, không bí mật, dùng cấu hình giả lập. Dưới đây là toàn bộ kiểm thử, không chỉ đầu ra; thiếu nó, bạn phải tin lời chúng tôi — chính kiểu tin tưởng mà bài này cảnh báo.

"""Kiểm thử cổng kiểm soát không dùng mạng: cấu hình giả lập, không bí mật, không POST."""
from dataclasses import dataclass


@dataclass(frozen=True)
class MockConfiguration:
    environment: str = "practice"
    host: str = "https://api-fxpractice.oanda.com"
    token: str = "dummy-token-for-offline-test"
    account: str = "000-000-0000000-000"


def test_gates() -> None:
    cfg = MockConfiguration()
    order_id = new_order_id()
    print("cấu hình:", cfg.environment, cfg.host, "token_length=", len(cfg.token))

    # Cố ý đặt session=None: nếu mã vẫn cố gửi POST,
    # kiểm thử sẽ phát sinh AttributeError thay vì im lặng vượt qua.
    result = submit_order(None, cfg, "EUR_USD", 100, order_id)
    assert result is None
    print("1. chế độ kiểm thử: chưa gửi gì - OK")

    for description, instrument, units in [
        ("công cụ không thuộc danh sách cho phép", "GBP_JPY", 100),
        ("quy mô vượt giới hạn", "EUR_USD", 5000),
        ("quy mô bằng không", "EUR_USD", 0),
        ("giá trị logic thay vì số", "EUR_USD", True),
    ]:
        try:
            build_order(instrument, units, order_id)
        except SafetyGateError as error:
            print(f"2. cổng [{description}]:", error)
        else:
            raise AssertionError(f"Cổng kiểm soát dự kiến không được kích hoạt: {description}")

    cfg_live = MockConfiguration(environment="live", host="https://api-fxtrade.oanda.com")
    try:
        submit_order(None, cfg_live, "EUR_USD", 100, order_id, test_mode=False)
    except SafetyGateError as error:
        print("3. cổng [chế độ thực chưa xác nhận]:", error)
    else:
        raise AssertionError("Chế độ thực vượt qua mà không xác nhận")

    print("mọi cổng kiểm soát đã vượt qua")


if __name__ == "__main__":
    test_gates()

Đầu ra lần chạy — Python 3.12.10 như phần 2, không mạng và không cần gói ngoài thư viện chuẩn: bỏ phần gửi đi thì mô-đun thực thi không còn cần requests. Dòng thứ hai là mục nhật ký: cùng nội dung đồng thời đi vào tệp execution.log và đầu ra chuẩn. Dấu thời gian và định danh lệnh thay đổi mỗi lần chạy — phần còn lại lặp lại chính xác từng ký tự:

cấu hình: practice https://api-fxpractice.oanda.com token_length= 28
1. chế độ kiểm thử: chưa gửi gì - OK
2. cổng [công cụ không thuộc danh sách cho phép]: Công cụ không thuộc danh sách cho phép: GBP_JPY
2. cổng [quy mô vượt giới hạn]: Quy mô 5000 vượt giới hạn 1000
2. cổng [quy mô bằng không]: Quy mô phải là số nguyên khác không
2. cổng [giá trị logic thay vì số]: Quy mô phải là số nguyên khác không
3. cổng [chế độ thực chưa xác nhận]: Máy chủ thực yêu cầu người vận hành xác nhận rõ ràng
mọi cổng kiểm soát đã vượt qua

Đầu ra này xác nhận và không xác nhận điều gì. Năm trường hợp âm kích hoạt đúng cổng: công cụ ngoài danh sách cho phép, quy mô quá lớn, bằng không, giá trị boolean và chế độ thật không xác nhận. Assertion còn kiểm tra chế độ thử; lần kiểm toán cũng kiểm tra biên ±1000, vượt giới hạn và kiểu không hợp lệ. Đối số gọi đầu tiên là None, và chưa triển khai gửi, nên kiểm thử không thể đặt lệnh. Đầu ra không kiểm tra API nhà môi giới, độ bền ghi nhật ký hay che mọi trường nhạy cảm. Token giả không được in: chỉ hiển thị độ dài. Trong dự án riêng, cần kiểm tra riêng việc ghi và phục hồi nhật ký, quy tắc che dữ liệu và lỗi ổ đĩa.

Tuy nhiên, nó không xác nhận hành vi với API thật — cũng không thể, vì mô-đun không có nhánh gửi lệnh. Phác thảo cấu trúc thay nhánh ấy và loại nơi duy nhất có thể gửi gì đó. Đó là khác biệt giữa “chưa chạy” và “không có gì để chạy”: cách đầu để lại hiện vật sẵn dán, cách sau để cấu trúc người đọc phải tự triển khai, đồng thời đọc tài liệu nhà môi giới của mình.

Kiểm thử cũng phải phát hiện thiếu cổng an toàn. Vì thế nó xác nhận kết quả chế độ thử và phát sinh AssertionError sau từng trường hợp âm nếu ngoại lệ kỳ vọng không xuất hiện. Thông báo cuối chỉ xuất hiện sau khi các kiểm tra qua. Đây vẫn là ví dụ nhỏ: không bao phủ định dạng phản hồi nhà môi giới, mọi kiểu dữ liệu hay phục hồi trạng thái. Dự án lớn nên chuyển các ca sang pytest, thêm ca dương và chạy mỗi khi bộ xác thực thay đổi.

Cổng xác nhận ngăn nhầm lẫn, không ngăn chương trình. Cần nói rõ phạm vi vì dễ kỳ vọng quá mức. Giá trị xác nhận không phải hằng trong tệp: chương trình đọc biến môi trường BROKER_LIVE_CONFIRMATION rồi so với số tài khoản, buộc người vận hành chủ động nhập vào shell số tài khoản muốn giao dịch. Nó đủ ngăn ba sai lầm thực tế: chạy cấu hình sản xuất “thử thôi”, nhầm tài khoản, vô tình tắt chế độ thử. Nó không bảo vệ hơn thế. Mã cùng tiến trình có thể tự đặt biến, khi đó cổng không ngăn được — xác nhận mà chương trình đọc từ môi trường của chính nó không phải biện pháp chống chương trình. Giới hạn quyền thực sự nằm ngoài mã: token không có quyền đặt lệnh nếu nhà môi giới hỗ trợ phân quyền, và giới hạn quy mô đặt phía tài khoản [TÍNH NĂNG TÙY NHÀ MÔI GIỚI/TÀI KHOẢN].

Đơn vị không phải lot. Chú thích cạnh MAX_UNITS không phải trang trí. API dùng quy ước quy mô khác nhau — đơn vị đồng cơ sở, lot, hợp đồng — và nhầm ở đây không gây lỗi cú pháp hay ngoại lệ. Nó tạo phơi nhiễm lớn hơn dự định hai hoặc ba bậc độ lớn trong khi mã vẫn chạy đúng. Trước khi gửi gì, kiểm tra tài liệu và xác nhận bằng một giao dịch quy mô tối thiểu trên tài khoản thực hành. Cần biết hai giới hạn của bộ xác thực trước khi đưa vào phép tính riêng. Quy mô tính trong pandas hoặc NumPy có kiểu numpy.int64, không phải lớp con của kiểu tích hợp int — bộ xác thực từ chối với thông báo về số nguyên dù giá trị đúng là số nguyên. Nó thất bại an toàn nhưng khó hiểu, nên chuyển số đọc từ DataFrame qua int(...) trước khi đưa tới build_order(). Giới hạn thứ hai: order_id hoàn toàn chưa được kiểm tra; chú thích kiểu không cưỡng chế gì. Vì nhận biết lệnh trùng dựa vào định danh đó, hãy kiểm tra độ dài, định dạng được phép trong tài liệu API rồi cưỡng chế như giới hạn quy mô.

Nhật ký, trạng thái và kiểm thử

Ghi nhật ký thay cho print(). Mô-đun logging có cấp độ nghiêm trọng, dấu thời gian và ghi tệp — nhưng không thứ nào tự bật. Chưa cấu hình thì mức mặc định là WARNING và chưa có đích ghi, nên các lần gọi log.info(...) bị bỏ âm thầm: mã trông như ghi nhật ký nhưng không ghi gì. Đó là nguyên nhân lặp lại khiến nhật ký trống sau sự cố. Chỉ cấu hình một lần tại điểm vào chương trình — trong tập lệnh được chạy, không trong mô-đun nhập vào. Lần gọi logging.basicConfig(...) nằm trong mô-đun thực thi trên chỉ vì ở đây mô-đun cũng là toàn bộ chương trình; chú thích trong mã nói rõ dự án lớn không nên đặt ở đó. Lý do cụ thể: cấu hình trong mô-đun chạy lúc import, chiếm cấu hình ghi log cho cả tiến trình; lần gọi logging.basicConfig(...) sau đó ở điểm vào thật sẽ không làm gì — hàm không có tác dụng khi bộ ghi gốc đã có đích, trừ khi gọi với force=True) và không báo lỗi [4]. Thiết lập của bạn biến mất; mục ghi đi tới nơi mô-đun nhập đã chỉ định: cùng lỗi âm thầm, chỉ từ phía khác. Ghi quyết định và căn cứ, nội dung yêu cầu, phản hồi máy chủ — cắt ngắn phản hồi để trang lỗi HTML không làm ngập nhật ký, tăng giới hạn khi chẩn đoán. Token là ngoại lệ: không bao giờ ghi. Thiếu phản hồi nhà môi giới, sau này không thể biết vấn đề nằm ở logic, mạng hay máy chủ.

Nhật ký thực thi cũng là dữ liệu nhạy cảm. Toàn bộ nội dung lệnh đi vào execution.log, và trong chương trình đầy đủ còn có phản hồi nhà môi giới: số tài khoản, định danh lệnh, quy mô, giá. Token không ở đó và không nên ở đó — mười lần chạy kiểm thử tạo mười một mục, không lần xuất hiện token — nhưng phần còn lại đủ để bảo vệ tệp như cấu hình chứa bí mật: đưa execution.log vào .gitignore cạnh .env. Bản thân .gitignore không phải kiểm soát truy cập: không giới hạn quyền tệp, không xóa sau một thời gian và không che nội dung. Vì thế phác thảo gửi yêu cầu che số tài khoản, chỉ giữ bốn ký tự cuối, và ghi các trường từ danh sách tự xác định, không ghi mọi thứ máy chủ trả. Còn hai điểm phải sửa trước khi chạy không giám sát. Đường dẫn: execution.log là tương đối nên tệp được tạo trong thư mục làm việc của tiến trình, không phải cạnh mã. Khi chạy từ Task Scheduler hoặc cron — đúng các cách chạy được bàn bên dưới — thư mục làm việc có thể khác hẳn thư mục mã; nhật ký làm nền cho phục hồi sự cố sẽ nằm nơi khác chỗ bạn tìm. Hãy dùng đường dẫn tuyệt đối, tính từ vị trí mã hoặc cấu hình. Về kích thước: theo khuyến nghị đoạn trên, tệp tăng vô hạn vì ví dụ chưa có luân chuyển nhật ký. logging.handlers.RotatingFileHandler có trong cùng thư viện chuẩn và — sau khi thêm import logging.handlers, vì chỉ import logging không cung cấp mô-đun con ấy — thay cho FileHandler mà không cần đổi cấu hình còn lại.

Trạng thái phải tồn tại qua khởi động lại. Sớm muộn tiến trình sẽ dừng — do cập nhật hệ thống, mất điện hoặc chính bạn nhấn Ctrl+C. Nếu thông tin lệnh đã gửi chỉ ở bộ nhớ, sau khởi động lại chương trình không biết mình đã làm gì. Tối thiểu phải ghi định danh lệnh vào tệp hoặc cơ sở dữ liệu trước khi gửi, rồi khi khởi động đối chiếu với trạng thái tài khoản lấy từ API. Chính quy trình ấy ngăn vào trùng sau mất kết nối.

Kiểm thử không cần mạng. Logic tín hiệu và xác thực giới hạn kiểm tra được trên dữ liệu chuẩn bị sẵn, không kết nối gì — như phần cổng an toàn ở trên. Kiểm thử cần API hoạt động không phải kiểm thử đơn vị và không thể chạy mỗi khi sửa mã. Bắt đầu từ thứ kiểm tra ngoại tuyến được: điều kiện vào, tính quy mô, hành vi với dữ liệu rỗng và có khoảng trống.

Dừng có kiểm soát. Chương trình thực thi nên phản ứng với tín hiệu kết thúc: hoàn tất thao tác hiện tại, lưu trạng thái rồi thoát, thay vì bị cắt giữa lúc gửi lệnh. Riêng biệt, nên có điều kiện ngăn điểm vào mới khi vượt ngưỡng lỗ ngày. Đặt ngưỡng ở đâu và tính thế nào nằm trong phần quản trị rủi ro của khóa học; trong mã đó là điều kiện thường kiểm tra trước từng lần vào, không phải hàm thư viện.

Chạy ở đâu. Máy chủ hoạt động liên tục, đồng bộ đồng hồ, tiếp tục sau khởi động lại và quy trình phục hồi sự cố là chủ đề của bài Tự động hóa giao dịch: EA, tập lệnh và VPS. Có nhiều cách khởi chạy, khác nhau ở điều xảy ra sau sự cố, không phải độ tiện: Task Scheduler trên Windows hoặc cron trên Unix kích hoạt định kỳ rồi kết thúc sau khi chạy; dịch vụ hệ thống, ví dụ một đơn vị systemd, giữ tiến trình chạy liên tục và có thể khởi động lại sau khi thoát; trình quản lý tiến trình làm tương tự ở lớp ứng dụng. Chọn lần chạy ngắn định kỳ hay tiến trình dài quyết định nơi giữ trạng thái và ai bảo đảm chỉ có một phiên bản chạy. Với mã Python, điều cốt yếu là mã từ bộ lập lịch phải chịu được việc lần trước chưa kết thúc — kiểm tra trạng thái trước hành động, không mặc định bắt đầu từ không.

Xem thêm: Giao dịch thuật toán – kiến trúc hệ thống và vị trí tự động hóa trong toàn bộ quá trình quyết định.

9. Thiên lệch nhìn trước và rò rỉ dữ liệu trong mã Python

Thiên lệch nhìn trước không phải lỗi phương pháp ở đây. Nó là lỗi của một dòng mã — vì thế thuộc bài này, không phải bài phương pháp kiểm thử. Kết quả gán cho một thời điểm dùng thông tin chưa tồn tại tại thời điểm đó.

Dạng phổ biến nhất trong pandas rất đơn giản: cột tín hiệu nhân cột lợi nhuận cùng dòng. Lợi nhuận ở dòng t phát sinh từ lúc đóng nến trước tới lúc đóng nến t. Tín hiệu tính từ giá đóng nến t chỉ được biết khi lợi nhuận ấy đã xảy ra. Nhân chúng với nhau là tự nhận kết quả của chuyển động chưa biết lúc ra quyết định.

Giả định ví dụ dưới: tám quan sát giờ tổng hợp, chỉ số UTC, giá trị minh họa, không chi phí giao dịch — mục tiêu là thể hiện cơ chế dịch, không đánh giá quy tắc. Môi trường như phần 2: Python 3.12.10, pandas 3.0.5.

"""Nhìn trước tương lai trong một dòng: cùng quy tắc, hai kết quả khác nhau."""
import pandas as pd

index = pd.date_range("2026-06-01", periods=8, freq="1h", tz="UTC")
df = pd.DataFrame(
    {"close": [1.0800, 1.0810, 1.0825, 1.0815, 1.0830, 1.0845, 1.0840, 1.0855]},
    index=index,
)
df["sma"] = df["close"].rolling(3).mean()
df["return"] = df["close"].pct_change()

# Quy tắc: vị thế mua khi giá đóng cửa cao hơn trung bình ba nến.
df["position"] = (df["close"] > df["sma"]).astype(int)

# SAI: vị thế của hàng t nhân với lợi suất cùng hàng đó.
# Lợi suất t hình thành giữa giá đóng cửa t-1 và giá đóng cửa t, nên quyết định
# dùng mức giá chưa tồn tại tại thời điểm vào lệnh.
df["result_with_lookahead"] = df["position"] * df["return"]

# ĐÚNG: vị thế quyết định lúc đóng cửa t chỉ hưởng lợi suất t+1.
df["result_without_lookahead"] = df["position"].shift(1) * df["return"]

print(df[["result_with_lookahead", "result_without_lookahead"]].sum().round(6))

Đầu ra lần chạy:

result_with_lookahead       0.005082
result_without_lookahead    0.001384
dtype: float64

Diễn giải. Cùng quy tắc, cùng dữ liệu, khác một lần gọi shift(1) — tổng lợi nhuận đơn từng kỳ giảm từ 0,005082 xuống 0,001384, tức dưới 30 phần trăm giá trị đầu. Tám quan sát đương nhiên chỉ minh họa, không phải phép đo; trên nhiều năm dữ liệu, cùng lỗi có thể biến chiến lược trung bình thành ngoạn mục, và ngoạn mục thành bất khả thi. Cả hai không có đơn vị và chưa tính spread hay hoa hồng — tính vào thì bản đúng còn thấp hơn.

Một lưu ý về phép tính. pct_change() cho lợi nhuận đơn; như phần 4 đã nói, chúng ghép bằng phép nhân, không cộng; chỉ lợi nhuận log mới cộng được. Ở đây cộng là xấp xỉ có chủ ý, vì với mức cỡ phần nghìn, chênh lệch giữa tổng và ghép đúng trong ví dụ nhỏ hơn 0,00001, tức thấp hơn hơn hai bậc độ lớn so với chênh lệch cần thể hiện. Khi tính kết quả tài khoản thay vì cơ chế dịch, hãy nhân dồn (1 + r) hoặc chuyển sang lợi nhuận log rồi mới cộng.

Cẩn thận dịch hai lần. Chỉ dịch ở một nơi: cột vị thế hoặc dữ liệu đầu vào tín hiệu, không đồng thời cả hai. Mã tính tín hiệu từ giá đã dịch rồi lại dịch vị thế sẽ làm trễ hai kỳ và âm thầm hạ kết quả. Lỗi hướng này ít nguy hiểm hơn nhìn trước nhưng vẫn sai.

Bảng giá so sánh cách gán lợi nhuận sai lệch và đã dịch
Dùng giá đóng tương lai ở đây đổi kết quả từ −0,98% thành +2,00% bề ngoài.

Các dạng nhìn trước khác trong mã Python – đều quy về một câu hỏi: giá trị đã biết lúc quyết định chưa?

  • Dùng giá cao hoặc thấp của nến hiện tại để vào, dù chỉ biết chúng sau khi nến đóng
  • Định tỷ lệ hoặc chuẩn hóa trên toàn bộ dữ liệu trước khi chia huấn luyện/xác thực — tham số biến đổi mang thông tin tương lai
  • Chỉ báo làm mượt hay đánh dấu đảo chiều bằng nến sau; giá trị lịch sử khác điều thấy trực tiếp
  • Dữ liệu vĩ mô sau điều chỉnh thay vì giá trị công bố ban đầu
  • Lệnh được khớp giá đóng của nến tạo tín hiệu

Rò rỉ khi chuẩn bị tập dữ liệu. Mục thứ hai đáng có giải thích riêng, vì trong Python rất dễ làm theo phản xạ. Gọi bộ chuẩn hóa trên toàn DataFrame trước khi chia kỳ chỉ một dòng và trông vô hại, nhưng truyền kiến thức về phân bố tương lai vào phần xác thực. Chỉ xác định tham số biến đổi trên dữ liệu trước, rồi áp dụng lên dữ liệu sau — không đảo ngược.

Hai bản mã gán tín hiệu cho lợi nhuận hiện tại hoặc tiếp theo
Dịch tín hiệu sửa lỗi thời điểm cụ thể theo mô hình này; không xác lập khả năng sinh lời.

Kiểm định nhiều lần. Càng thử nhiều biến thể trên cùng dữ liệu, càng có khả năng biến thể tốt nhất là trùng hợp. Với mô hình được nêu rõ cho giả thuyết không (null), nếu 10.000 kiểm định đều có xác suất dương giả 0,05, số kết quả kỳ vọng được gọi “có ý nghĩa” là 0,05 × 10.000 = 500. Kỳ vọng này do cộng xác suất, không cần độc lập; sự phụ thuộc đổi phân bố số lượng, không đổi trung bình trong các giả định đó. Bậc độ lớn cho thấy vì sao phải báo cáo số lần thử cùng kết quả.

Có hiệu chỉnh chính thức tính tới số lần thử — Deflated Sharpe Ratio, do Bailey và López de Prado đề xuất năm 2014 [12]. Trước khi dùng triển khai sẵn, hãy kiểm tra tình trạng và giấy phép thư viện; một số dự án lĩnh vực này đã đổi mô hình phân phối. Công thức gốc được mô tả trong công bố nguồn và có thể tự triển khai.

Phần này không bao gồm gì. Quá khớp, lựa chọn mẫu, phân tích cuốn chiếu, thiên lệch sống sót và kiểm tra độ vững khi chi phí tăng thuộc phương pháp kiểm thử, được mô tả trong bài về backtest và kiểm thử tiến. Ở đây chỉ quan tâm thứ có thể làm hỏng trong mã — và có thể kiểm tra trong mã.

Kiểm tra mã trước khi tin kết quả:

  1. Kết quả dòng t có chỉ dùng dữ liệu có trước lúc quyết định không?
  2. Có chỉ dịch đúng một lần — tín hiệu hoặc dữ liệu, không cả hai không?
  3. Có ghi rõ thời điểm và giá khớp thay vì để ngầm định không?
  4. Mô hình có chi phí giao dịch, hoặc chủ ý bỏ và nêu rõ không?
  5. Dữ liệu có chia huấn luyện và xác thực theo thời gian không?
  6. Các phép biến đổi có chỉ được xác định trên phần trước không?
  7. Có ghi số biến thể đã thử cùng kết quả không?
  8. Chỉ số thời gian có múi giờ rõ ràng, tăng dần và không trùng không?
Quy tắc thực hành: kết quả tốt bất ngờ là giả thuyết về lỗi trước khi là giả thuyết về lợi thế. Trước khi coi mình tìm được điều đặc biệt, rà từng mục và chạy mã trên dữ liệu biết trước đáp án — ví dụ chuỗi giá không đổi, lợi nhuận biến động giá bằng không trước chi phí; giao dịch vẫn có thể lỗ sau chi phí. Kiểm thử với đáp án biết trước bắt được nhiều lỗi hơn nhìn chằm chằm đường vốn.

10. Bạn thực sự cần bao nhiêu mã

Không cần thành lập trình viên. Cần biết đủ để không bị mã mình hoặc tập lệnh người khác tải từ kho đánh lừa.

MứcKỹ năngMang lại điều gìThời gian ước tính
0 – không mãKhông cóPhân tích thứ nền tảng cho phép, theo cách nền tảng áp đặt—
1 – đọc mãHiểu mã người khác, đổi tham số, chạy trong môi trường cô lậpKiểm tra mã từ kho có nhìn trước và có tính chi phí hay khôngVài tuần
2 – tự phân tíchpandas, numpy, matplotlib; mã phân tích riêngPhân tích nhật ký và dữ liệu thị trường không có giới hạn bảng tínhVài tháng
3 – tích hợpAPI, xử lý lỗi, ghi nhật ký, lập lịch, kiểm thửThông báo và chuẩn bị lệnh có xác nhậnSáu tháng trở lên
4 – mã thực thiKiểm thử tự động, quản lý trạng thái, triển khai, giám sátChương trình được phép chạy mà không xác nhận từng quyết địnhTính bằng nhiều năm thực hành

Thời gian cột cuối chỉ ước tính, tùy điểm xuất phát và số giờ thực sự dành mỗi tuần — hãy coi là bậc độ lớn, không phải lời hứa.

Mức 1 và 2 không tạo lợi thế thị trường. Chúng cho khả năng kiểm tra giải pháp người khác có hợp lý không và ngừng mua hệ thống không thể xác minh. Đó là giá trị thật nhưng khác giá trị nhà bán công cụ sẵn quảng bá. Cần mức 4 nếu muốn duy trì chương trình thực thi, không phải để giao dịch tốt hơn. Giáo trình Hilpisch khảo sát ứng dụng Python trong tài chính [11], kèm lưu ý áp dụng mọi sách về mã: phải kiểm tra đoạn mã với thư viện hiện hành trước khi dùng vì một số giao diện đã đổi từ lúc xuất bản.

Mã do mô hình ngôn ngữ tạo. Trợ lý có thể viết phần lớn mã phân tích nhanh hơn bạn. Chúng cũng có thể tạo mã nhìn trước trông đúng hoặc gọi hàm không tồn tại trong phiên bản thư viện bạn có. Không có kỹ năng mức 1, bạn không phát hiện được điều nào. Khả năng và giới hạn mô hình ngôn ngữ được trình bày trong bài riêng: AI và LLM trong giao dịch.

Thứ tự học hợp lý – mỗi bước dựa trên bước trước và kết thúc bằng thứ hoạt động được:

  1. Kiến thức ngôn ngữ cơ bản – biến, danh sách, hàm, điều kiện. Đủ đọc mã người khác mà không phải đoán
  2. pandas trên dữ liệu của bạn – tải bản xuất nhật ký, tính thống kê theo thứ và công cụ. Nếu thích học giáo trình hơn tài liệu, sách McKinney, tác giả pandas, bao phủ đầy đủ giai đoạn này [10]
  3. Một biểu đồ – vẽ đường vốn và sụt giảm từ kết quả bước trước
  4. Kiểm tra chất lượng dữ liệu – lặp ví dụ phần 4 trên tệp mình: múi giờ, trùng, khoảng trống
  5. Tích hợp chỉ đọc – lấy dữ liệu API trên tài khoản thực hành, dùng token không có quyền giao dịch nếu nhà cung cấp phân quyền

Thứ tự có chủ ý. Bước thứ năm chỉ đến sau khi biết kiểm tra dữ liệu tải về — nếu không, bạn đang tự động hóa lỗi thay vì công việc.

Ứng dụng giá trị nhất lại ít ngoạn mục nhất. Phân tích nhật ký mình bằng pandas cho thấy kết quả thật sự đến từ đâu và đâu chỉ là cảm giác. Việc này thường hữu ích hơn xây chương trình thực thi và chỉ cần mức 2 — không phải vì ai đã đo, mà vì đây là phân tích duy nhất dựa trên dữ liệu của bạn thay vì người khác.

Bắt đầu bằng một việc: tải dữ liệu của mình, tính thứ có thể kiểm tra tay trên ba dòng rồi so kết quả. Nếu Python khớp với đếm bằng tay, bạn có nền tảng. Nếu không, bạn vừa tìm lỗi đầu tiên — đó là khởi đầu tốt hơn mã chạy ngay mà bạn không hiểu.

Python không sửa được chiến lược tệ. Tốt nhất, nó cho thấy chiến lược tệ nhanh hơn.

FAQ – Python trong giao dịch

Python có đủ nhanh để tự động hóa giao dịch không?
Tùy nút thắt nằm đâu. Với chiến lược vào đầu nến H1 tiếp theo, thời gian tính Python không đáng kể so với thời gian lệnh qua mạng và máy chủ nhà môi giới. Với giao dịch trong nến một phút, cả chuỗi trở nên quan trọng: tuổi dữ liệu khi quyết định, thời gian phản hồi điểm cuối và thời gian tới xác nhận khớp. Không đại lượng nào được dẫn theo trí nhớ — phải đo trong chuỗi của bạn tại nhà môi giới của bạn, ghi kèm ngày và số lần thử. Python không phù hợp giao dịch độ trễ cực thấp, nhưng đó là lớp vấn đề khác giao dịch cá nhân.
Cần học Python bao lâu để hữu ích trong giao dịch?
Để đọc mã người khác và đổi tham số — vài tuần làm đều đặn. Để tự phân tích nhật ký bằng pandas có biểu đồ — vài tháng. Khoảng này tùy điểm xuất phát và số giờ mỗi tuần, nên coi là bậc độ lớn. Không cần khóa bao trùm cả ngôn ngữ: tải tệp và tính thống kê không cần lớp, decorator hay đồng thời. Học nhanh nhất trên dữ liệu mình vì biết ngay kết quả có hợp lý không.
Có thể kết nối Python với tài khoản MT4 hoặc MT5 không?
Với MT5 có gói chính thức MetaTrader5 cài qua pip — nhưng chỉ trên Windows: macOS và Linux không có bản cài chính thức, cần chạy nền tảng cùng Python dưới Wine hoặc dựng lớp trung gian riêng [TÍNH NĂNG TÙY PHIÊN BẢN/MÔI TRƯỜNG]. Cũng phải hiểu gói làm gì: giao tiếp với phần mềm MetaTrader 5 cài cùng máy, không trực tiếp với máy chủ nhà môi giới. Phần mềm phải được cài, nhưng không cần mở trước: tài liệu nhà cung cấp nêu lần gọi initialize() tự khởi động nền tảng khi cần; tham số đăng nhập không bắt buộc vì nếu thiếu sẽ dùng dữ liệu lưu trong phần mềm [18]. Đây không phải chi tiết nhỏ: mã từ bộ lập lịch có thể mở nền tảng và đăng nhập mà bạn không tham gia, vừa tiện vừa cho thấy việc “phần mềm đang đóng” không phải bảo vệ. Tích hợp xuất hiện trong phiên bản năm 2019 (bản dựng 2085) [7]. Đây không phải REST API độc lập nhà môi giới và không nên mô tả như vậy. Phương án khác là cầu nối đám mây ngoài, thêm mắt xích và buộc tin bên thứ ba với quyền tài khoản. Lối bắt đầu đơn giản, an toàn nhất vẫn một chiều: xuất dữ liệu nền tảng ra tệp, phân tích Python, con người quyết định và thực thi.
Nên giữ khóa API ở đâu và làm gì nếu khóa lộ?
Chỉ trong biến môi trường hoặc tệp cấu hình ngoài kho được liệt kê trong .gitignore. Không trong mã, chú thích hay notebook — ô notebook lưu cả đầu ra, nên token đã in sẽ nằm trong tệp. Nếu khóa vào kho, xóa dòng không có tác dụng: giá trị vẫn ở lịch sử phiên bản. Cách hiệu quả duy nhất là thu hồi khóa phía nhà môi giới và tạo khóa mới, sau đó mới dọn lịch sử. Ngoài ra nên đặt giới hạn quy mô phía tài khoản làm lớp bảo vệ thứ hai độc lập mã.
Mã trong bài đã được kiểm tra chưa?
Có — từng khối mã, và bài nêu rõ theo từng khối. Ví dụ tải CSV phần 4 và so sánh dịch tín hiệu phần 9 đã chạy trong môi trường nêu (Python 3.12.10, pandas 3.0.5), kết quả là đầu ra thật. Khối tải cấu hình được thử cả năm nhánh, từ thiếu biến môi trường tới từ chối tên môi trường lạ. Cổng an toàn lớp thực thi được kiểm tra ngoại tuyến, không mạng hay bí mật. Đoạn lấy dữ liệu API chạy với bộ giả lập phiên HTTP trong bộ nhớ, không khởi chạy máy chủ cục bộ và không mở kết nối mạng, kể cả tới 127.0.0.1 hay tới nhà môi giới. Các kịch bản xử lý phản hồi được ghi nhận đã được kiểm tra, gồm hết số lần thử và ngoại lệ kết nối. Tuy nhiên, bài không có dòng nào gửi lệnh: nhánh gửi được thay bằng phác thảo cấu trúc vì không thử mã giao dịch trên tài khoản người khác, còn hàm hoàn chỉnh thiếu kiểm thử chỉ dạy tin mã chưa xác minh. Chạy mã không đồng nghĩa xác nhận giả định về máy chủ người khác: hãy kiểm tra đường dẫn, cấu trúc phản hồi, giới hạn trong tài liệu nhà môi giới và trên tài khoản thực hành.
Có thể tin chiến lược trong kho công khai không?
Không thể nếu chưa tự kiểm tra; đây không phải cáo buộc tác giả thiếu thiện chí. Thiếu sót thường là dịch tín hiệu chưa xác minh, không mô hình chi phí, không mô tả môi trường và phiên bản, không nêu đã thử bao nhiêu biến thể trước khi công bố bản này. Trước khi chạy: đọc nơi tín hiệu phát sinh và kỳ được gán kết quả, kiểm tra mã có chi phí không, dựng lại môi trường từ tệp phụ thuộc rồi chạy cô lập. Nếu kho không nêu phiên bản thư viện thì kết quả vốn không tái lập — không thể xác minh cũng không thể bác bỏ.

Nguồn và tài liệu tham khảo

  1. Tài liệu pandas 3.0.5 – pandas.pydata.org/docs – cấu trúc dữ liệu, chỉ số thời gian và múi giờ, resample, xử lý giá trị thiếu; phiên bản kiểm tra 2026-08-22
  2. pandas – ghi chú phát hành 2.2.0 và 3.0.0 – pandas.pydata.org/docs/whatsnew – ngừng khuyến nghị tham số method trong fillna() và thay đổi hành vi mặc định
  3. NumPy – tài liệu và ghi chú phát hành – numpy.org/doc – vector hóa, bộ tạo số ngẫu nhiên và hạt giống
  4. Tài liệu Python – docs.python.org/3 – các mô-đun venv, logging và timeit, xử lý ngoại lệ
  5. OANDA v20 – Thực hành tốt – developer.oanda.com/rest-live-v20/best-practices – tần suất yêu cầu được khuyến nghị trên kết nối đã thiết lập (100/s) và kết nối mới (2/s); kiểm tra 2026-08-22
  6. MQL5 – Tích hợp Python – mql5.com/en/docs/python_metatrader5 – gói MetaTrader5 và giao tiếp với phần mềm nền tảng
  7. MetaTrader 5 – ghi chú phát hành bản dựng 2085 – metatrader5.com/en/releasenotes/terminal/2085 – giới thiệu tích hợp Python trong nền tảng
  8. cTrader Algo – tài liệu – help.ctrader.com/ctrader-algo – hỗ trợ sẵn C# và Python cho cBot, chỉ báo và plugin; tình trạng 2026-08-22
  9. TA-Lib (liên kết Python) – pypi.org/project/TA-Lib – gói nhị phân trên PyPI, cài không cần biên dịch thư viện C
  10. McKinney, W. – Python for Data Analysis, O'Reilly, ấn bản thứ 3, 2022 – pandas, numpy, làm việc với chuỗi thời gian
  11. Hilpisch, Y. – Python for Finance, O'Reilly, ấn bản thứ 2, 2018 – ứng dụng Python trong tài chính; kiểm tra mã với thư viện hiện hành
  12. Bailey, D. H., López de Prado, M. – The Deflated Sharpe Ratio, Journal of Portfolio Management, 2014 – hiệu chỉnh tỷ số Sharpe theo số biến thể đã thử
  13. PyPI – trang gói và giao diện JSON của kho đăng ký – pypi.org – ngày và số phiên bản mới nhất của thư viện nêu trong bài (mplfinance, streamlit, openpyxl, pandas-ta, backtrader, vectorbt, backtesting.py, ib_insync, ib_async); kiểm tra 2026-10-08
  14. OANDA v20 – API Lệnh và Giao dịch – developer.oanda.com/rest-live-v20/transaction-df – clientExtensions là dữ liệu tùy chỉnh không bắt buộc gắn với lệnh, cùng cảnh báo không đặt trên tài khoản liên kết MT4; không có bảo đảm lũy đẳng được tài liệu hóa cho lệnh thị trường; kiểm tra 2026-08-22
  15. OANDA v20 – Hướng dẫn phát triển – developer.oanda.com/rest-live-v20/development-guide – giới hạn REST được cưỡng chế 120 yêu cầu mỗi giây với phản hồi HTTP 429, 20 luồng đang hoạt động và 2 kết nối mới mỗi giây, tính theo IP gửi yêu cầu; kiểm tra 2026-08-22
  16. Microsoft – Đặc tả và giới hạn Excel – support.microsoft.com – kích thước trang tính: 1.048.576 dòng và 16.384 cột; kiểm tra 2026-08-22
  17. Ngân hàng Quốc gia Thụy Sĩ – thông cáo ngày 15 tháng 1 năm 2015 – snb.ch – bỏ tỷ giá tối thiểu 1,20 franc mỗi euro và giảm lãi suất xuống −0,75 phần trăm
  18. MQL5 – MetaTrader5.initialize() – mql5.com/en/docs/python_metatrader5/mt5initialize_py – khởi động nền tảng khi kết nối nếu cần, cùng các tham số không bắt buộc path, login, password và server với dữ liệu lưu trong nền tảng; kiểm tra 2026-08-22

Tiến độ học của bạn

Đã học xong bài này? Hãy tự đánh dấu. Mở hoặc cuộn trang không tự động đánh dấu hoàn thành.

Tiến độ cần JavaScript và bộ nhớ trình duyệt.

Chỉ lưu trong trình duyệt này. Không cần tài khoản, không đồng bộ giữa các thiết bị. Xóa dữ liệu trang web có thể làm mất tiến độ.

Jarosław Wasiński

Tổng biên tập, ForexMechanics.com • Chuyên gia phân tích vĩ mô và FX

Chuyên gia phân tích độc lập và chuyên nghiệp với hơn 20 năm kinh nghiệm trong các thị trường tài chính. Hoạt động trong lĩnh vực Forex từ năm 2004, tập trung vào phân tích cơ bản, cấu trúc thị trường OTC và quản lý rủi ro vốn một cách kỷ luật.

  • Người sáng lập và tổng biên tập của MyBank.pl, cung cấp giáo dục tài chính đáng tin cậy từ năm 2004.
  • Tác giả của hàng trăm bài bình luận thị trường chuyên sâu, phân tích cấu trúc và tài liệu giáo dục dành cho trader và nhà đầu tư.
  • Người ủng hộ sự minh bạch của thị trường, thúc đẩy giáo dục dựa trên dữ liệu cụ thể và các báo cáo của tổ chức (BIS, ESMA, CFTC).

Toàn bộ nội dung chỉ mang tính chất giáo dục và cung cấp thông tin – không phải là tư vấn đầu tư hay khuyến nghị bất kỳ. Xin lưu ý: giao dịch có đòn bẩy (Forex/CFD) tiềm ẩn rủi ro cao về mất vốn nhanh chóng.