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/v1REST
https://algo-socket.sfi.com.vnStream
Bản v1.2 · 09/10/2026 — thay v1.1 07/10 Ví dụ lấy từ cụm dev lúc 09:52 ngày 22/09/2026 11 thẻ sản phẩm
Base URL — RESThttps://algo-api.sfi.com.vn (đã trỏ về VPS)
Base URL — Streamhttps://algo-socket.sfi.com.vn (đã trỏ về VPS)
Môi trườngThậ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ạngJSON UTF-8, tiếng Việt có dấu. Stream: text/event-stream hoặc frame JSON qua WebSocket
Phương thứcChỉ 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ựcAuthorization: Bearer <key> hoặc X-API-Key: <key>. Scope algo:read cho REST, algo:stream cho stream
Key của appGử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ềnUTC, 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.

VPS 10 engine API chỉ đọc, trên VPS REST · algo-api ảnh chụp — gọi khi cần Stream · algo-socket sự kiện — đẩy tới ngay App SFI Trade màn hình người dùng Server thông báo của bên app, chạy 24/7 Điện thoại — thông báo đẩy khi app đang mở

Vòng đời của app — năm bước

  1. 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ối copy), GET /v1/trades (lịch sử, theo trang). Vẽ màn hình.
  2. Mở một kết nối stream — SSE /stream/signals hoặc WebSocket /ws/signals ở algo-socket.sfi.com.vn — và giữ nó suốt lúc app mở.
  3. 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 id của sự kiện cuối cùng đã xử lý.
  4. 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.
  5. 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ì

eventApp sửa gì trên màn hình
position_openedThê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_updatedThay hàng vị thế cùng data.id — SL mới, nhãn BE, TP
position_closedXoá 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_correctedThay dòng lịch sử cùng trade.id
product_updated, product_availableThay nguyên thẻ cùng code (cả hỗ trợ/kháng cự)
product_unavailableLàm mờ thẻ, tắt nút Copy của thẻ đó
product_added v1.2Thêm thẻ mới — data là ProductCard đầy đủ
product_removed v1.2Gỡ thẻ data.code cùng vị thế và lịch sử của nó
service_changedBật / tắt băng bảo trì của cả app (mục 7)
loại khácBỏ 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 v1Vì sao
Hàng chỉ số tổng — tổng lãi/lỗ, tổng số lệnh, winrate chung, số engine đang chạyLà 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ẩmSả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ếtApp 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ệnhApp 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 liveQuy đị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.

Bảng vận hành tổng quan: tín hiệu toàn thị trường, vị thế mở, lịch sử tín hiệu 1 2 3
Bảng vận hành, cụm dev ngày 17/09/2026, khổ 1440 px, đã ẩn hàng chỉ số và các ghi chú vận hành. Ba khối đánh số được ánh xạ ngay dưới đây.
Bảng tín hiệu toàn thị trường

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ế.

Bảng vị thế mở

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.

Bảng lịch sử tín hiệu

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

Trang chi tiết bạc SIZ26: trạng thái, hỗ trợ kháng cự, xu hướng, winrate, giao dịch gần đây
Trang chi tiết SIZ26 → 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ì

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ụpSự 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ệnhViệ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 ở đâuGET /v1/products · /v1/products/{code} · /v1/positions · /v1/trades/stream/signals (SSE) · /ws/signals (WS) · đọc bù GET /v1/events
NhịpVPS gửi ảnh chụp mỗi 5 giây, chỉ khi có thay đổiGửi ngay khi phát hiện, trễ vài giây
Đảm bảoLuô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ớiCập nhật tức thì trên màn hình, thông báo đẩy

Danh mục endpoint

EndpointServiceMô tảXác thực
GET /v1/productsRESTCác thẻ sản phẩm đang công bốalgo:read
GET /v1/products/{code}RESTChi tiết một thẻ: vùng hỗ trợ/kháng cự, vị thế, lệnh gần đâyalgo:read
GET /v1/positionsRESTVị thế đang mở theo từng thẻ, kèm lệnh để copyalgo:read
GET /v1/tradesRESTLịch sử lệnh đã đóng — con trỏ hai chiều, lọc sản phẩm và khoảng ngàyalgo:read
GET /v1/eventsRESTĐọc bù sự kiện sau khi mất kết nối stream (giữ 7 ngày)algo:stream
SSE /stream/signalsStreamServer-Sent Events — đẩy sự kiện, trình duyệt tự nối lạialgo:stream
WS /ws/signalsStreamWebSocket — cùng dữ liệu, cho client đã có hạ tầng WSalgo:stream
GET /v1/health v1.2RESTKiểm sống cho giám sát: {"ok": true, "source_error": null}. Không mang số liệukhông cần key

