SFI External API — Algo Trading
Đưa bảng theo dõi 10 engine giao dịch tự động lên app SFI Trade — chỉ đọc, một chiều, và toàn quyền điều khiển nằm ở phía VPS.
https://algo-api.sfi.com.vn/v1RESThttps://algo-socket.sfi.com.vnStream| Base URL — REST | https://algo-api.sfi.com.vn (đã trỏ về VPS) |
|---|---|
| Base URL — Stream | https://algo-socket.sfi.com.vn (đã trỏ về VPS) |
| Môi trường | Thật — số từ VPS, đang chạy (kiểm qua hai tên miền ngày 07/10/2026). Không còn môi trường UAT riêng: key cấp cho app là key môi trường thật |
| Tiền tố phiên bản | /v1 cho REST; stream không có tiền tố. v1 chỉ thêm field, không bao giờ đổi nghĩa hay bỏ field cũ — đổi cấu trúc thì lên /v2 và chạy song song một thời gian |
| Định dạng | JSON UTF-8, tiếng Việt có dấu. Stream: text/event-stream hoặc frame JSON qua WebSocket |
| Phương thức | Chỉ GET (và OPTIONS cho preflight CORS). Không một endpoint nào ghi, đóng lệnh hay roll hợp đồng |
| Xác thực | Authorization: Bearer <key> hoặc X-API-Key: <key>. Scope algo:read cho REST, algo:stream cho stream |
| Key của app | Gửi riêng qua kênh bảo mật — không đăng trên tài liệu này. Môi trường thật, scope algo:read + algo:stream. Lộ key thì báo đội Algo để thu hồi và cấp key mới |
| Giờ và tiền | UTC, ISO 8601 có Z. Tiền USD hai chữ số thập phân, chưa trừ phí. Giá theo đơn vị của sàn |
Bản v1.2 (09/10/2026) chỉ thêm — không đổi nghĩa hay bỏ field nào của v1.1, nên code viết theo v1.1 vẫn chạy. Đọc mục 0 trước: cả luồng dữ liệu trong một trang. Những gì mới so với v1.1 ở mục 9.
0 Luồng dữ liệu — đọc trước
API có đúng hai đường ra. REST trả ảnh chụp: trạng thái hiện tại của thẻ, vị thế, lịch sử — app gọi khi cần. Stream đẩy sự kiện: mỗi lần có gì thay đổi, server gửi ngay một gói nhỏ nói “cái gì vừa đổi”. App ghép hai thứ: mở app thì lấy ảnh chụp một lần, từ đó chỉ sửa màn hình theo sự kiện. Không phải gọi lại REST liên tục.
Vòng đời của app — năm bước
- Mở app: gọi
GET /v1/products(thẻ, kèm hỗ trợ/kháng cự),GET /v1/positions(vị thế đang mở, kèm khốicopy),GET /v1/trades(lịch sử, theo trang). Vẽ màn hình. - Mở một kết nối stream — SSE
/stream/signalshoặc WebSocket/ws/signalsởalgo-socket.sfi.com.vn— và giữ nó suốt lúc app mở. - Mỗi sự kiện tới: tra bảng dưới, sửa đúng một chỗ trên màn hình. Lưu
idcủa sự kiện cuối cùng đã xử lý. - Mất mạng rồi nối lại: mở lại stream kèm
?since_id=<id cuối>— server phát bù mọi sự kiện đã lỡ rồi chạy tiếp realtime. Không còn id cuối, hoặc app tắt quá 7 ngày → quay lại bước 1. - Thông báo đẩy không làm trong app: server thông báo của bên app giữ một kết nối stream riêng, chạy 24/7, và đẩy push tới điện thoại — mục 12.
Sự kiện tới → app sửa gì
| event | App sửa gì trên màn hình |
|---|---|
position_opened | Thêm một hàng vị thế; bật nút Copy của thẻ đó. data là Position, có sẵn khối copy |
position_updated | Thay hàng vị thế cùng data.id — SL mới, nhãn BE, TP |
position_closed | Xoá hàng vị thế data.position_id. Nếu data.trade khác null: thêm một dòng lịch sử. Null = lệnh vào sớm bị huỷ, không thêm gì |
trade_corrected | Thay dòng lịch sử cùng trade.id |
product_updated, product_available | Thay nguyên thẻ cùng code (cả hỗ trợ/kháng cự) |
product_unavailable | Làm mờ thẻ, tắt nút Copy của thẻ đó |
product_added v1.2 | Thêm thẻ mới — data là ProductCard đầy đủ |
product_removed v1.2 | Gỡ thẻ data.code cùng vị thế và lịch sử của nó |
service_changed | Bật / tắt băng bảo trì của cả app (mục 7) |
| loại khác | Bỏ qua — VPS có thể thêm loại mới mà app cũ không lỗi |
Ba chỗ hay hiểu nhầm. (1) API không gửi push tới điện thoại — API chỉ phát sự kiện;
server thông báo bên app nghe và tự đẩy. (2) Hệ không có lệnh chờ — “Chờ mua / Chờ bán” là engine đang chờ điều
kiện; bấm Copy là lệnh thị trường lấy số từ khối copy (mục 13). (3) Thêm hay gỡ
sản phẩm, app không cần ra bản mới — server báo bằng product_added / product_removed
(mục 10).
1 Tóm tắt và phạm vi
Trên một VPS riêng có 10 engine giao dịch tự động chạy liên tục — bạc, đồng, bạch kim, ngô, đậu tương, dầu đậu tương, khô đậu tương, lúa mì, đường #11, cà phê Arabica (bạc có hai thẻ — hợp đồng đầy đủ và mini — nên app thấy 11 thẻ) — và một bảng vận hành hiển thị trạng thái của chúng. API này mang đúng những gì bảng đó đang hiển thị ra cho app, và không kèm một quyền điều khiển nào.
Hai bên: Algo chạy dịch vụ API ngay trên VPS — algo-api (REST) và
algo-socket (stream) trỏ thẳng về VPS qua tunnel Cloudflare do IT quản lý, không qua dịch vụ
trung gian nào; app vẽ màn hình và làm nút copy lệnh. Người dùng cuối chỉ ở trên app SFI
Trade — đây là bên tiêu thụ duy nhất của API.
Những gì API không mang
| Không có trong v1 | Vì sao |
|---|---|
| Hàng chỉ số tổng — tổng lãi/lỗ, tổng số lệnh, winrate chung, số engine đang chạy | Là số của người vận hành, không phải của người dùng app. |
| Tình trạng kỹ thuật từng sản phẩm | Sản phẩm tạm dừng thì app biết qua unavailable và service — mục 7. |
| Giờ cập nhật trên trang chi tiết | App mặc định dữ liệu là mới; lúc không mới thì mục 8 nói rõ app hiện gì. |
| Nhãn giao dịch thử | Không có ý nghĩa với người dùng app. |
| Lý do xu hướng và lý do thoát lệnh | App hiện kết luận, không diễn giải. Bảng vận hành cũng không có cột lý do thoát. |
| Giá thị trường live | Quy định từ 04/09/2026: không hiện giá thị trường đang chạy. API cũng không phát. |
| Các con số bảng không hiện — số lần chạm và khoảng cách của vùng, SL ban đầu, rủi ro còn lại, kết quả theo R, lãi/lỗ tích luỹ của thẻ | App hiện đúng như bảng. Vì v1 chỉ được thêm field, cần thì thêm sau — còn bớt sau khi app đã build là phá app. |
2 Từ bảng vận hành sang app
Mỗi khối trên bảng đang chạy tương ứng với một phần của API. Số trên app sẽ khớp số trên bảng vì hai bên đọc cùng một lớp dữ liệu.
1Tín hiệu toàn thị trường
GET /v1/products → items[]: mã hợp đồng contract,
trạng thái state_label, win_rate, số vị thế open_positions.
Hỗ trợ/kháng cự lấy ngay trên mỗi thẻ: items[].zones v1.2 — in vùng đầu
mỗi phía dạng lo–hi (mục 11).
Nút copy lệnh nằm đúng chỗ bảng đã dành sẵn: ô “Sao chép” ở mỗi hàng, chỉ bật khi sản phẩm đang có vị thế.
2Vị thế mở
GET /v1/positions và các sự kiện position_*: hợp đồng, chiều Mua/Bán,
giá vào, SL kèm nhãn BE khi đã về hoà vốn, TP hoặc “—”, giờ vào, lãi/lỗ tạm
tính, và object copy cho nút copy lệnh.
3Lịch sử tín hiệu
GET /v1/trades và sự kiện position_closed: hợp đồng, chiều, giá mở,
giá đóng, hiệu suất (pnl_usd), thời gian đóng. Một quyết định của engine bạc sinh
lệnh ở cả hai thẻ — short ngày 14/09: SIZ26 −$5.325,00, QIZ26 −$2.662,50.
Trang chi tiết một sản phẩm
GET /v1/products/SI: Trạng thái =
state_label · Hỗ trợ/kháng cự = zones · Xu hướng = trend ·
Winrate = win_rate trên trades lệnh · Giao dịch gần đây =
recent_trades. Xu hướng chỉ hiện ở trang chi tiết — cột xu hướng trên bảng
tổng quan đã tắt.App lấy gì
- một lần
/v1/products,/v1/positionskhi mở màn hình (mục 0); - một kết nối stream cho mọi thay đổi sau đó — kể cả thẻ mới, thẻ bị gỡ và vùng hỗ trợ/kháng cự;
/v1/tradestheo trang khi người dùng cuộn lịch sử;- nút copy lệnh đọc object
copyVPS đã tính sẵn.
3 Hai loại dữ liệu · danh mục endpoint
Toàn bộ API chỉ có hai loại dữ liệu. Phân biệt được hai loại này thì phần còn lại đọc rất nhanh.
| Ảnh chụp | Sự kiện | |
|---|---|---|
| Là gì | Trạng thái hiện tại: thẻ sản phẩm, vị thế đang mở, lịch sử lệnh | Việc vừa xảy ra: vào lệnh, dời SL, đóng lệnh, thẻ tạm ngưng, bảo trì |
| Lấy ở đâu | GET /v1/products · /v1/products/{code} · /v1/positions · /v1/trades | /stream/signals (SSE) · /ws/signals (WS) · đọc bù GET /v1/events |
| Nhịp | VPS gửi ảnh chụp mỗi 5 giây, chỉ khi có thay đổi | Gửi ngay khi phát hiện, trễ vài giây |
| Đảm bảo | Luôn là ảnh chụp mới nhất đã nhận. snapshot_at chỉ dùng để đồng bộ. Mỗi ảnh chụp thay nguyên danh sách thẻ | Giao ít nhất một lần. Khử trùng theo id; bù phần lỡ bằng since_id |
| Dùng để | Mở màn hình lần đầu, kéo xuống làm mới | Cập nhật tức thì trên màn hình, thông báo đẩy |
Danh mục endpoint
| Endpoint | Service | Mô tả | Xác thực |
|---|---|---|---|
GET /v1/products | REST | Các thẻ sản phẩm đang công bố | algo:read |
GET /v1/products/{code} | REST | Chi tiết một thẻ: vùng hỗ trợ/kháng cự, vị thế, lệnh gần đây | algo:read |
GET /v1/positions | REST | Vị thế đang mở theo từng thẻ, kèm lệnh để copy | algo:read |
GET /v1/trades | REST | Lịch sử lệnh đã đóng — con trỏ hai chiều, lọc sản phẩm và khoảng ngày | algo:read |
GET /v1/events | REST | Đọc bù sự kiện sau khi mất kết nối stream (giữ 7 ngày) | algo:stream |
SSE /stream/signals | Stream | Server-Sent Events — đẩy sự kiện, trình duyệt tự nối lại | algo:stream |
WS /ws/signals | Stream | WebSocket — cùng dữ liệu, cho client đã có hạ tầng WS | algo:stream |
GET /v1/health v1.2 | REST | Kiểm sống cho giám sát: {"ok": true, "source_error": null}. Không mang số liệu | không cần key |
4 REST API
4.1 Danh sách thẻ sản phẩm
/v1/productsalgo:readcurl -H "X-API-Key: <key>" \
"https://algo-api.sfi.com.vn/v1/products"
Response 200 — rút gọn còn 3 thẻ
{
"snapshot_at": "2026-09-22T02:52:16Z",
"service": {
"state": "binh_thuong",
"label": null,
"since": null,
"version": "1.3",
"data_at": "2026-09-22T02:52:16Z"
},
"items": [
{
"code": "SI",
"name": "Bạc",
"category": "Kim loại",
"contract": "SIZ26",
"cqg_symbol": "SIEZ26",
"cqg_root": "SIE",
"state": "watching",
"state_label": "Đang theo dõi",
"trend": "di_ngang",
"trades": 50,
"win_rate": 0.6,
"open_positions": 0
},
{
"code": "QI",
"name": "Bạc mini",
"category": "Kim loại",
"contract": "QIZ26",
"cqg_symbol": "MQIZ26",
"cqg_root": "MQI",
"state": "watching",
"state_label": "Đang theo dõi",
"trend": "di_ngang",
"trades": 63,
"win_rate": 0.619,
"open_positions": 0
},
{
"code": "HG",
"name": "Đồng",
"category": "Kim loại",
"contract": "HGZ26",
"cqg_symbol": "CPEZ26",
"cqg_root": "CPE",
"state": "wait_buy",
"state_label": "Chờ mua",
"trend": "tang",
"trades": 77,
"win_rate": 0.519,
"open_positions": 0
}
],
"count": 9
}
Xem đủ 9 thẻ, số thật từ cụm dev (ảnh chụp 22/09 — trước khi có SB, KC)
{
"snapshot_at": "2026-09-22T02:52:16Z",
"service": {
"state": "binh_thuong",
"label": null,
"since": null,
"version": "1.3",
"data_at": "2026-09-22T02:52:16Z"
},
"items": [
{
"code": "SI",
"name": "Bạc",
"category": "Kim loại",
"contract": "SIZ26",
"cqg_symbol": "SIEZ26",
"cqg_root": "SIE",
"state": "watching",
"state_label": "Đang theo dõi",
"trend": "di_ngang",
"trades": 50,
"win_rate": 0.6,
"open_positions": 0
},
{
"code": "QI",
"name": "Bạc mini",
"category": "Kim loại",
"contract": "QIZ26",
"cqg_symbol": "MQIZ26",
"cqg_root": "MQI",
"state": "watching",
"state_label": "Đang theo dõi",
"trend": "di_ngang",
"trades": 63,
"win_rate": 0.619,
"open_positions": 0
},
{
"code": "HG",
"name": "Đồng",
"category": "Kim loại",
"contract": "HGZ26",
"cqg_symbol": "CPEZ26",
"cqg_root": "CPE",
"state": "wait_buy",
"state_label": "Chờ mua",
"trend": "tang",
"trades": 77,
"win_rate": 0.519,
"open_positions": 0
},
{
"code": "PL",
"name": "Bạch kim",
"category": "Kim loại",
"contract": "PLV26",
"cqg_symbol": "PLEV26",
"cqg_root": "PLE",
"state": "watching",
"state_label": "Đang theo dõi",
"trend": "di_ngang",
"trades": 32,
"win_rate": 0.562,
"open_positions": 0
},
{
"code": "ZC",
"name": "Ngô",
"category": "Nông sản",
"contract": "ZCZ26",
"cqg_symbol": "ZCEZ26",
"cqg_root": "ZCE",
"state": "watching",
"state_label": "Đang theo dõi",
"trend": "di_ngang",
"trades": 15,
"win_rate": 0.267,
"open_positions": 0
},
{
"code": "ZS",
"name": "Đậu tương",
"category": "Nông sản",
"contract": "ZSX26",
"cqg_symbol": "ZSEX26",
"cqg_root": "ZSE",
"state": "wait_buy",
"state_label": "Chờ mua",
"trend": "tang",
"trades": 20,
"win_rate": 0.8,
"open_positions": 0
},
{
"code": "ZL",
"name": "Dầu đậu tương",
"category": "Nông sản",
"contract": "ZLZ26",
"cqg_symbol": "ZLEZ26",
"cqg_root": "ZLE",
"state": "wait_sell",
"state_label": "Chờ bán",
"trend": "giam",
"trades": 24,
"win_rate": 0.625,
"open_positions": 0
},
{
"code": "ZM",
"name": "Khô đậu tương",
"category": "Nông sản",
"contract": "ZMZ26",
"cqg_symbol": "ZMEZ26",
"cqg_root": "ZME",
"state": "wait_buy",
"state_label": "Chờ mua",
"trend": "tang",
"trades": 34,
"win_rate": 0.559,
"open_positions": 0
},
{
"code": "ZW",
"name": "Lúa mì",
"category": "Nông sản",
"contract": "ZWZ26",
"cqg_symbol": "ZWAZ26",
"cqg_root": "ZWA",
"state": "wait_sell",
"state_label": "Chờ bán",
"trend": "giam",
"trades": 52,
"win_rate": 0.308,
"open_positions": 0
}
],
"count": 9
}
v1.2: mỗi phần tử items mang thêm
zones — cùng khuôn trang chi tiết (4.2). Ví dụ trên rút gọn nên chưa in zones; ví dụ đầy đủ ở
mục 11.
Field cấp ngoài
| Field | Kiểu | Giải thích |
|---|---|---|
snapshot_at | string (ISO) | Thời điểm ảnh chụp |
service | object | Trạng thái dịch vụ — bình thường, đang bảo trì, hay mất kết nối. Luôn có mặt; xem mục 7 |
items | array | Mảng ProductCard của các thẻ đang công bố. Thứ tự do VPS quyết — app hiện đúng thứ tự này, không tự sắp lại |
count | number | Số phần tử trong items |
4.2 Chi tiết một thẻ
/v1/products/{code}algo:read| Tham số đường dẫn | Kiểu | Giải thích |
|---|---|---|
code | string | Mã của một thẻ đang công bố — lấy từ /v1/products, app không giữ danh sách cố định. Không phân biệt hoa thường. Mã không có hoặc đã bị gỡ trả 404 {"error":"not_found"} |
curl -H "X-API-Key: <key>" \
"https://algo-api.sfi.com.vn/v1/products/SI"
Response 200 — bạc, số thật
{
"code": "SI",
"name": "Bạc",
"category": "Kim loại",
"contract": "SIZ26",
"cqg_symbol": "SIEZ26",
"cqg_root": "SIE",
"state": "watching",
"state_label": "Đang theo dõi",
"trend": "di_ngang",
"trades": 50,
"win_rate": 0.6,
"open_positions": 0,
"zones": {
"support": [
{
"price": 66.2,
"lo": 65.82,
"hi": 66.2
},
{
"price": 65.465,
"lo": 65.06,
"hi": 65.465
}
],
"resistance": [
{
"price": 67.035,
"lo": 67.035,
"hi": 67.485
},
{
"price": 67.685,
"lo": 67.685,
"hi": 68.08
}
],
"support_note": null,
"resistance_note": null
},
"positions": [],
"recent_trades": [
{
"id": 1041,
"key": "SI:short:2026-09-16T18:15:00Z",
"product": "SI",
"contract": "SIZ26",
"side": "short",
"quantity": 1,
"entry_time": "2026-09-16T18:15:00Z",
"entry_price": 64.13,
"exit_time": "2026-09-16T18:45:00Z",
"exit_price": 64.645,
"pnl_usd": -2575.01
},
{
"id": 1017,
"key": "SI:short:2026-09-14T09:30:00Z",
"product": "SI",
"contract": "SIZ26",
"side": "short",
"quantity": 1,
"entry_time": "2026-09-14T09:30:00Z",
"entry_price": 63.05,
"exit_time": "2026-09-14T16:47:08Z",
"exit_price": 64.115,
"pnl_usd": -5325.0
}
]
}
| Field thêm | Kiểu | Giải thích |
|---|---|---|
zones.support, zones.resistance | array | Mảng Zone, vùng gần giá nhất đứng trước, tối đa 2 mỗi phía |
zones.support_note, zones.resistance_note | string · null | Chỉ có chữ khi phía đó không có vùng nào: câu giải thích vì sao, ví dụ giá đang ở vùng cao nhất của dữ liệu. App in câu này, không in gạch ngang — không có vùng là một câu trả lời, không phải thiếu dữ liệu |
positions | array | Vị thế đang mở của riêng thẻ này |
recent_trades | array | 10 lệnh đóng gần nhất của thẻ (ví dụ trên rút gọn còn 2) |
4.3 Vị thế đang mở
/v1/positionsalgo:read| Tham số | Kiểu | Mặc định | Giải thích |
|---|---|---|---|
products | string | không lọc | Danh sách mã ngăn bằng dấu phẩy, ví dụ SI,QI. Mã lạ không gây lỗi — chỉ là không khớp gì |
curl -H "Authorization: Bearer <key>" \
"https://algo-api.sfi.com.vn/v1/positions?products=SI,QI"
Ví dụ dưới dựng lại lệnh short bạc ngày 14/09 lúc còn mở — vào 09:30 UTC giá 63,05,
SL 64,115 — với giá giả định 63,25 để minh hoạ lãi/lỗ tạm tính. Lệnh bạc không đặt TP nên
take_profit là null.
Response 200 — minh hoạ
{
"snapshot_at": "2026-09-14T10:05:00Z",
"service": {
"state": "binh_thuong",
"label": null,
"since": null,
"version": "1.3",
"data_at": "2026-09-22T02:52:16Z"
},
"items": [
{
"id": "SI:short:2026-09-14T09:30:00Z",
"product": "SI",
"contract": "SIZ26",
"side": "short",
"entry_time": "2026-09-14T09:30:00Z",
"entry_price": 63.05,
"stop_price": 64.115,
"take_profit": null,
"breakeven_locked": false,
"quantity": 1,
"unrealized_pnl_usd": -1000.0,
"pnl_basis": "live",
"copy": {
"contract": "SIZ26",
"cqg_symbol": "SIEZ26",
"side": "sell",
"quantity": 1,
"entry_ref": 63.05,
"stop_price": 64.115,
"take_profit": null,
"tick_size": 0.005
}
},
{
"id": "QI:short:2026-09-14T09:30:00Z",
"product": "QI",
"contract": "QIZ26",
"side": "short",
"entry_time": "2026-09-14T09:30:00Z",
"entry_price": 63.05,
"stop_price": 64.115,
"take_profit": null,
"breakeven_locked": false,
"quantity": 1,
"unrealized_pnl_usd": -500.0,
"pnl_basis": "live",
"copy": {
"contract": "QIZ26",
"cqg_symbol": "MQIZ26",
"side": "sell",
"quantity": 1,
"entry_ref": 63.05,
"stop_price": 64.125,
"take_profit": null,
"tick_size": 0.0125
}
}
],
"count": 2
}
4.4 Lịch sử lệnh
App hiện đúng như bảng vận hành: mã hợp đồng, chiều lệnh, giá mở, giá đóng, hiệu suất và thời gian đóng. Bảng không hiện lệnh thoát bằng gì, nên API cũng không trả lý do thoát.
/v1/tradesalgo:read| Tham số | Kiểu | Mặc định | Giải thích |
|---|---|---|---|
products | string | không lọc | Mã thẻ ngăn bằng dấu phẩy, không phân biệt hoa thường |
from | string | không giới hạn | Đầu khoảng, tính trên exit_time. Dạng ngày 2026-09-01 = từ 00:00 UTC hôm đó. Nhận cả ISO đầy đủ. Sai định dạng trả 400 |
to | string | không giới hạn | Cuối khoảng. Dạng ngày gồm trọn ngày đó; timestamp đầy đủ thì là mốc loại trừ |
since_id | integer | 0 | Con trỏ “có gì mới”: chỉ trả lệnh sau lệnh mang id này. Truyền lại next_since_id của lần trước — đừng tự tính |
before_id | integer | 0 | Lật về quá khứ: chỉ trả lệnh trước lệnh mang id này. Truyền lại next_before_id |
order | string | desc | desc mới nhất trước, asc cũ nhất trước |
limit | integer | 100 | Kẹp trong 1…100. Xin nhiều hơn không báo lỗi, chỉ trả 100 |
Ví dụ
# 20 lệnh đóng gần nhất, mọi sản phẩm — cũng là cách poll
GET /v1/trades?limit=20
# Poll vòng sau: chỉ những lệnh mới hơn con trỏ đã lưu
GET /v1/trades?since_id=1043
# Lệnh bạc (thẻ SI và thẻ QI) trong tháng 9
GET /v1/trades?products=SI,QI&from=2026-09-01&to=2026-09-30
# Lật tiếp về quá khứ: dùng next_before_id của trang trước
GET /v1/trades?limit=20&before_id=1041
Response 200 — ba lệnh thật, id minh hoạ
{
"items": [
{
"id": 1043,
"key": "ZL:short:2026-08-10T11:45:00Z",
"product": "ZL",
"contract": "ZLQ26",
"side": "short",
"quantity": 1,
"entry_time": "2026-08-10T11:45:00Z",
"entry_price": null,
"exit_time": "2026-08-10T12:15:00Z",
"exit_price": null,
"pnl_usd": -114.0
},
{
"id": 1042,
"key": "QI:short:2026-09-14T09:30:00Z",
"product": "QI",
"contract": "QIZ26",
"side": "short",
"quantity": 1,
"entry_time": "2026-09-14T09:30:00Z",
"entry_price": 63.05,
"exit_time": "2026-09-14T16:47:08Z",
"exit_price": 64.115,
"pnl_usd": -2662.5
},
{
"id": 1041,
"key": "SI:short:2026-09-14T09:30:00Z",
"product": "SI",
"contract": "SIZ26",
"side": "short",
"quantity": 1,
"entry_time": "2026-09-14T09:30:00Z",
"entry_price": 63.05,
"exit_time": "2026-09-14T16:47:08Z",
"exit_price": 64.115,
"pnl_usd": -5325.0
}
],
"next_since_id": 1043,
"next_before_id": 1041,
"count": 3
}
Giá vào và giá ra có thể là null. Một phần lệnh của dầu đậu tương và khô
đậu tương không được ghi giá — lệnh đầu tiên trong ví dụ trên là một lệnh như vậy. Lãi/lỗ của lệnh
vẫn đúng. App hiện “chưa ghi” như bảng vận hành, không hiện 0 và không để ô trống: ô trống
làm hai lệnh liền nhau trông như ghi trùng.
| Field | Kiểu | Giải thích |
|---|---|---|
items | array | Mảng Trade, sắp theo id theo chiều order. Rỗng khi không còn lệnh nào khớp |
next_since_id | number | id mới nhất trong trang. Khi items rỗng thì trả lại đúng since_id đã gửi, nên vòng poll không bao giờ lùi |
next_before_id | number | id cũ nhất trong trang — truyền lại vào ?before_id= để lật tiếp |
count | number | Số phần tử. count < limit nghĩa là đã đọc hết dữ liệu khớp bộ lọc |
4.5 Đọc bù sự kiện
/v1/eventsalgo:streamStream tự nối lại nhưng không tự phát lại phần đã lỡ. Hai cách bù: mở stream kèm ?since_id=
v1.2 (mục 5.1), hoặc sau mỗi lần nối lại gọi endpoint này từ id cuối cùng đã
xử lý. Sự kiện được giữ 7 ngày.
| Tham số | Kiểu | Mặc định | Giải thích |
|---|---|---|---|
since_id | integer | bắt buộc | Chỉ trả sự kiện có id lớn hơn |
products | string | không lọc | Như ở /v1/trades |
limit | integer | 100 | Kẹp trong 1…100 |
# Vừa nối lại stream: đọc bù mọi sự kiện sau id cuối đã xử lý
GET /v1/events?since_id=58213
# Chỉ sự kiện của bạc
GET /v1/events?since_id=58213&products=SI,QI
Response 200
{
"items": [
{
"id": 58190,
"event": "position_opened",
"at": "2026-09-14T09:30:01Z",
"product": "SI",
"data": {
"id": "SI:short:2026-09-14T09:30:00Z",
"product": "SI",
"contract": "SIZ26",
"side": "short",
"entry_time": "2026-09-14T09:30:00Z",
"entry_price": 63.05,
"stop_price": 64.115,
"take_profit": null,
"breakeven_locked": false,
"quantity": 1,
"unrealized_pnl_usd": 0.0,
"pnl_basis": "live",
"copy": {
"contract": "SIZ26",
"cqg_symbol": "SIEZ26",
"side": "sell",
"quantity": 1,
"entry_ref": 63.05,
"stop_price": 64.115,
"take_profit": null,
"tick_size": 0.005
}
}
},
{
"id": 58213,
"event": "position_closed",
"at": "2026-09-14T16:47:08Z",
"product": "SI",
"data": {
"position_id": "SI:short:2026-09-14T09:30:00Z",
"trade": {
"id": 1041,
"key": "SI:short:2026-09-14T09:30:00Z",
"product": "SI",
"contract": "SIZ26",
"side": "short",
"quantity": 1,
"entry_time": "2026-09-14T09:30:00Z",
"entry_price": 63.05,
"exit_time": "2026-09-14T16:47:08Z",
"exit_price": 64.115,
"pnl_usd": -5325.0
}
}
}
],
"next_since_id": 58213,
"count": 2
}
5 Realtime stream
Stream đẩy sự kiện ngay khi VPS phát hiện. Stream không có tiền tố phiên bản; nội dung sự kiện theo đúng đối tượng Event.
Xác thực
Trình duyệt không gắn được header vào EventSource hay WebSocket, nên stream nhận thêm
?key=<key>. Key thiếu, sai, đã thu hồi hoặc không có scope algo:stream:
SSE trả 401, WebSocket đóng với mã 4401.
5.1 Server-Sent Events
/stream/signalsalgo:streamTham số products lọc theo mã thẻ, giống REST. v1.2: tham số
since_id — server phát bù ngay trong kết nối mọi sự kiện có id lớn hơn (tối đa 1.000), rồi chạy realtime.
Dùng được cho cả SSE lẫn WebSocket, thay cho bước gọi /v1/events lúc nối lại.
| Dòng | Ý nghĩa |
|---|---|
data: | JSON một dòng — một Event |
retry: | Gợi ý chờ 3000 ms trước khi nối lại. Chỉ gửi một lần, lúc mở kết nối |
: connected | Comment SSE gửi ngay khi mở — mốc “từ đây trở đi là realtime” |
: keepalive | Mỗi 20 giây khi không có sự kiện, để proxy không cắt kết nối. Bỏ qua nó |
Khung sự kiện trên dây — lệnh thật 14/09, rút gọn
: connected
retry: 3000
data: {"id": 58213, "event": "position_closed", "at": "2026-09-14T16:47:08Z", "product": "SI", "data": {"position_id": "SI:short:2026-09-14T09:30:00Z", "trade": {"id": 1041, "side": "short", "entry_price": 63.05, "exit_price": 64.115, "pnl_usd": -5325.0}}}
: keepalive
Ví dụ — curl
curl -N -H "Authorization: Bearer <key>" \
"https://algo-socket.sfi.com.vn/stream/signals?products=SI,QI"
Ví dụ — JavaScript
const API = 'https://algo-api.sfi.com.vn';
const SOCKET = 'https://algo-socket.sfi.com.vn';
const daXuLy = new Set(); // giao ít nhất một lần → tự khử trùng theo id
let idCuoi = Number(localStorage.getItem('algo_id_cuoi') || 0);
function xuLy(ev) {
if (daXuLy.has(ev.id)) return;
daXuLy.add(ev.id);
idCuoi = Math.max(idCuoi, ev.id);
localStorage.setItem('algo_id_cuoi', idCuoi);
switch (ev.event) {
case 'position_opened': veViThe(ev.data); break; // có lệnh → copy được ngay
case 'position_updated': veViThe(ev.data); baoNguoiDaCopy(ev); break; // kéo SL, BE, sửa giá vào
case 'position_closed': xoaViThe(ev.data.position_id);
if (ev.data.trade) themLenh(ev.data.trade); break; // v1.2: null = vào sớm bị huỷ
case 'trade_corrected': suaLenh(ev.data.trade); break;
case 'product_unavailable': tamNgungThe(ev.data.code, ev.data.unavailable); break; // mờ thẻ, tắt copy thẻ đó
case 'product_available': thayThe(ev.data); break; // thẻ đầy đủ, app thay nguyên thẻ
case 'product_updated': thayThe(ev.data); break; // v1.1: thẻ đổi trạng thái, xu hướng, winrate, vùng…
case 'product_added': themThe(ev.data); break; // v1.2: thẻ mới — ProductCard đầy đủ
case 'product_removed': goThe(ev.data.code); break; // v1.2: gỡ thẻ + vị thế + lịch sử
case 'service_changed': datTrangThaiDichVu(ev.data); break; // màn hình bảo trì bật/tắt
}
}
async function docBu() { // EventSource tự nối lại nhưng KHÔNG bù phần đã lỡ
const r = await fetch(`${API}/v1/events?since_id=${idCuoi}`, { headers: { 'X-API-Key': KEY } });
(await r.json()).items.forEach(xuLy);
}
const es = new EventSource(`${SOCKET}/stream/signals?key=${KEY}`);
es.onopen = docBu;
es.onmessage = (e) => xuLy(JSON.parse(e.data));
5.2 WebSocket
/ws/signalsalgo:stream| Frame | Ý nghĩa |
|---|---|
{"event":"position_closed", "id":…} | Một Event, cấu trúc như SSE |
{"event":"ping"} | Keepalive mỗi 20 giây khi không có sự kiện. Đừng xử lý như một sự kiện |
Mã đóng kết nối
| Code | Nguyên nhân | Nên làm gì |
|---|---|---|
4401 | Key thiếu, sai, đã thu hồi, hoặc không có scope algo:stream | Đừng nối lại — sửa key trước, nếu không sẽ quay vòng vô hạn |
1013 | Client xử lý quá chậm, hàng đợi 200 sự kiện đầy | Nối lại ngay, rồi đọc bù bằng /v1/events |
1000 / 1001 / khác | Đóng bình thường, deploy, hoặc đứt mạng | Nối lại có backoff |
Ví dụ — JavaScript
function noi() {
const ws = new WebSocket(`wss://algo-socket.sfi.com.vn/ws/signals?key=${KEY}`);
ws.onopen = docBu; // cùng hàm đọc bù như SSE
ws.onmessage = (e) => {
const m = JSON.parse(e.data);
if (m.event === 'ping') return; // keepalive 20 giây
xuLy(m);
};
ws.onclose = (e) => {
if (e.code === 4401) return; // key sai / thu hồi — nối lại vô nghĩa
setTimeout(noi, backoff()); // 1013 và mọi trường hợp khác
};
}
noi();
5.3 Các loại sự kiện
| event | Khi nào | data |
|---|---|---|
position_opened | Có lệnh mới | Position. Copy được ngay, kèm SL (và TP nếu có) hiện tại |
position_updated | Kéo SL (kể cả lên hoà vốn), đổi TP, hoặc VPS sửa giá vào sau khi đối chiếu sổ. Tối đa một lần mỗi 15 phút cho mỗi vị thế; riêng lần đầu lên hoà vốn gửi ngay | Position. App báo người đã copy đặt lại theo copy; breakeven_locked = true thì hiện BE |
position_closed | Lệnh đóng — gửi ngay khi VPS ghi nhận | {position_id, trade}. v1.2: trade = null khi lệnh vào sớm bị huỷ, không thành lệnh (gửi khi đã chờ sổ 30 phút) — app xoá vị thế, không thêm lịch sử |
trade_corrected | Hiếm: đối chiếu sổ ra giá hoặc tiền của một lệnh đã đóng khác lúc VPS gửi | {trade} — bản đúng, cùng id; app sửa dòng lịch sử |
product_unavailable | Một thẻ tạm không có số: đang nâng cấp engine, hoặc tạm dừng vì lý do khác. Gửi một lần lúc chuyển trạng thái | {code, unavailable}. App làm mờ thẻ, hiện unavailable.label, tắt nút copy của riêng thẻ đó — 7.1 |
product_available | Thẻ có số trở lại | ProductCard đầy đủ. App thay nguyên thẻ |
product_updated v1.1 | Thẻ đổi một trong: state, state_label, trend, trades, win_rate, contract (roll), stale_since, zones v1.2. Tối đa một lần mỗi 60 giây cho mỗi thẻ — đổi nhiều lần trong 60 giây thì gửi bản mới nhất. Không gửi cho thẻ đang unavailable | ProductCard đầy đủ. App thay nguyên thẻ |
product_added v1.2 | Đội Algo thêm một sản phẩm vào cấu hình VPS. Engine mới có thể còn đang khởi động — thẻ khi đó mang unavailable | ProductCard đầy đủ. App thêm thẻ, nạp lịch sử bằng /v1/trades?products={code} — mục 10 |
product_removed v1.2 | Đội Algo gỡ sản phẩm khỏi cấu hình VPS. Chỉ lúc đó — engine lỗi, mất feed, khởi động lại không bao giờ sinh sự kiện này | {code}. App gỡ thẻ cùng vị thế và lịch sử của nó |
service_changed | Trạng thái dịch vụ đổi: vào bảo trì, ra khỏi bảo trì, mất kết nối với VPS | Object ServiceState. product là null vì đây là việc của cả cụm |
App gặp event lạ thì bỏ qua — VPS có thể thêm loại sự kiện mới mà không làm app cũ lỗi.
Minh hoạ — có lệnh là copy được
{
"id": 58190,
"event": "position_opened",
"at": "2026-09-14T09:30:01Z",
"product": "SI",
"data": {
"id": "SI:short:2026-09-14T09:30:00Z",
"product": "SI",
"contract": "SIZ26",
"side": "short",
"entry_time": "2026-09-14T09:30:00Z",
"entry_price": 63.05,
"stop_price": 64.115,
"take_profit": null,
"breakeven_locked": false,
"quantity": 1,
"unrealized_pnl_usd": 0.0,
"pnl_basis": "live",
"copy": {
"contract": "SIZ26",
"cqg_symbol": "SIEZ26",
"side": "sell",
"quantity": 1,
"entry_ref": 63.05,
"stop_price": 64.115,
"take_profit": null,
"tick_size": 0.005
}
}
}
6 Đối tượng dữ liệu
Quy tắc chung để VPS thêm sản phẩm mới mà không phải phát hành lại app: mọi danh sách mã và mọi
field dạng liệt kê (state, event, service.state) đều có thể có
thêm giá trị. Gặp giá trị lạ, app hiện nhãn *_label do server trả; gặp
event lạ thì bỏ qua; sản phẩm chưa có biểu tượng thì dùng hình mặc định.
ProductCard
| Field | Kiểu | Null? | Giải thích |
|---|---|---|---|
code | string | không | Mã thẻ. Danh sách mở — hiện có SI, QI, HG, PL, ZC, ZS, ZL, ZM, ZW, SB, KC; app không giữ danh sách cố định |
name | string | không | Tên tiếng Việt: Bạc, Bạc mini, Đồng, Dầu đậu tương… |
category | string | có | Nhóm để app chia tab: Kim loại · Nông sản · Nguyên liệu công nghiệp (SB, KC — v1.1). null khi VPS chưa khai |
contract | string | không | Hợp đồng đang giao dịch, mã sàn (SIZ26). Đổi khi roll |
cqg_symbol | string | không | Mã CQG (SIEZ26) — cùng hệ mã với AI News Feed |
cqg_root | string | không | Gốc CQG (SIE). Truyền vào products= của AI News để lấy tin của đúng sản phẩm |
icon_url v1.1 | string | có | Địa chỉ ảnh sản phẩm do server trả. Hiện null cho mọi thẻ — app dùng ảnh dự phòng của mình. Khi server có ảnh thì field tự có giá trị, không cần ra bản app. Có cả ở thẻ đang unavailable |
state | string | không | open_position · wait_buy · wait_sell · watching · closed (v1.1). Danh sách mở. Thứ tự: đang giữ vị thế → open_position (kể cả giữ qua đêm ngoài giờ phiên); ngoài giờ phiên → closed; còn lại theo luật của engine |
state_label | string | không | Nhãn hiển thị: Lệnh đang mở · Chờ mua · Chờ bán · Đang theo dõi · Đóng cửa |
trend | string | có | tang · giam · di_ngang. Chỉ hiện ở trang chi tiết. null khi chưa đủ nến để tính |
trades | number | không | Số lệnh thẻ này tham gia — mẫu số của win_rate |
win_rate | number | có | 0…1, hoà tính là thắng. null khi chưa có lệnh |
open_positions | number | không | Số vị thế đang mở của thẻ. Lớn hơn 0 thì bật nút copy |
stale_since v1.1 | string (ISO) | có | null = số đang sống. Có giờ = feed của sản phẩm đứt giữa phiên từ giờ đó (giờ tick cuối cùng). Thẻ vẫn giữ số cuối như bảng vận hành; app hiện nhãn kiểu “Dữ liệu tạm dừng từ HH:MM” và nên tắt nút copy của thẻ tới khi field về null. Ngoài giờ phiên luôn null — thẻ đã là closed |
zones v1.2 | object | vắng | Hỗ trợ / kháng cự — cùng khuôn zones của 4.2: support, resistance (mảng Zone, gần giá trước, tối đa 2), support_note, resistance_note. Vắng khi thẻ unavailable. Xem mục 11 |
unavailable | object | vắng | Chỉ có mặt khi thẻ tạm không có số. Lúc đó các field số ở trên vắng theo, và app giữ số cuối cùng nó đang có. Xem 7.1 |
Zone
| Field | Kiểu | Null? | Giải thích |
|---|---|---|---|
price | number | không | Mép vùng gần giá nhất: mép trên với hỗ trợ, mép dưới với kháng cự. VPS chọn sẵn. In số này khi chỉ còn chỗ cho một con số; còn lại in vùng lo–hi (v1.2, mục 11) |
lo, hi | number | không | Cận dưới, cận trên của vùng — nếu app muốn vẽ dải |
Position
| Field | Kiểu | Null? | Giải thích |
|---|---|---|---|
id | string | không | Khoá bền: {product}:{side}:{entry_time} |
product | string | không | Thẻ chứa vị thế |
contract | string | không | Hợp đồng của thẻ: SIZ26 ở thẻ SI, QIZ26 ở thẻ QI |
side | string | không | long · short — app hiện Mua · Bán |
entry_time, entry_price | string (ISO), number | không | Giờ và giá vào — giá mở của nến vào lệnh |
stop_price | number | không | SL hiện tại |
take_profit | number | có | TP hiện tại. Chỉ một số sản phẩm đặt TP (hôm nay là dầu đậu tương và khô đậu tương); null thì app hiện “TP: —” như bảng |
breakeven_locked | boolean | không | SL đã kéo lên hoà vốn hoặc tốt hơn — app hiện nhãn BE |
quantity | number | không | Số hợp đồng của vị thế trên thẻ này |
unrealized_pnl_usd | number | có | Lãi/lỗ tạm tính. null khi chưa có giá — không bao giờ là một số 0 giả |
pnl_basis | string | có | live theo giá đang chạy · close theo giá đóng nến gần nhất khi chưa có giá (ngoài phiên, ngày nghỉ). Bảng in số close nghiêng và gạch chấm; app làm tương tự để hai loại số không đọc như nhau |
copy | object | không | Lệnh để copy, VPS tính sẵn — xem ngay dưới |
Copy lệnh — Position.copy
Phần duy nhất app phải làm thêm là nút copy lệnh. Mọi con số trong object này do VPS tính sẵn — app chỉ hiện và cho copy, không tự ghép mã hay làm tròn giá.
| Field | Kiểu | Null? | Giải thích |
|---|---|---|---|
contract | string | không | Mã sàn: SIZ26 |
cqg_symbol | string | không | Mã CQG: SIEZ26 — đã chốt, cùng bảng mã với ProductCard.cqg_symbol; contract là mã sàn. Luôn có cả hai |
side | string | không | buy · sell |
quantity | number | không | Số hợp đồng |
entry_ref | number | không | Giá vào tham chiếu — giá mở nến vào lệnh |
stop_price | number | không | SL hiện tại, đã làm tròn theo bước giá của hợp đồng |
take_profit | number | có | TP hiện tại, đã làm tròn theo bước giá. Có thì lệnh copy phải kèm TP; null thì lệnh copy không có TP |
tick_size | number | không | Bước giá: 0.005 với SIZ26, 0.0125 với QIZ26 |
Có lệnh là copy được. copy luôn mang SL và TP hiện tại: mỗi lần kéo SL, VPS
gửi position_updated và app báo người đã copy đặt lại; SL lên hoà vốn thì hiện
BE. Cũng vì thế mà nút copy bị tắt khi app không chắc mình đang thấy mức mới nhất — xem
mục 7.
Làm tròn — “kích hoạt y hệt luật” (đã chốt, v1.1). SL và TP của lệnh copy được
làm tròn về bước giá của chính hợp đồng copy, theo chiều để lệnh kích hoạt đúng lúc luật kích hoạt:
vị thế mua — SL làm tròn xuống, TP làm tròn lên; vị thế bán — SL làm tròn
lên, TP làm tròn xuống. Giá đã nằm đúng bước thì giữ nguyên. Ví dụ: short bạc có SL 64.115 trên
SIZ26 (bước 0.005); copy sang QIZ26 (bước 0.0125) thì SL là 64.125 — 64.1125 sẽ
kích hoạt sớm hơn luật. App dùng nguyên số server gửi, không tự làm tròn.
Trade
| Field | Kiểu | Null? | Giải thích |
|---|---|---|---|
id | number | không | Số tăng dần do algo-api cấp khi nhận — dùng cho since_id / before_id |
key | string | không | Khoá nghiệp vụ bền: {product}:{side}:{entry_time} |
product, contract, side, quantity | như Position | không | Như Position |
entry_time | string (ISO) | không | Giờ vào |
entry_price | number | có | Giá vào. null ở những lệnh không được ghi giá — app hiện “chưa ghi”. Lệnh gieo nghiên cứu của ZL, ZM (trước ngày chạy thật) có giá dựng lại từ dữ liệu giá lịch sử, khớp đúng tiền từng lệnh — bổ sung ngày 07/10/2026 |
exit_time | string (ISO) | không | Giờ ra, theo lúc VPS ghi nhận lệnh đóng — có thể là giờ tick (16:47:08) hoặc giờ nến |
exit_price | number | có | Giá ra. null như entry_price |
pnl_usd | number | không | Hiệu suất — lãi/lỗ của lệnh trên thẻ này, USD, chưa trừ phí. Đúng con số cột Hiệu suất của bảng |
Event
| Field | Kiểu | Null? | Giải thích |
|---|---|---|---|
id | number | không | Số tăng dần của cả dòng sự kiện — khoá khử trùng và con trỏ since_id |
event | string | không | Loại sự kiện, xem 5.3 |
at | string (ISO) | không | Thời điểm xảy ra |
product | string | có | Thẻ liên quan. null với sự kiện của cả cụm (service_changed) |
data | object | không | Nội dung theo loại sự kiện |
ServiceState
| Field | Kiểu | Null? | Giải thích |
|---|---|---|---|
state | string | không | binh_thuong · bao_tri · mat_ket_noi. Danh sách mở — giá trị lạ thì app xử như mat_ket_noi và hiện label |
label | string | có | Câu hiển thị cho người dùng, do server gửi. null khi binh_thuong |
since | string (ISO) | có | Ở trạng thái này từ lúc nào. App đếm số phút đã trôi từ mốc này |
version | string | có | Tên bản hệ thống đang cài, ví dụ 1.3 — hiện trên màn hình bảo trì |
data_at | string (ISO) | có | Ảnh chụp cuối cùng còn dùng được — app hiện “số liệu giữ ở lúc …” |
7 Bảo trì và nâng cấp
Cập nhật engine và nâng cấp định kỳ là việc thường xuyên của cụm. Có hai việc khác hẳn nhau, và app hiện chúng khác hẳn nhau:
| Nâng cấp một sản phẩm | Nâng cả hệ thống | |
|---|---|---|
| Là gì | Sửa lại logic giao dịch của một sản phẩm | Sửa phần dùng chung của cả cụm |
| Các sản phẩm khác | Không đụng tới — vẫn chạy, số vẫn thật | Dừng hết trong lúc nâng |
| App | Ghi ngay trên thẻ đó, các thẻ khác không đổi gì | Màn hình bảo trì thay cả màn |
| Tín hiệu | product_unavailable → product_available | service_changed |
7.1 Nâng cấp một sản phẩm
Đây là việc thường xuyên nhất: một sản phẩm được sửa logic, chạy thử, rồi thay vào — trong khi các sản phẩm khác vẫn giao dịch bình thường. Thẻ đó không rời danh sách, chỉ tạm không có số; lịch sử lệnh của thẻ vẫn giữ nguyên.
| Mốc | Chuyện gì xảy ra | App nhận |
|---|---|---|
| 0 · Chuẩn bị | Bản engine mới được cài lên VPS, chưa chạy | Không gì cả. Engine cũ vẫn giao dịch, người dùng chưa bị ảnh hưởng |
| 1 · Bắt đầu | Engine của sản phẩm dừng để chạy bản mới | product_unavailable — thẻ ghi “Đang nâng cấp engine” |
| 2 · Đang dựng | Bản mới dựng lại dữ liệu — nửa phút tới gần mười phút tuỳ sản phẩm | Không có sự kiện mới — thẻ vẫn đang nâng cấp, app tự đếm phút |
| 3 · Xong | Bản mới ra số đầu tiên | product_available — thẻ đầy đủ, app thay nguyên thẻ |
VPS tự phân biệt nâng cấp có chủ đích với dừng ngoài ý muốn: nâng cấp thì nhãn “Đang nâng
cấp engine”, còn lại là “Tạm ngưng cập nhật”. App chỉ hiện label, không cần phân
biệt.
Độ trễ khoảng 5–10 giây kể từ lúc sản phẩm dừng. Một sản phẩm dừng rồi tự chạy lại liên tục vẫn chỉ là một lần tạm ngưng, nên app không bị bắn sự kiện qua lại.
App không phải phát hành lại cho một lần nâng cấp sản phẩm. Bản vá kiểu “lệnh hiện sớm
hơn” chỉ làm position_opened tới sớm hơn — app đã nghe sự kiện đó sẵn rồi.
Ví dụ — sự kiện lúc bắt đầu
{
"id": 58251,
"event": "product_unavailable",
"at": "2026-09-14T08:41:07Z",
"product": "ZS",
"data": {
"code": "ZS",
"unavailable": {
"label": "Đang nâng cấp engine",
"since": "2026-09-14T08:41:02Z"
}
}
}
Ví dụ — thẻ trong ảnh chụp lúc đang nâng cấp
{
"code": "ZS",
"name": "Đậu tương",
"category": "Nông sản",
"contract": "ZSX26",
"cqg_symbol": "ZSEX26",
"cqg_root": "ZSE",
"unavailable": {
"label": "Đang nâng cấp engine",
"since": "2026-09-14T08:41:02Z"
}
}
Phần nhận dạng của thẻ vẫn còn; các field số thì vắng. App giữ nguyên số cuối cùng nó đang có,
làm mờ đi, hiện unavailable.label, và tắt nút copy của riêng thẻ đó.
7.2 Nâng cả hệ thống
Khi nâng phần dùng chung, cả cụm dừng một lúc. Bảng vận hành trên web lúc đó bị thay bằng trang
“đang nâng cấp”; app làm đúng như vậy: nhận service_changed với
state = "bao_tri" thì hiện màn hình bảo trì thay cả màn, và nhận
service_changed với binh_thuong thì tải lại số mới trong một lần. Trang bảo
trì của bảng vận hành đếm số phút đã trôi chứ không hứa thời điểm xong — app làm y vậy.
Ví dụ — service lúc đang nâng cả hệ thống
{
"state": "bao_tri",
"label": "Đang nâng cấp hệ thống",
"since": "2026-09-14T08:41:02Z",
"version": "1.3",
"data_at": "2026-09-14T08:40:55Z"
}
Ví dụ — sự kiện đẩy khi trạng thái đổi
{
"id": 58240,
"event": "service_changed",
"at": "2026-09-14T08:41:04Z",
"product": null,
"data": {
"state": "bao_tri",
"label": "Đang nâng cấp hệ thống",
"since": "2026-09-14T08:41:02Z",
"version": "1.3",
"data_at": "2026-09-14T08:40:55Z"
}
}
7.3 Ai đặt trạng thái nào
service.state | Nghĩa | Ai đặt |
|---|---|---|
binh_thuong | Bình thường | VPS |
bao_tri | Đang nâng cả hệ thống, có kế hoạch | VPS — gửi service_changed lúc bắt đầu và lúc xong |
mat_ket_noi | Không nhận được gì từ VPS — mất điện, đứt mạng | Nền tảng SFI — tự đặt khi quá một ngưỡng không nhận được ảnh chụp nào, vì lúc đó VPS không nói được gì |
Ví dụ — mất kết nối với VPS
{
"state": "mat_ket_noi",
"label": "Chưa nhận được số mới",
"since": "2026-09-14T08:46:10Z",
"version": "1.3",
"data_at": "2026-09-14T08:45:05Z"
}
7.4 App hiện gì
Nguyên tắc: không bao giờ để màn hình đứng im mà không nói lý do, và cũng không làm người dùng hoảng. Số cũ vẫn là số đúng — chỉ là đúng tại một thời điểm đã qua, nên app nói rõ thời điểm đó thay vì xoá đi hay để nguyên như thật.
| Nâng cấp một sản phẩm | Nâng cả hệ thống | Mất kết nối | |
|---|---|---|---|
| Phạm vi | đúng một thẻ | cả màn hình | cả màn hình |
| Hiện gì | thẻ đó mờ đi, ghi “Đang nâng cấp engine”, đếm số phút đã trôi | màn hình bảo trì thay cả màn, đếm số phút đã trôi | băng xám giữ nguyên màn hình, có nút “Thử lại” |
| Các thẻ khác | bình thường tuyệt đối — số của chúng vẫn thật | — | giữ số cuối cùng |
| Nút copy lệnh | tắt riêng thẻ đó | không có màn hình để bấm | tắt toàn bộ |
| Thông báo đẩy | không bắn cho thẻ đó | không bắn | không bắn |
| Lịch sử lệnh của thẻ | giữ nguyên, vẫn đọc được | giữ nguyên | giữ nguyên |
| Khi xong | nhãn biến mất, thẻ có số trở lại | màn hình bảo trì biến mất, số nhảy về mới trong một lần | như nâng cả hệ thống |
Ba màn hình trên chỉ minh hoạ hành vi; bố cục và màu thuộc về đội app.
Ba chi tiết đáng giữ
- Đếm lên, không đếm ngược. Không ai biết trước một lần nâng cấp mất bao lâu, nên app hiện số phút đã trôi, không hứa thời điểm xong.
- Tắt nút copy là quyết định về an toàn, không phải về giao diện. Giá trị của nút copy nằm ở chỗ nó luôn kèm SL và TP hiện tại. Lúc app không chắc mình đang thấy mức mới nhất thì thà không cho copy còn hơn cho copy một mức đã cũ.
- Câu chữ do server gửi. App hiển thị
label, chỉ tự tính phần định dạng (số phút đã trôi, giờ địa phương). Đổi câu chữ không cần phát hành lại app.
// App: một chỗ duy nhất quyết định màn hình đang ở trạng thái nào.
function datTrangThaiDichVu(sv) {
const dungDuoc = sv.state === 'binh_thuong';
bangBao.hien = !dungDuoc;
bangBao.chu = sv.label; // câu do server gửi, app không tự chế
bangBao.dem = sv.since; // app đếm số phút ĐÃ trôi, không hứa lúc xong
bangBao.ban = sv.state === 'bao_tri' ? sv.version : null;
bangBao.soLuc = sv.data_at; // "số liệu giữ ở lúc ..."
batNutCopy(dungDuoc); // tắt copy: SL có thể đã đổi mà app không thấy
batThongBaoDay(dungDuoc);
}
8 Quy ước số liệu
- Một quyết định, nhiều thẻ. Khi engine bạc vào cả hợp đồng lớn và mini, VPS phát hai vị
thế ở hai thẻ: SI (1 HĐ
SIZ26, 5.000 oz) và QI (1 HĐQIZ26, 2.500 oz). Tiền mỗi thẻ là phần của đúng hợp đồng đó. Ví dụ thật 14/09: SIZ26 −$5.325,00 và QIZ26 −$2.662,50, cộng lại −$7.987,50. - Chưa trừ phí. Mọi con số lãi/lỗ là tiền của lệnh trên chính thẻ đó và chưa trừ phí giao dịch. App hiện đúng con số VPS gửi, không tự trừ thêm.
- Hoà tính là thắng trong mọi
win_rate— quy ước chung của cả 10 engine. - Lệnh hiện lúc nào. Tuỳ sản phẩm, lệnh hiện ngay lúc nến vào lệnh mở, hoặc lúc nến đó
đóng. App không cần phân biệt — hiện khi nhận
position_opened. Nếu VPS sửa giá vào sau khi đối chiếu sổ, app nhậnposition_updatedvà báo cho người đã copy. - Kéo SL và BE. Mỗi lần SL được kéo, VPS gửi
position_updatedvới mức mới trongcopy.stop_price. SL lên hoà vốn thìbreakeven_locked = truevà app hiện BE. - Đóng lệnh ngay khi chạm SL. Với một số sản phẩm, lệnh đóng được gửi ngay lúc giá chạm SL
và sổ đối chiếu sau; nếu ra giá hoặc tiền khác, VPS gửi
trade_correctedvà app sửa dòng lịch sử. - Chưa có thì là null, và app nói rõ. Lãi/lỗ tạm tính chưa có giá thì null, không phải 0.
Giá vào/ra không được ghi thì null và app hiện “chưa ghi”. Một phía không có vùng hỗ trợ/kháng cự
thì app in câu trong
*_note. - Không có giá thị trường live — theo quy định từ 04/09/2026. Lãi/lỗ tạm tính vẫn có.
- Thẻ chỉ tồn tại khi VPS công bố. VPS gỡ một thẻ thì thẻ đó và lịch sử lệnh của nó rời khỏi app; engine lỗi, mất feed hay đang nâng cấp thì thẻ vẫn còn.
9 Thay đổi v1.2, v1.1 và các mục đã chốt
Thay đổi v1.2 · 09/10/2026
Chỉ thêm. “Nay mới ghi” = server đã chạy như vậy từ v1.1, tài liệu giờ mới viết ra. Giờ là giờ Việt Nam.
| Mục | Thay đổi | App cần làm | Hiệu lực |
|---|---|---|---|
product_added, product_removed | Sự kiện MỚI: thêm sản phẩm → thẻ đầy đủ; gỡ sản phẩm → {code} | Thêm / gỡ thẻ theo sự kiện — mục 10 | khi đội Algo báo đã lên VPS |
ProductCard.zones | Field MỚI trên mỗi thẻ của /v1/products, cùng khuôn trang chi tiết | Bảng tổng đọc vùng từ danh sách, in lo–hi — mục 11 | 09/10 11:39 |
product_updated | Phát thêm khi zones đổi; vẫn ≤ 1 lần / 60 giây mỗi thẻ | Thay nguyên thẻ như cũ | 09/10 11:39 |
| Vùng đổi phía | Vùng chỉ chuyển hỗ trợ ↔ kháng cự khi một nến 15 phút đóng xuyên qua | In nguyên thứ tự server gửi | 09/10 12:13 |
position_closed.trade = null | Lệnh vào sớm bị huỷ | Xoá vị thế, không thêm lịch sử, không văng lỗi | Nay mới ghi |
Stream ?since_id= | Phát bù ngay trong kết nối | Tuỳ chọn — thay bước gọi /v1/events | Nay mới ghi |
GET /v1/health | Kiểm sống, không cần key | Tuỳ chọn | Nay mới ghi |
| Thông báo đẩy | Ghép layout với sự kiện; không đẩy trade_corrected — thay gợi ý cũ ở cuối mục này | Server thông báo nghe stream — mục 12 | Không đổi server |
| Hợp đồng lệnh gieo ZW, ZC | Sửa contract của 51 lệnh gieo (ZW 40, ZC 11) cho đúng hợp đồng thật; phát 51 trade_corrected, id giữ nguyên | Không | 09/10 09:39 |
Thay đổi v1.1 · 07/10/2026
Bản v1.1 chỉ thêm — không đổi nghĩa hay bỏ field nào của v1. Mọi mục dưới đây đã chạy trên VPS và kiểm qua hai tên miền ngày 07/10/2026.
| Mục | Thay đổi | App cần làm |
|---|---|---|
Thẻ mới SB, KC | Đường #11 và Cà phê Arabica, danh mục “Nguyên liệu công nghiệp”. Tổng 11 thẻ cho 10 engine (bạc có hai thẻ) | Không — danh sách thẻ vốn mở |
state = closed | Ngoài giờ phiên của sản phẩm — KC, SB nghỉ đêm; ngũ cốc nghỉ giữa ngày; kim loại nghỉ 16:00–17:00 Chicago. Nhãn “Đóng cửa”. Đang giữ vị thế qua đêm thì vẫn là open_position | Hiện nhãn server trả. Thẻ đóng cửa vẫn đủ số, không phải unavailable |
ProductCard.stale_since | Field MỚI — giờ feed đứt giữa phiên; null khi bình thường | Hiện “Dữ liệu tạm dừng từ …”, nên tắt copy của thẻ |
ProductCard.icon_url | Field MỚI — hiện null cho mọi thẻ | Dùng ảnh dự phòng khi null |
Sự kiện product_updated | MỚI — thẻ đổi trạng thái, nhãn, xu hướng, số lệnh, winrate, hợp đồng hoặc stale_since; tối đa một lần mỗi 60 giây cho mỗi thẻ | Thay nguyên thẻ, như product_available |
| KC chờ mua / chờ bán | Trong phiên, thẻ KC trả wait_buy / wait_sell theo luật của engine (bản chạy thử trả watching) | Không |
| Làm tròn lệnh copy | Đã chốt “kích hoạt y hệt luật” — xem Copy lệnh. Ví dụ QI ở mục 4.3 sửa 64.1125 → 64.125 | Không — dùng nguyên số server gửi |
| Mã trong lệnh copy | contract = mã sàn, cqg_symbol = mã CQG — đã chốt, luôn có cả hai | Dùng cqg_symbol khi đặt lệnh qua CQG |
| Lệnh gieo trong lịch sử | KC, ZL, ZM có lệnh gieo từ nghiên cứu (trước ngày chạy thật), giữ đúng như bảng vận hành. Giá vào/ra của lệnh gieo ZL, ZM được dựng lại từ dữ liệu giá lịch sử và khớp đúng tiền từng lệnh | Không — ngày 07/10 app đã nhận 53 trade_corrected một lần khi bổ sung giá |
| Bạc: hai thẻ | Một quyết định của engine bạc có thể mở vị thế ở cả SI (hợp đồng đầy đủ) và QI (mini). Hai thẻ là hai lệnh copy độc lập, mỗi lệnh đúng khối lượng của thẻ đó. Không thêm field nhóm chân lệnh | Tuỳ app: có thể ghi chú ở thẻ bạc |
algo-socket | Một tên miền nhận cả SSE /stream/signals lẫn WebSocket /ws/signals | Không |
| Key | Key môi trường thật, scope algo:read + algo:stream, cấp ngày 07/10/2026 — gửi riêng, không đăng trên tài liệu. Không còn key nào khác | Dùng key mới |
Đã chốt
- Hạ tầng — không có dịch vụ trung gian.
algo-api.sfi.com.vn(REST) vàalgo-socket.sfi.com.vn(SSE + WebSocket) trỏ thẳng về VPS qua tunnel Cloudflare do IT quản lý. - Key — một key riêng cho app, môi trường thật, scope
algo:read+algo:stream; đội Algo cấp và giữ. Gửi riêng qua kênh bảo mật, không đăng trên tài liệu. - Ngưỡng mất kết nối — 60 giây không có ảnh chụp mới từ VPS thì
service_changedchuyển sangmat_ket_noi. - Nút copy trong lúc bảo trì — tắt hẳn, như mục 7.
- Hệ mã và làm tròn —
contractlà mã sàn,cqg_symbollà mã CQG; SL/TP làm tròn “kích hoạt y hệt luật” trên bước giá của hợp đồng copy. - Giá trị
category— Kim loại (SI, QI, HG, PL) · Nông sản (ZC, ZW, ZS, ZL, ZM) · Nguyên liệu công nghiệp (SB, KC). - Lưu lịch sử — giữ toàn bộ, từ ngày bắt đầu của từng sản phẩm; app nạp bằng
/v1/tradeslúc khởi tạo rồi theo dòng sự kiện. - Gỡ thẻ thì lịch sử rời app — xác nhận: thẻ bị gỡ thì lệnh cũ của nó không còn trong
/v1/trades. - Giới hạn nhịp gọi — chưa đặt. App nên dùng stream cho cập nhật và chỉ gọi REST lúc mở app, lúc nối lại và khi đọc bù. Nếu cần giới hạn, đội Algo báo trước.
- Thu hồi key — báo đội Algo; key bị thu hồi mất hiệu lực sau vài giây (REST trả 401, WebSocket đóng mã 4401) và key mới được gửi riêng.
- Thông báo đẩy — v1.2 xem mục 12: đẩy
position_opened,position_updated(khi SL đổi),position_closed; không đẩytrade_corrected.
10 Thêm và gỡ sản phẩm — app tự cập nhật
Đội Algo thêm hay gỡ sản phẩm ở phía VPS. Từ v1.2 server báo ngay bằng hai sự kiện; app không phải tự dò, cũng không cần ra bản app mới.
| Tình huống | Server làm gì | App làm gì |
|---|---|---|
| Thêm sản phẩm | Phát product_added, data = ProductCard đầy đủ. Thẻ có ngay trong GET /v1/products, đúng vị trí; lịch sử ở /v1/trades?products={code}. Engine mới còn khởi động thì thẻ mang unavailable, có số sau đó báo product_available | Thêm thẻ; muốn đúng thứ tự thì lấy lại /v1/products; nạp lịch sử của mã đó |
| Gỡ sản phẩm | Phát product_removed, data = {code}. Thẻ rời /v1/products, lệnh cũ rời /v1/trades | Gỡ thẻ cùng vị thế và lịch sử của nó |
| Engine lỗi, mất feed, đang nâng cấp | Không phải gỡ. Thẻ vẫn trong danh sách: product_unavailable, rồi product_available khi có số lại (mục 7) | Làm mờ thẻ, không bỏ thẻ |
product_removed chỉ phát khi sản phẩm bị gỡ khỏi cấu hình VPS. Engine chết, router hay API khởi
động lại đều không sinh sự kiện này — đã kiểm cả trường hợp khe bạc chết làm thẻ Bạc mini tạm vắng khỏi dữ liệu.
Ví dụ — hai sự kiện (mã RC là minh hoạ)
{"id": 402, "event": "product_added", "at": "2026-10-20T03:00:05Z", "product": "RC",
"data": {"code": "RC", "name": "Cà phê Robusta", "category": "Nguyên liệu công nghiệp",
"contract": "RCF27", "cqg_symbol": "LRCF27", "state": "watching", ...}}
{"id": 517, "event": "product_removed", "at": "2026-11-02T03:00:04Z", "product": "RC",
"data": {"code": "RC"}}
Lưới an toàn — vẫn nên có
- Lấy lại
GET /v1/productslúc mở app, và lúc nối lại stream mà không bù được (không còn id cuối, hoặc quá 7 ngày). So với danh sách đang hiện: mã mới → thêm, mã biến mất → gỡ, mã còn lại → thay nguyên thẻ. - Sự kiện mang một mã app chưa có mà không phải
product_added(ví dụ app vừa mở lại): không văng lỗi — lấy lại danh sách trước rồi mới xử lý; lấy lại xong vẫn không có thì bỏ qua sự kiện đó. - Giá trị lạ trên thẻ mới theo mục 6:
categorymới → nhóm mới, đúng chữ server trả;icon_urlnull → ảnh mặc định.
Mẫu JavaScript
case 'product_added': themThe(ev.data); napLichSu(ev.data.code); break; // thẻ mới
case 'product_removed': goThe(ev.data.code); break; // bỏ thẻ + vị thế + lịch sử
Cách thử
Khi đội Algo thêm sản phẩm thật, hai bên hẹn giờ: app mở sẵn, thẻ mới phải tự hiện mà không tải lại app.
11 Hỗ trợ / kháng cự dạng vùng
Thay đổi phía server
| Mục | Thay đổi | App cần làm |
|---|---|---|
ProductCard.zones | Field MỚI trên mỗi thẻ của
GET /v1/products — cùng khuôn zones của GET /v1/products/{code}
(mục 4.2): support, resistance (vùng gần giá nhất đứng trước, tối đa 2 mỗi phía, mỗi vùng
{lo, hi, price}), support_note, resistance_note. Thẻ đang
unavailable không mang field này. | Bảng tổng đọc vùng từ danh sách, không gọi chi tiết từng thẻ nữa |
Sự kiện product_updated | Phát thêm khi zones
của thẻ đổi. Vẫn tối đa một lần mỗi 60 giây cho mỗi thẻ, vẫn mang ProductCard đầy đủ — giờ có cả
zones. product_available cũng mang zones. | Thay nguyên thẻ như cũ — vùng tự cập nhật |
zones[].price | Không đổi: mép vùng gần giá nhất — mép trên của vùng hỗ trợ, mép dưới của vùng kháng cự. | Giữ để tương thích; in khi chỉ còn chỗ cho một số |
Hiện trên app
- Cột Hỗ trợ / Kháng cự của bảng tổng: in vùng đầu mỗi phía dạng
lo–hi—zones.support[0]vàzones.resistance[0]. Ví dụ ZL: 67,68–67,91 / 68,19–68,42. - Vùng có
lo=hi→ in một số. - Vùng chỉ đổi phía khi một nến 15 phút đóng xuyên qua nó. Hỗ trợ vẫn là hỗ trợ khi giá chạm, đi vào
trong, hay chọc thủng trong lúc nến 15 phút còn chạy; chỉ khi nến đó đóng dưới
lothì vùng mới chuyển sang kháng cự (kháng cự ngược lại). Vì vậy giá hiện tại có lúc nằm trong[lo, hi], hoặc vượt qua mép vùng vài phút. Đó là chủ đích để vùng không nhảy theo từng nhịp giá — app in nguyên thứ tự server gửi, không tự lọc hay xếp lại vùng theo giá. - Phía không có vùng →
support/resistancelà mảng rỗng và*_notecó câu giải thích (ví dụ “Giá đang ở vùng cao nhất của dữ liệu — chưa có kháng cự phía trên.”) → in “—”, câu note để ở dòng phụ hoặc khi chạm vào. - Trang chi tiết giữ như cũ (
GET /v1/products/{code}→zones), nên in cả hai vùng mỗi phía dạnglo–hi. - Số đã nằm đúng bước giá của hợp đồng — app in nguyên, không làm tròn lại.
Ví dụ — một thẻ trong GET /v1/products
{
"code": "ZL", "name": "Dầu đậu tương", "category": "Nông sản",
"contract": "ZLZ26", "state": "watching", "state_label": "Đang theo dõi",
"trend": "di_ngang", "trades": 29, "win_rate": 0.621, "open_positions": 0,
"stale_since": null, "icon_url": null,
"zones": {
"support": [{"price": 67.91, "lo": 67.68, "hi": 67.91},
{"price": 67.64, "lo": 67.42, "hi": 67.64}],
"resistance": [{"price": 68.19, "lo": 68.19, "hi": 68.42},
{"price": 68.46, "lo": 68.46, "hi": 68.68}],
"support_note": null,
"resistance_note": null
}
}
Mẫu JavaScript
function vung(z) { // {lo, hi} -> "67.68–67.91"
return z.lo === z.hi ? fmt(z.lo) : fmt(z.lo) + '–' + fmt(z.hi);
}
function oHoTroKhangCu(the) {
const z = the.zones;
if (!z) return ''; // thẻ unavailable: giữ ô cũ, làm mờ
const ht = z.support[0] ? vung(z.support[0]) : '—';
const kc = z.resistance[0] ? vung(z.resistance[0]) : '—';
return ht + ' / ' + kc; // note (*_note) đặt ở dòng phụ khi phía đó là "—"
}
// product_updated / product_available: thay nguyên thẻ -> ô tự vẽ lại với zones mới.
12 Thông báo đẩy
Các thông báo trên layout của app — vị thế mở mới, dời SL, đóng vị thế — dùng được ngay các sự kiện mục 5.3, không cần API mới. Điểm mấu chốt: người nghe stream là server thông báo của app, không phải app trên điện thoại. App tắt thì không nghe được gì, mà thông báo đẩy vẫn phải tới.
Server thông báo làm gì
- Giữ một kết nối cho cả hệ tới
algo-socket.sfi.com.vn— WebSocket/ws/signalshoặc SSE/stream/signals— bằng key (scopealgo:stream). Hoặc gọiGET /v1/events?since_id=<id cuối>vài giây một lần (limittối đa 100). - Lưu id sự kiện cuối vào chỗ bền (cơ sở dữ liệu). Nối lại hay server khởi động lại thì mở stream kèm
?since_id=<id cuối>— không sót thông báo nào. - Khử trùng theo
id— stream giao ít nhất một lần.
Ghép thông báo với sự kiện
| Thông báo | Sự kiện | Lấy gì để hiện |
|---|---|---|
| Vị thế mở mới | position_opened | Tên thẻ, copy.cqg_symbol, Mua/Bán (side), entry_price, stop_price, take_profit nếu có, entry_time |
| Dời SL / lên hoà vốn | position_updated khi stop_price đổi | SL mới; breakeven_locked = true → ghi “hoà vốn”. Sự kiện chỉ mang bản mới nhất — SL cũ thì server thông báo tự nhớ theo data.id |
| Đóng vị thế | position_closed có trade | trade.exit_price, lãi/lỗ trade.pnl_usd, trade.exit_time |
| (không đẩy) | position_closed với trade = null | Lệnh vào sớm bị huỷ. Không đẩy, hoặc báo “tín hiệu đã huỷ” nếu đã đẩy “vị thế mở mới” |
position_updated còn phát khi TP, giá vào hay khối lượng đổi. Chỉ đẩy khi stop_price đổi.
Không đẩy
trade_corrected— sửa lịch sử, có thể đến hàng loạt (07/10: 53 lần một lượt; 09/10: 51 lần). Chỉ cập nhật dòng lịch sử.product_*(kể cảproduct_added/product_removed) — cập nhật thẻ trong app; muốn báo “có sản phẩm mới” thì app tự quyết.service_changed— hiện băng bảo trì trong app (mục 7).
Tên hiển thị
Dùng name + mã CQG, ví dụ “Ngô ZCEZ26”, “Bạc SIEZ26” — cùng mã bảng vận hành đang hiện.
contract là mã sàn (ZCZ26); mã CQG nằm ở copy.cqg_symbol của vị thế và cqg_symbol
của thẻ. Không dùng dạng “ZCE/USD”. Giá in nguyên số server gửi.
Ví dụ — một position_opened thật (ZL, 08/10/2026)
{"id": 199, "event": "position_opened", "at": "2026-10-08T16:30:00Z", "product": "ZL",
"data": {"id": "ZL:short:2026-10-08T16:30:00Z", "product": "ZL", "contract": "ZLZ26",
"side": "short", "entry_time": "2026-10-08T16:30:00Z", "entry_price": 67.86,
"stop_price": 68.26, "take_profit": null, "breakeven_locked": false, "quantity": 1,
"copy": {"contract": "ZLZ26", "cqg_symbol": "ZLEZ26", "side": "sell", "quantity": 1,
"entry_ref": 67.86, "stop_price": 68.26, "take_profit": null, "tick_size": 0.01}}}
Mẫu — server thông báo (Node.js, thư viện ws)
import WebSocket from 'ws';
let idCuoi = await docIdCuoi(); // lưu bền (DB), không phải bộ nhớ
const viThe = new Map(); // id vị thế -> {sl đã báo, mã CQG}
const tenThe = new Map(); // code -> name, nạp từ GET /v1/products
function xuLy(ev) {
if (ev.id <= idCuoi) return; // khử trùng: giao ít nhất một lần
idCuoi = ev.id; luuIdCuoi(idCuoi);
const d = ev.data, ten = tenThe.get(ev.product) ?? ev.product;
switch (ev.event) {
case 'position_opened':
viThe.set(d.id, { sl: d.stop_price, ma: d.copy.cqg_symbol });
return day(`${ten} ${d.copy.cqg_symbol}`,
`${d.side === 'long' ? 'Mua' : 'Bán'} @ ${d.entry_price} · SL ${d.stop_price}`);
case 'position_updated': {
const cu = viThe.get(d.id)?.sl;
if (d.stop_price === cu) return; // đổi TP / giá vào / khối lượng: không đẩy
viThe.set(d.id, { sl: d.stop_price, ma: d.copy.cqg_symbol });
return day(`${ten} ${d.copy.cqg_symbol}`,
`Dời SL ${cu ?? '—'} → ${d.stop_price}${d.breakeven_locked ? ' (hoà vốn)' : ''}`);
}
case 'position_closed': {
const v = viThe.get(d.position_id);
viThe.delete(d.position_id);
if (!d.trade) return; // vào sớm bị huỷ: không đẩy
return day(`${ten} ${v?.ma ?? d.trade.contract}`,
`Đóng @ ${d.trade.exit_price} · ${d.trade.pnl_usd >= 0 ? '+' : ''}${d.trade.pnl_usd} USD`);
}
case 'product_added': tenThe.set(d.code, d.name); return;
// trade_corrected, product_*, service_changed: cập nhật dữ liệu, KHÔNG đẩy
}
}
function noi() {
const ws = new WebSocket(`wss://algo-socket.sfi.com.vn/ws/signals?since_id=${idCuoi}`,
{ headers: { Authorization: `Bearer ${KEY}` } });
ws.on('message', (b) => { const m = JSON.parse(b); if (m.event !== 'ping') xuLy(m); });
ws.on('close', (code) => { if (code !== 4401) setTimeout(noi, 3000); }); // 4401: key sai, đừng nối lại
}
noi();
13 Hỏi đáp
“Giá lệnh chờ” (ví dụ “Bán @661.5”) lấy ở đâu?
- Hệ không đặt lệnh chờ, nên không có giá này.
state=wait_buy/wait_sell(“Chờ mua / Chờ bán”) nghĩa là engine đang chờ điều kiện vào lệnh — chưa có giá. - Khi thẻ có vị thế, khách bấm Copy là vào lệnh thị trường, SL theo lệnh đang chạy — lấy từ
copycủa vị thế (GET /v1/positionshoặc sự kiệnposition_*):cqg_symbol,side,quantity,stop_price,take_profit,tick_size. Giá khớp của khách có thể khác giá vào của engine;entry_refchỉ để tham chiếu. - Cột “Lệnh mở” của bảng tổng: thẻ không có vị thế → “—”; có vị thế → chiều +
entry_pricetừ/v1/positions.
Vì sao take_profit thường là null?
take_profitlà field có thểnull(mục 4.3). Hiện các engine kéo SL theo giá (trail) nên TP lànull; ZL, ZM có TP. TP có thể xuất hiện sau khi mở lệnh, qua mộtposition_updated— ví dụ vị thế ZL ngày 08/10 mở với TPnull, sau đó cập nhật TP 66,93.- App cứ giữ field này. Khi nâng cấp, server điền TP vào đúng field đó, không đổi khuôn.
App tắt lâu rồi mở lại thì sao?
Làm lại bước 1 của mục 0: lấy ảnh chụp REST, rồi mở stream. Nếu còn id cuối và chưa quá 7 ngày,
mở stream kèm ?since_id= để nhận lại các sự kiện đã lỡ (cần cho lịch sử thông báo); không thì ảnh chụp REST
là đủ.
App có cần gọi REST định kỳ không?
Không bắt buộc. Stream đã mang mọi thay đổi, kể cả thẻ mới, thẻ bị gỡ và vùng hỗ trợ/kháng cự. REST chỉ cần lúc mở app, lúc người dùng kéo làm mới, và lúc nối lại mà không bù được.