4 REST API

4.1 Danh sách thẻ sản phẩm

GET/v1/productsalgo:read
curl -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

FieldKiểuGiải thích
snapshot_atstring (ISO)Thời điểm ảnh chụp
serviceobjectTrạ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
itemsarrayMả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
countnumberSố phần tử trong items

4.2 Chi tiết một thẻ

GET/v1/products/{code}algo:read
Tham số đường dẫnKiểuGiải thích
codestringMã 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êmKiểuGiải thích
zones.support, zones.resistancearrayMả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_notestring · nullChỉ 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
positionsarrayVị thế đang mở của riêng thẻ này
recent_tradesarray10 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ở

GET/v1/positionsalgo:read
Tham sốKiểuMặc địnhGiải thích
productsstringkhông lọcDanh 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.

GET/v1/tradesalgo:read
Tham sốKiểuMặc địnhGiải thích
productsstringkhông lọcMã thẻ ngăn bằng dấu phẩy, không phân biệt hoa thường
fromstringkhô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
tostringkhông giới hạnCuối khoảng. Dạng ngày gồm trọn ngày đó; timestamp đầy đủ thì là mốc loại trừ
since_idinteger0Con 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_idinteger0Lật về quá khứ: chỉ trả lệnh trước lệnh mang id này. Truyền lại next_before_id
orderstringdescdesc mới nhất trước, asc cũ nhất trước
limitinteger100Kẹ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.

FieldKiểuGiải thích
itemsarrayMả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_idnumberid 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_idnumberid cũ nhất trong trang — truyền lại vào ?before_id= để lật tiếp
countnumberSố 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

GET/v1/eventsalgo:stream

Stream 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ểuMặc địnhGiải thích
since_idintegerbắt buộcChỉ trả sự kiện có id lớn hơn
productsstringkhông lọcNhư ở /v1/trades
limitinteger100Kẹ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

SSE/stream/signalsalgo:stream

Tham 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
: connectedComment SSE gửi ngay khi mở — mốc “từ đây trở đi là realtime”
: keepaliveMỗ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/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

CodeNguyên nhânNên làm gì
4401Key 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
1013Client xử lý quá chậm, hàng đợi 200 sự kiện đầyNố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ạngNố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

eventKhi nàodata
position_openedCó lệnh mớiPosition. Copy được ngay, kèm SL (và TP nếu có) hiện tại
position_updatedKé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 ngayPosition. App báo người đã copy đặt lại theo copy; breakeven_locked = true thì hiện BE
position_closedLệ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_correctedHiế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_unavailableMộ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_availableThẻ có số trở lạiProductCard đầy đủ. App thay nguyên thẻ
product_updated v1.1Thẻ đổ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 unavailableProductCard đầ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 unavailableProductCard đầ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_changedTrạ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 VPSObject 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

FieldKiểuNull?Giải thích
codestringkhôngMã 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
namestringkhôngTên tiếng Việt: Bạc, Bạc mini, Đồng, Dầu đậu tương…
categorystringcó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
contractstringkhôngHợp đồng đang giao dịch, mã sàn (SIZ26). Đổi khi roll
cqg_symbolstringkhôngMã CQG (SIEZ26) — cùng hệ mã với AI News Feed
cqg_rootstringkhôngGố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.1stringcóĐị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
statestringkhôngopen_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_labelstringkhôngNhãn hiển thị: Lệnh đang mở · Chờ mua · Chờ bán · Đang theo dõi · Đóng cửa
trendstringcótang · giam · di_ngang. Chỉ hiện ở trang chi tiết. null khi chưa đủ nến để tính
tradesnumberkhôngSố lệnh thẻ này tham gia — mẫu số của win_rate
win_ratenumbercó0…1, hoà tính là thắng. null khi chưa có lệnh
open_positionsnumberkhôngSố vị thế đang mở của thẻ. Lớn hơn 0 thì bật nút copy
stale_since v1.1string (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.2objectvắngHỗ 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
unavailableobjectvắngChỉ 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

FieldKiểuNull?Giải thích
pricenumberkhôngMé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, hinumberkhôngCận dưới, cận trên của vùng — nếu app muốn vẽ dải

Position

FieldKiểuNull?Giải thích
idstringkhôngKhoá bền: {product}:{side}:{entry_time}
productstringkhôngThẻ chứa vị thế
contractstringkhôngHợp đồng của thẻ: SIZ26 ở thẻ SI, QIZ26 ở thẻ QI
sidestringkhônglong · short — app hiện Mua · Bán
entry_time, entry_pricestring (ISO), numberkhôngGiờ và giá vào — giá mở của nến vào lệnh
stop_pricenumberkhôngSL hiện tại
take_profitnumbercó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_lockedbooleankhôngSL đã kéo lên hoà vốn hoặc tốt hơn — app hiện nhãn BE
quantitynumberkhôngSố hợp đồng của vị thế trên thẻ này
unrealized_pnl_usdnumbercóLãi/lỗ tạm tính. null khi chưa có giá — không bao giờ là một số 0 giả
pnl_basisstringcó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
copyobjectkhôngLệ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á.

FieldKiểuNull?Giải thích
contractstringkhôngMã sàn: SIZ26
cqg_symbolstringkhôngMã CQG: SIEZ26 — đã chốt, cùng bảng mã với ProductCard.cqg_symbol; contract là mã sàn. Luôn có cả hai
sidestringkhôngbuy · sell
quantitynumberkhôngSố hợp đồng
entry_refnumberkhôngGiá vào tham chiếu — giá mở nến vào lệnh
stop_pricenumberkhôngSL hiện tại, đã làm tròn theo bước giá của hợp đồng
take_profitnumbercó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_sizenumberkhôngBướ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

FieldKiểuNull?Giải thích
idnumberkhôngSố tăng dần do algo-api cấp khi nhận — dùng cho since_id / before_id
keystringkhôngKhoá nghiệp vụ bền: {product}:{side}:{entry_time}
product, contract, side, quantitynhư PositionkhôngNhư Position
entry_timestring (ISO)khôngGiờ vào
entry_pricenumbercó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_timestring (ISO)khôngGiờ 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_pricenumbercóGiá ra. null như entry_price
pnl_usdnumberkhôngHiệ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

FieldKiểuNull?Giải thích
idnumberkhôngSố tăng dần của cả dòng sự kiện — khoá khử trùng và con trỏ since_id
eventstringkhôngLoại sự kiện, xem 5.3
atstring (ISO)khôngThời điểm xảy ra
productstringcóThẻ liên quan. null với sự kiện của cả cụm (service_changed)
dataobjectkhôngNội dung theo loại sự kiện

ServiceState

FieldKiểuNull?Giải thích
statestringkhôngbinh_thuong · bao_tri · mat_ket_noi. Danh sách mở — giá trị lạ thì app xử như mat_ket_noi và hiện label
labelstringcóCâu hiển thị cho người dùng, do server gửi. null khi binh_thuong
sincestring (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
versionstringcó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_atstring (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ẩmNâng cả hệ thống
Là gìSửa lại logic giao dịch của một sản phẩmSửa phần dùng chung của cả cụm
Các sản phẩm khácKhông đụng tới — vẫn chạy, số vẫn thậtDừng hết trong lúc nâng
AppGhi 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ệuproduct_unavailable → product_availableservice_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ốcChuyện gì xảy raApp nhận
0 · Chuẩn bịBản engine mới được cài lên VPS, chưa chạyKhông gì cả. Engine cũ vẫn giao dịch, người dùng chưa bị ảnh hưởng
1 · Bắt đầuEngine của sản phẩm dừng để chạy bản mớiproduct_unavailable — thẻ ghi “Đang nâng cấp engine”
2 · Đang dựngBả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ẩmKhông có sự kiện mới — thẻ vẫn đang nâng cấp, app tự đếm phút
3 · XongBản mới ra số đầu tiênproduct_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.stateNghĩaAi đặt
binh_thuongBình thườngVPS
bao_triĐang nâng cả hệ thống, có kế hoạchVPS — gửi service_changed lúc bắt đầu và lúc xong
mat_ket_noiKhông nhận được gì từ VPS — mất điện, đứt mạngNề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ẩmNâng cả hệ thốngMất kết nối
Phạm viđúng một thẻcả màn hìnhcả màn hình
Hiện gìthẻ đó mờ đi, ghi “Đang nâng cấp engine”, đếm số phút đã trôimàn hình bảo trì thay cả màn, đếm số phút đã trôibăng xám giữ nguyên màn hình, có nút “Thử lại”
Các thẻ khácbì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ệnhtắt riêng thẻ đókhông có màn hình để bấmtắt toàn bộ
Thông báo đẩykhông bắn cho thẻ đókhông bắnkhông bắn
Lịch sử lệnh của thẻgiữ nguyên, vẫn đọc đượcgiữ nguyêngiữ nguyên
Khi xongnhãn biến mất, thẻ có số trở lạimàn hình bảo trì biến mất, số nhảy về mới trong một lầnnhư nâng cả hệ thống
Giao dịch tự động11 thẻ
BạcSIZ26
Lệnh đang mở · Bán−$1.000
Sao chép lệnh
ĐồngHGZ26
Chờ bán51,9%
Đậu tươngZSX26
Chờ mua76,5%
Bình thường — không băng, không thêm chữ nào.
Giao dịch tự động11 thẻ
BạcSIZ26
Lệnh đang mở · Bán−$1.000
Sao chép lệnh
Đậu tươngZSX26
Chờ mua76,5%
Đang nâng cấp engine · 2 phút Tạm ngưng
Lúa mìZWZ26
Chờ bán30,8%
Nâng cấp một sản phẩm — chỉ thẻ đó đổi, các thẻ khác vẫn là số thật.
Đang nâng cấp hệ thống Bảng theo dõi tạm ngưng trong lúc cài bản mới. 4 phút Số liệu giữ ở lúc 15:35 · bản 1.3
Nâng cả hệ thống — màn hình bảo trì thay cả màn, như trên web.

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ữ

// 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

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ụcThay đổiApp cần làmHiệu lực
product_added, product_removedSự 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 10khi đội Algo báo đã lên VPS
ProductCard.zonesField MỚI trên mỗi thẻ của /v1/products, cùng khuôn trang chi tiếtBảng tổng đọc vùng từ danh sách, in lo–hi — mục 1109/10 11:39
product_updatedPhá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íaVùng chỉ chuyển hỗ trợ ↔ kháng cự khi một nến 15 phút đóng xuyên quaIn nguyên thứ tự server gửi09/10 12:13
position_closed.trade = nullLệnh vào sớm bị huỷXoá vị thế, không thêm lịch sử, không văng lỗiNay mới ghi
Stream ?since_id=Phát bù ngay trong kết nốiTuỳ chọn — thay bước gọi /v1/eventsNay mới ghi
GET /v1/healthKiểm sống, không cần keyTuỳ chọnNay mới ghi
Thông báo đẩyGhép layout với sự kiện; không đẩy trade_corrected — thay gợi ý cũ ở cuối mục nàyServer thông báo nghe stream — mục 12Không đổi server
Hợp đồng lệnh gieo ZW, ZCSử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ênKhông09/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ụcThay đổiApp 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 = closedNgoà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_positionHiện nhãn server trả. Thẻ đóng cửa vẫn đủ số, không phải unavailable
ProductCard.stale_sinceField MỚI — giờ feed đứt giữa phiên; null khi bình thườngHiện “Dữ liệu tạm dừng từ …”, nên tắt copy của thẻ
ProductCard.icon_urlField MỚI — hiện null cho mọi thẻDùng ảnh dự phòng khi null
Sự kiện product_updatedMỚ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ánTrong 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.125Không — dùng nguyên số server gửi
Mã trong lệnh copycontract = mã sàn, cqg_symbol = mã CQG — đã chốt, luôn có cả haiDù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ệnhKhô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ệnhTuỳ app: có thể ghi chú ở thẻ bạc
algo-socketMột tên miền nhận cả SSE /stream/signals lẫn WebSocket /ws/signalsKhông
KeyKey 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ácDùng key mới

Đã chốt

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ốngServer làm gìApp làm gì
Thêm sản phẩmPhá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_availableThê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ẩmPhát product_removed, data = {code}. Thẻ rời /v1/products, lệnh cũ rời /v1/tradesGỡ thẻ cùng vị thế và lịch sử của nó
Engine lỗi, mất feed, đang nâng cấpKhô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ó

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ụcThay đổiApp cần làm
ProductCard.zonesField 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_updatedPhá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[].priceKhô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

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ì

Ghép thông báo với sự kiện

Thông báoSự kiệnLấy gì để hiện
Vị thế mở mớiposition_openedTê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ốnposition_updated khi stop_price đổiSL 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ó tradetrade.exit_price, lãi/lỗ trade.pnl_usd, trade.exit_time
(không đẩy)position_closed với trade = nullLệ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

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?

Vì sao take_profit thường là null?

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.