Tài nguyên đang được tải lên... tải...

FMZ Quant Trading Platform Hướng dẫn truy cập giao thức tùy chỉnh

Tác giả:FMZ~Lydia, Tạo: 2024-11-08 16:37:25, Cập nhật: 2024-11-19 13:31:01

img

Chúng tôi FMZ Quant Trading Platform hỗ trợ nhiều sàn giao dịch tiền điện tử và đóng gói các sàn giao dịch chính trên thị trường. Tuy nhiên, vẫn còn nhiều sàn giao dịch không được đóng gói. Đối với người dùng cần sử dụng các sàn giao dịch này, họ có thể truy cập chúng thông qua FMZ Quant Custom Protocol. Không chỉ giới hạn trong sàn giao dịch tiền điện tử, bất kỳ nền tảng nào hỗ trợRESTgiao thức hoặcFIXgiao thức cũng có thể được truy cập.

Bài viết này sẽRESTgiao thức truy cập như một ví dụ để giải thích cách sử dụng giao thức tùy chỉnh của nền tảng giao dịch FMZ Quant để đóng gói và truy cập API của sàn giao dịch OKX. Trừ khi có quy định khác, bài viết này đề cập đến giao thức tùy chỉnh REST.

  • Dòng công việc của giao thức tùy chỉnh là: Quá trình yêu cầu: Trường hợp chiến lược chạy trên docker -> Chương trình giao thức tùy chỉnh -> Exchange API Quá trình phản hồi: Exchange API -> Chương trình giao thức tùy chỉnh -> Trường hợp chiến lược chạy trên docker

1. Thiết lập Exchange

Trang để cấu hình sàn giao dịch trên nền tảng giao dịch lượng tử FMZ:

https://www.fmz.com/m/platforms/add

img

  • Chọn giao thức: Chọn Protocol tùy chỉnh.
  • Địa chỉ dịch vụ: Chương trình giao thức tùy chỉnh về cơ bản là một dịch vụ RPC. Do đó, cần phải xác định rõ địa chỉ dịch vụ và cổng khi cấu hình trao đổi. Bằng cách này, docker biết nơi truy cập chương trình giao thức tùy chỉnh trong quá trìnhStrategy instance running on the docker -> Custom protocol program. Ví dụ:http://127.0.0.1:6666/OKX, chương trình giao thức tùy chỉnh và docker thường chạy trên cùng một thiết bị (server), vì vậy địa chỉ dịch vụ được viết là máy cục bộ (localhost), và cổng có thể được sử dụng như một cổng không bị chiếm đóng bởi hệ thống.
  • Chìa khóa truy cập: Thông tin cấu hình trao đổi được truyền qua trong quá trìnhStrategy instance running on the docker -> Custom protocol program.
  • Chìa khóa bí mật: Thông tin cấu hình trao đổi được truyền qua trong quá trìnhStrategy instance running on the docker -> Custom protocol program.
  • Tag: Nhãn của đối tượng trao đổi trên nền tảng giao dịch FMZ Quant, được sử dụng để xác định một đối tượng trao đổi nhất định.

Ảnh chụp màn hình của cấu hình OKX plug-in được tiết lộ trong bài viết là như sau:

img

OKX trao đổi thông tin cấu hình khóa bí mật:

accessKey:  accesskey123    // accesskey123, these are not actual keys, just for demonstration
secretKey:  secretkey123
passphrase: passphrase123

2. Việc triển khai Docker và chương trình giao thức tùy chỉnh (plugin)

    1. Docker Một docker phải được triển khai để chạy bất kỳ chiến lược nào trên nền tảng giao dịch FMZ Quant. Để biết chi tiết về cách triển khai một docker, vui lòng tham khảo hướng dẫn nền tảng. Nó sẽ không đi vào chi tiết ở đây.
    1. Chương trình giao thức tùy chỉnh (plugin) Docker và giao thức tùy chỉnh thường được triển khai trên cùng một thiết bị. Chương trình giao thức (dịch vụ) tùy chỉnh có thể được viết bằng bất kỳ ngôn ngữ nào. Bài viết này được viết bằng Python3. Cũng giống như chạy bất kỳ chương trình Python nào, bạn có thể thực hiện trực tiếp (làm các cấu hình khác nhau của môi trường Python trước). Tất nhiên, FMZ cũng hỗ trợ chạy các chương trình python, và bạn cũng có thể chạy giao thức tùy chỉnh này như một giao dịch trực tiếp để cung cấp cho nền tảng giao dịch FMZ Quant với hỗ trợ cho việc truy cập API trao đổi không đóng gói. Sau khi chương trình giao thức tùy chỉnh được chạy, nó bắt đầu nghe:http://127.0.0.1:6666Chương trình giao thức tùy chỉnh có thể xử lý các đường dẫn cụ thể, chẳng hạn như/OKX.

3. Chiến lược trường hợp yêu cầu FMZ API Function

Khi hàm API nền tảng (FMZ) được gọi trong chiến lược, chương trình giao thức tùy chỉnh sẽ nhận được yêu cầu từ docker. Bạn cũng có thể kiểm tra nó bằng cách sử dụng các công cụ gỡ lỗi của nền tảng, ví dụ:

Trang công cụ gỡ lỗi:

https://www.fmz.com/m/debug

function main() {
    return exchange.GetTicker("LTC_USDT")
}

Gọi hàmexchange.GetTicker(), chương trình giao thức tùy chỉnh nhận được yêu cầu:

POST /OKX HTTP/1.1 
{
    "access_key":"xxx",
    "method":"ticker",
    "nonce":1730275031047002000,
    "params":{"symbol":"LTC_USDT"},
    "secret_key":"xxx"
}
  • access_key: Chìa khóa trao đổi được cấu hình trong nền tảng Configure Exchange ở trên
  • secret_key: Chìa khóa trao đổi được cấu hình trong nền tảng Configure Exchange ở trên
  • phương pháp: liên quan đến giao diện gọi trong chiến lược, khi gọiexchange.GetTicker(), methodticker.
  • nonce: Thời gian khi yêu cầu xảy ra.
  • paramars: Các tham số liên quan đến cuộc gọi giao diện trong chiến lược, trong ví dụ này, khi gọiexchange.GetTicker(), các thông số liên quan là:{"symbol":"LTC_USDT"}.

4. Chương trình giao thức tùy chỉnh truy cập giao diện trao đổi

Khi chương trình giao thức tùy chỉnh nhận được yêu cầu từ docker, nó có thể lấy thông tin như chức năng API nền tảng (bao gồm thông tin tham số) được yêu cầu bởi chiến lược, khóa trao đổi, v.v. dựa trên thông tin được mang trong yêu cầu.

Dựa trên thông tin này, chương trình giao thức tùy chỉnh có thể truy cập giao diện trao đổi để lấy dữ liệu cần thiết hoặc thực hiện một số hoạt động nhất định.

Thông thường giao diện trao đổi có các phương thức như GET / POST / PUT / DELETE, được chia thành giao diện công cộng và giao diện riêng.

  • Giao diện công cộng: giao diện không yêu cầu xác minh chữ ký và được yêu cầu trực tiếp trong một chương trình giao thức tùy chỉnh.
  • Giao diện riêng: giao diện đòi hỏi xác minh chữ ký. chữ ký cần được thực hiện trong chương trình giao thức tùy chỉnh để yêu cầu giao diện API của các trao đổi này.

Chương trình giao thức tùy chỉnh nhận dữ liệu phản hồi từ giao diện trao đổi và xử lý thêm nó để xây dựng dữ liệu mong đợi của docker (được mô tả bên dưới).GetTicker, GetAccountvà các chức năng khác trong thực hiện lớp CustomProtocolOKX trong ví dụ giao thức tùy chỉnh Python.

5. Chương trình giao thức tùy chỉnh trả lời dữ liệu cho Docker

Khi chương trình giao thức tùy chỉnh truy cập vào giao diện API của trao đổi, thực hiện một số hoạt động nhất định hoặc thu thập một số dữ liệu nhất định, nó cần cung cấp dữ liệu cho docker.

Dữ liệu được cung cấp trở lại cho docker thay đổi tùy theo giao diện được gọi bởi chiến lược, và đầu tiên được chia thành hai loại:

  • Chương trình giao thức tùy chỉnh gọi giao diện trao đổi thành công:
{
    "data": null,  // "data" can be of any type 
    "raw": null    // "raw" can be of any type 
}

dữ liệu: Cấu trúc cụ thể của trường này liên quan đếnmethodtrong yêu cầu được nhận bởi chương trình giao thức tùy chỉnh, và được sử dụng để xây dựng cấu trúc dữ liệu cuối cùng được trả về bởi chức năng API nền tảng FMZ. Tất cả các giao diện sẽ được liệt kê dưới đây. raw: Trường này có thể được sử dụng để truyền dữ liệu thô của phản hồi giao diện API trao đổi, chẳng hạn như cấu trúc Ticker được trả về bởiexchange.GetTicker()Các lĩnh vực thông tin của cấu trúc Ticker ghi lại dữ liệu củarawlĩnh vực vàdataMột số chức năng API của nền tảng không cần dữ liệu này.

  • Chương trình giao thức tùy chỉnh không thể gọi giao diện trao đổi (lỗi kinh doanh, lỗi mạng, v.v.)
{
    "error": ""    // "error" contains an error message as a string
}

lỗi: thông tin lỗi, sẽ được hiển thị trong nhật ký lỗi trong khu vực nhật ký của nền tảng giao dịch trực tiếp (FMZ), công cụ gỡ lỗi và các trang khác.

Hiển thị dữ liệu phản hồi giao thức tùy chỉnh được nhận bởi chương trình chiến lược:

// Tested in the debugging tool of the FMZ platform
function main() {
    Log(exchange.GetTicker("USDT"))       // The trading pair is incomplete, the BaseCurrency part is missing, and the custom protocol plug-in is required to return an error message: {"error": "..."}
    Log(exchange.GetTicker("LTC_USDT"))
}

img

6. Thỏa thuận cấu trúc dữ liệu trong giao thức tùy chỉnh

Trên đây là một quá trình ngắn gọn của chương trình giao thức tùy chỉnh tham gia vào việc truy cập vào API trao đổi (FMZ unpackaged).exchange.GetTicker()Các chi tiết sau đây sẽ giải thích chi tiết tương tác của tất cả các chức năng API nền tảng chi tiết.

Nền tảng đóng gói các chức năng chung của các sàn giao dịch khác nhau và thống nhất chúng thành một chức năng nhất định, chẳng hạn như chức năng GetTicker, yêu cầu thông tin thị trường hiện tại của một sản phẩm nhất định. Đây là một API mà về cơ bản tất cả các sàn giao dịch đều có. Do đó, khi truy cập giao diện API được đóng gói bởi nền tảng trong một trường hợp chiến lược, docker sẽ gửi yêu cầu đến chương trình plug-in Custom Protocol (được đề cập ở trên):

POST /OKX HTTP/1.1 
{
    "access_key": "xxx",
    "method": "ticker",
    "nonce": 1730275031047002000,
    "params": {"symbol":"LTC_USDT"},
    "secret_key": "xxx"
}

Khi gọi các hàm API được đóng gói trên nền tảng FMZ khác nhau trong chiến lược (như GetTicker), định dạng yêu cầu được docker gửi đến giao thức tùy chỉnh cũng sẽ khác nhau.methodparams. Khi thiết kế một giao thức tùy chỉnh, thực hiện các hoạt động cụ thể theo nội dung của phương pháp. Sau đây là các kịch bản yêu cầu-phản ứng cho tất cả các giao diện.

Giao dịch tại chỗ

Ví dụ, cặp giao dịch hiện tại là:ETH_USDTDữ liệu mà docker mong đợi giao thức tùy chỉnh sẽ trả lời chủ yếu được viết trong trường dữ liệu, và một trường thô cũng có thể được thêm để ghi lại dữ liệu gốc của giao diện trao đổi.

  • GetTicker

trường phương pháp: ticker trường tham số:

{"symbol":"ETH_USDT"}

Dữ liệu mà docker mong đợi trong phản hồi giao thức tùy chỉnh:

{
    "data": {
        "symbol": "ETH_USDT",      // Corresponds to the Symbol field in the Ticker structure returned by the GetTicker function
        "buy": "2922.18",          // ...corresponds to the Buy field
        "sell": "2922.19", 
        "high": "2955", 
        "low": "2775.15", 
        "open": "2787.72", 
        "last": "2922.18", 
        "vol": "249400.888156", 
        "time": "1731028903911"
    },
    "raw": {}                      // A raw field can be added to record the raw data of the exchange API interface response
}
  • GetDepth

trường phương pháp: thần độ trường tham số:

{"limit":"30","symbol":"ETH_USDT"}

Dữ liệu mà docker mong đợi trong phản hồi giao thức tùy chỉnh:

{
    "data" : {
        "time" : 1500793319499,
        "asks" : [
            [1000, 0.5], [1001, 0.23], [1004, 2.1]
            // ... 
        ],
        "bids" : [
            [999, 0.25], [998, 0.8], [995, 1.4]
            // ... 
        ]
    }
}
  • GetTrades

trường phương pháp: trades trường tham số:

{"symbol":"eth_usdt"}

Dữ liệu mà docker mong đợi trong phản hồi giao thức tùy chỉnh:

{ 
    "data": [
        {
            "id": 12232153,
            "time" : 1529919412968,
            "price": 1000,
            "amount": 0.5,
            "type": "buy",             // "buy"、"sell"、"bid"、"ask"
        }, {
            "id": 12545664,
            "time" : 1529919412900,
            "price": 1001,
            "amount": 1,
            "type": "sell",
        }
        // ...
    ]
}
  • GetRecords

trường phương pháp: records trường tham số:

{
    "limit":"500",
    "period":"60",          // 60 minutes
    "symbol":"ETH_USDT"
}

Dữ liệu mà docker mong đợi trong phản hồi giao thức tùy chỉnh:

{
    "data": [
            // "Time":1500793319000,"Open":1.1,"High":2.2,"Low":3.3,"Close":4.4,"Volume":5.5
            [1500793319, 1.1, 2.2, 3.3, 4.4, 5.5],
            [1500793259, 1.01, 2.02, 3.03, 4.04, 5.05],
            // ...
    ]
}
  • GetMarketsPhải thực hiện

trường phương pháp: trường tham số:

{}

Dữ liệu mà docker mong đợi trong phản hồi giao thức tùy chỉnh:

{}
  • GetTickersPhải thực hiện

trường phương pháp: trường tham số:

{}

Dữ liệu mà docker mong đợi trong phản hồi giao thức tùy chỉnh:

{}
  • GetAccount

trường phương thức: số trường tham số:

{}

Dữ liệu mà docker mong đợi trong phản hồi giao thức tùy chỉnh:

{
    "data": [
        {"currency": "TUSD", "free": "3000", "frozen": "0"}, 
        {"currency": "BTC", "free": "0.2482982056277609", "frozen": "0"}, 
        // ...
    ]
}
  • GetAssets

trường phương pháp: tài sản trường tham số:

{}

Dữ liệu mà docker mong đợi trong phản hồi giao thức tùy chỉnh:

{
    "data": [
        {"currency": "TUSD", "free": "3000", "frozen": "0"},
        {"currency": "BTC", "free": "0.2482982056277609", "frozen": "0"}, 
        // ...
    ]
}
  • CreateOrder / Mua / Bán

trường phương pháp: giao dịch trường tham số:

{"amount":"0.1","price":"1000","symbol":"BTC_USDT","type":"buy"}

Dữ liệu mà docker mong đợi trong phản hồi giao thức tùy chỉnh:

{
    "data": {
        "id": "BTC-USDT,123456"
    }
}
  • GetOrders

trường phương pháp: lệnh trường tham số:

{"symbol":"ETH_USDT"}

Dữ liệu mà docker mong đợi trong phản hồi giao thức tùy chỉnh:

{
    "data": [
        {
            "id": "ETH-USDT,123456",
            "symbol": "ETH_USDT",
            "amount": 0.25,
            "price": 1005,
            "deal_amount": 0,
            "avg_price": "1000",
            "type": "buy",         // "buy"、"sell"
            "status": "pending",   // "pending", "pre-submitted", "submitting", "submitted", "partial-filled"
        }, 
        // ...
    ]
}
  • GetOrder

trường phương pháp: order trường tham số:

{
    "id":"ETH-USDT,123456",       // Calling in the strategy: exchange.GetOrder("ETH-USDT,123456")
    "symbol":"ETH_USDT"
}

Dữ liệu mà docker mong đợi trong phản hồi giao thức tùy chỉnh:

{ 
    "data": {
        "id": "ETH-USDT,123456",
        "symbol": "ETH_USDT"
        "amount": 0.15,
        "price": 1002,
        "status": "pending",    // "pending", "pre-submitted", "submitting", "submitted", "partial-filled", "filled", "closed", "finished", "partial-canceled", "canceled"
        "deal_amount": 0,
        "type": "buy",          // "buy"、"sell"
        "avg_price": 0,         // If the exchange does not provide it, it can be assigned a value of 0 during processing.
    }
}
  • GetHistoryOrders

trường phương thức: historyorders trường tham số:

{"limit":0,"since":0,"symbol":"ETH_USDT"}

Dữ liệu mà docker mong đợi trong phản hồi giao thức tùy chỉnh:

{
    "data": [
        {
            "id": "ETH-USDT,123456",
            "symbol": "ETH_USDT",
            "amount": 0.25,
            "price": 1005,
            "deal_amount": 0,
            "avg_price": 1000,
            "type": "buy",       // "buy"、"sell"
            "status": "filled",  // "filled"
        }, 
        // ...
    ]
}
  • Hủy lệnh

trường phương thức: hoãn trường tham số:

{"id":"ETH-USDT,123456","symbol":"ETH_USDT"}

Dữ liệu mà docker mong đợi trong phản hồi giao thức tùy chỉnh:

{
    "data": true    // As long as there is no error field in the JSON, the order cancellation is considered successful by default.
}
  • IO

Cácexchange.IOchức năng được sử dụng để truy cập giao diện trao đổi trực tiếp.GET /api/v5/trade/orders-pending, parameters: instType=SPOT, instId=ETH-USDTví dụ.

// Called in the strategy instance
exchange.IO("api", "GET", "/api/v5/trade/orders-pending", "instType=SPOT&instId=ETH-USDT")

trường phương pháp:"__api_/api/v5/trade/orders-pending", trường phương thức bắt đầu với _nấm, cho thấy rằng điều này được kích hoạt bởiexchange.IOgọi hàm trong phiên bản chiến lược. trường tham số:

{"instId":"ETH-USDT","instType":"SPOT"}   // instType=SPOT&instId=ETH-USDT encoded parameters will be restored to JSON

Dữ liệu mà docker mong đợi trong phản hồi giao thức tùy chỉnh:

{
    "data": {"code": "0", "data": [], "msg": ""}    // The data attribute value is the data of the exchange API: GET /api/v5/trade/orders-pending response
}
  • Các loại khác Các chức năng API nền tảng FMZ khác được sử dụng trong các ví dụ chiến lược, chẳng hạn như:exchange.Go(), exchange.GetRawJSON()và các chức năng khác không cần phải được đóng gói, và phương pháp gọi và chức năng vẫn không thay đổi.

Giao dịch tương lai

Ngoài việc hỗ trợ tất cả các chức năng của sàn giao dịch tại chỗ, sàn giao dịch tương lai cũng có một số chức năng API độc đáo cho sàn giao dịch tương lai.

Phải thực hiện

  • GetPositions
  • SetMarginLevel
  • GetFundings

Ví dụ giao thức tùy chỉnh trong phiên bản Python

REST Custom Protocol - Truy cập vào giao diện OKX Exchange REST API, được đóng gói dưới dạng đối tượng trao đổi tại chỗ. Thực hiện một yêu cầu giao diện công cộng và đáp ứng dữ liệu đóng gói. Thực hiện một chữ ký giao diện riêng, yêu cầu và phản hồi dữ liệu đóng gói. Ví dụ này chủ yếu là để thử nghiệm và học tập. Các giao diện khác sử dụng dữ liệu mô phỏng để phản hồi trực tiếp với docker để thử nghiệm.

import http.server
import socketserver
import json
import urllib.request
import urllib.error
import argparse
import ssl
import hmac
import hashlib
import base64

from datetime import datetime

ssl._create_default_https_context = ssl._create_unverified_context

class BaseProtocol:
    ERR_NOT_SUPPORT = {"error": "not support"}

    def __init__(self, apiBase, accessKey, secretKey):
        self._apiBase = apiBase
        self._accessKey = accessKey
        self._secretKey = secretKey


    def _httpRequest(self, method, path, query="", params={}, addHeaders={}):
        headers = {
            'User-Agent': 'Mozilla/5.0 (Windows; U; Windows NT 6.1; en-US; rv:1.9.1.6) Gecko/20091201 Firefox/3.5.6', 
            'Content-Type': 'application/json; charset=UTF-8'
        }

        # add headers
        for key in addHeaders:
            headers[key] = addHeaders[key]

        if method == "GET":
            url = f"{self._apiBase}{path}?{query}" if query != "" else f"{self._apiBase}{path}"
            req = urllib.request.Request(url, method=method, headers=headers)
        else:
            url = f"{self._apiBase}{path}"
            req = urllib.request.Request(url, json.dumps(params, separators=(',', ':')).encode('utf-8'), method=method, headers=headers)
        
        print(f'send request by protocol: {self.exName}, req:', req.method, req.full_url, req.headers, req.data, "\n")

        try:
            with urllib.request.urlopen(req) as resp:
                data = json.loads(resp.read())
        except json.JSONDecodeError:
            data = {"error": "Invalid JSON response"}
        except urllib.error.HTTPError as e:
            data = {"error": f"HTTP error: {e.code}"}
        except urllib.error.URLError as e:
            data = {"error": f"URL error: {e.reason}"}
        except Exception as e:
            data = {"error": f"Exception occurred: {str(e)}"}

        print(f'protocol response received: {self.exName}, resp:', data, "\n")

        return data
    

    def GetTickers(self):
        return self.ERR_NOT_SUPPORT


    def GetMarkets(self):
        return self.ERR_NOT_SUPPORT


    def GetTicker(self, symbol):
        return self.ERR_NOT_SUPPORT


    def GetDepth(self, symbol=""):
        return self.ERR_NOT_SUPPORT


    def GetTrades(self, symbol=""):
        return self.ERR_NOT_SUPPORT


    def GetRecords(self, symbol, period, limit):
        return self.ERR_NOT_SUPPORT


    def GetAssets(self):
        return self.ERR_NOT_SUPPORT


    def GetAccount(self):
        return self.ERR_NOT_SUPPORT


    def CreateOrder(self, symbol, side, price, amount):
        return self.ERR_NOT_SUPPORT


    def GetOrders(self, symbol=""):
        return self.ERR_NOT_SUPPORT


    def GetOrder(self, orderId):
        return self.ERR_NOT_SUPPORT


    def CancelOrder(self, orderId):
        return self.ERR_NOT_SUPPORT


    def GetHistoryOrders(self, symbol, since, limit):
        return self.ERR_NOT_SUPPORT


    def GetPostions(self, symbol=""):
        return self.ERR_NOT_SUPPORT


    def SetMarginLevel(self, symbol, marginLevel):
        return self.ERR_NOT_SUPPORT


    def GetFundings(self, symbol=""):
        return self.ERR_NOT_SUPPORT


    def IO(self, params):
        return self.ERR_NOT_SUPPORT


class ProtocolFactory:
    @staticmethod
    def createExWrapper(apiBase, accessKey, secretKey, exName) -> BaseProtocol:
        if exName == "OKX":
            return CustomProtocolOKX(apiBase, accessKey, secretKey, exName)
        else:
            raise ValueError(f'Unknown exName: {exName}')


class CustomProtocolOKX(BaseProtocol):
    """
    CustomProtocolOKX - OKX API Wrapper

    # TODO: add information.
    """

    def __init__(self, apiBase, accessKey, secretKey, exName):
        secretKeyList = secretKey.split(",")
        self.exName = exName
        self._x_simulated_trading = 0
        if len(secretKeyList) > 1:
            self._passphrase = secretKeyList[1]
            if len(secretKeyList) > 2:
                if secretKeyList[2] == "simulate":
                    self._x_simulated_trading = 1
        else:
            raise ValueError(f"{self.exName}: invalid secretKey format.")
        super().__init__(apiBase, accessKey, secretKeyList[0])


    def getCurrencys(self, symbol):
        baseCurrency, quoteCurrency = "", ""
        arrCurrency = symbol.split("_")
        if len(arrCurrency) == 2:
            baseCurrency = arrCurrency[0]
            quoteCurrency = arrCurrency[1]
        return baseCurrency, quoteCurrency


    def getSymbol(self, instrument):
        arrCurrency = instrument.split("-")
        if len(arrCurrency) == 2:
            baseCurrency = arrCurrency[0]
            quoteCurrency = arrCurrency[1]
        else:
            raise ValueError(f"{self.exName}: invalid instrument: {instrument}")
        return f'{baseCurrency}_{quoteCurrency}'


    def callUnsignedAPI(self, httpMethod, path, query="", params={}):
        return self._httpRequest(httpMethod, path, query, params)


    def callSignedAPI(self, httpMethod, path, query="", params={}):
        strTime = datetime.utcnow().strftime('%Y-%m-%dT%H:%M:%S.%f')[:-3] + 'Z'
        jsonStr = json.dumps(params, separators=(',', ':')) if len(params) > 0 else ""
        message = f'{strTime}{httpMethod}{path}{jsonStr}'
        if httpMethod == "GET" and query != "":
            message = f'{strTime}{httpMethod}{path}?{query}{jsonStr}'
        mac = hmac.new(bytes(self._secretKey, encoding='utf8'), bytes(message, encoding='utf-8'), digestmod='sha256')
        signature = base64.b64encode(mac.digest())
        
        headers = {}
        if self._x_simulated_trading == 1:
            headers["x-simulated-trading"] = str(self._x_simulated_trading)
        headers["OK-ACCESS-KEY"] = self._accessKey
        headers["OK-ACCESS-PASSPHRASE"] = self._passphrase
        headers["OK-ACCESS-TIMESTAMP"] = strTime        
        headers["OK-ACCESS-SIGN"] = signature
        return self._httpRequest(httpMethod, path, query, params, headers)

    
    # Encapsulates requests to the exchange API.
    def GetTicker(self, symbol):
        """
        GET /api/v5/market/ticker , param: instId 
        """

        baseCurrency, quoteCurrency = self.getCurrencys(symbol)
        if baseCurrency == "" or quoteCurrency == "":
            return {"error": "invalid symbol"}

        path = "/api/v5/market/ticker"
        query = f'instId={baseCurrency}-{quoteCurrency}'
        data = self.callUnsignedAPI("GET", path, query=query)
        if "error" in data.keys() and "data" not in data.keys():
            return data

        ret_data = {}
        if data["code"] != "0" or not isinstance(data["data"], list):
            return {"error": json.dumps(data, ensure_ascii=False)}
        for tick in data["data"]:
            if not all(k in tick for k in ("instId", "bidPx", "askPx", "high24h", "low24h", "vol24h", "ts")):
                return {"error": json.dumps(data, ensure_ascii=False)}

            ret_data["symbol"] = self.getSymbol(tick["instId"])
            ret_data["buy"] = tick["bidPx"]
            ret_data["sell"] = tick["askPx"]
            ret_data["high"] = tick["high24h"]
            ret_data["low"] = tick["low24h"]
            ret_data["open"] = tick["open24h"]
            ret_data["last"] = tick["last"]
            ret_data["vol"] = tick["vol24h"]
            ret_data["time"] = tick["ts"]

        return {"data": ret_data, "raw": data}


    def GetDepth(self, symbol):
        """
        TODO: Implementation code
        """
        
        # Mock data for testing.
        ret_data = {            
            "time" : 1500793319499,
            "asks" : [
                [1000, 0.5], [1001, 0.23], [1004, 2.1]
            ],
            "bids" : [
                [999, 0.25], [998, 0.8], [995, 1.4]
            ]            
        }
        
        return {"data": ret_data}


    def GetTrades(self, symbol):
        """
        TODO: Implementation code
        """

        # Mock data for testing.
        ret_data = [
            {
                "id": 12232153,
                "time" : 1529919412968,
                "price": 1000,
                "amount": 0.5,
                "type": "buy",
            }, {
                "id": 12545664,
                "time" : 1529919412900,
                "price": 1001,
                "amount": 1,
                "type": "sell",
            }
        ]

        return {"data": ret_data}


    def GetRecords(self, symbol, period, limit):
        """
        TODO: Implementation code
        """

        # Mock data for testing.
        ret_data = [
            [1500793319, 1.1, 2.2, 3.3, 4.4, 5.5],
            [1500793259, 1.01, 2.02, 3.03, 4.04, 5.05],
        ]

        return {"data": ret_data}


    def GetMarkets(self):
        """
        TODO: Implementation code
        """

        ret_data = {}

        return {"data": ret_data}


    def GetTickers(self):
        """
        TODO: Implementation code
        """

        ret_data = {}

        return {"data": ret_data}


    def GetAccount(self):
        """
        GET /api/v5/account/balance
        """

        path = "/api/v5/account/balance"
        data = self.callSignedAPI("GET", path)

        ret_data = []
        if data["code"] != "0" or "data" not in data or not isinstance(data["data"], list):
            return {"error": json.dumps(data, ensure_ascii=False)}
        for ele in data["data"]:
            if "details" not in ele or not isinstance(ele["details"], list):
                return {"error": json.dumps(data, ensure_ascii=False)}
            for detail in ele["details"]:
                asset = {"currency": detail["ccy"], "free": detail["availEq"], "frozen": detail["ordFrozen"]}
                if detail["availEq"] == "":
                    asset["free"] = detail["availBal"]
                ret_data.append(asset)
        return {"data": ret_data, "raw": data}


    def GetAssets(self):
        """
        TODO: Implementation code
        """
        
        # Mock data for testing.
        ret_data = [
            {"currency": "TUSD", "free": "3000", "frozen": "0"},
            {"currency": "BTC", "free": "0.2482982056277609", "frozen": "0"}
        ]

        return {"data": ret_data}


    def CreateOrder(self, symbol, side, price, amount):
        """
        TODO: Implementation code
        """
        
        # Mock data for testing.
        ret_data = {
            "id": "BTC-USDT,123456"
        }

        return {"data": ret_data}

    
    def GetOrders(self, symbol):
        """
        GET /api/v5/trade/orders-pending  instType SPOT instId  after limit
        """
        
        baseCurrency, quoteCurrency = self.getCurrencys(symbol)
        if baseCurrency == "" or quoteCurrency == "":
            return {"error": "invalid symbol"}

        path = "/api/v5/trade/orders-pending"
        after = ""
        limit = 100

        ret_data = []
        while True:
            query = f"instType=SPOT&instId={baseCurrency}-{quoteCurrency}&limit={limit}"
            if after != "":
                query = f"instType=SPOT&instId={baseCurrency}-{quoteCurrency}&limit={limit}&after={after}"
        
            data = self.callSignedAPI("GET", path, query=query)
            
            if data["code"] != "0" or not isinstance(data["data"], list):
                return {"error": json.dumps(data, ensure_ascii=False)}
            for ele in data["data"]:
                order = {}

                order["id"] = f'{ele["instId"]},{ele["ordId"]}'
                order["symbol"] = f'{baseCurrency}-{quoteCurrency}'
                order["amount"] = ele["sz"]
                order["price"] = ele["px"]
                order["deal_amount"] = ele["accFillSz"]
                order["avg_price"] = 0 if ele["avgPx"] == "" else ele["avgPx"]
                order["type"] = "buy" if ele["side"] == "buy" else "sell"
                order["state"] = "pending"

                ret_data.append(order)
                after = ele["ordId"]

            if len(data["data"]) < limit:
                break

        return {"data": ret_data}


    def GetOrder(self, orderId):
        """
        TODO: Implementation code
        """
        
        # Mock data for testing.
        ret_data = {
            "id": "ETH-USDT,123456",
            "symbol": "ETH_USDT",
            "amount": 0.15,
            "price": 1002,
            "status": "pending",
            "deal_amount": 0,
            "type": "buy",
            "avg_price": 0,
        }

        return {"data": ret_data}


    def GetHistoryOrders(self, symbol, since, limit):
        """
        TODO: Implementation code
        """

        # Mock data for testing.
        ret_data = [
            {
                "id": "ETH-USDT,123456",
                "symbol": "ETH_USDT",
                "amount": 0.25,
                "price": 1005,
                "deal_amount": 0,
                "avg_price": 1000,
                "type": "buy",
                "status": "filled"
            }
        ]

        return {"data": ret_data}


    def CancelOrder(self, orderId):
        """
        TODO: Implementation code
        """

        # Mock data for testing.
        ret_data = True

        return {"data": ret_data}


    def IO(self, httpMethod, path, params={}):
        if httpMethod == "GET":
            query = urllib.parse.urlencode(params)
            data = self.callSignedAPI(httpMethod, path, query=query)
        else:
            data = self.callSignedAPI(httpMethod, path, params=params)
        
        if data["code"] != "0":
            return {"error": json.dumps(data, ensure_ascii=False)}

        return {"data": data}


class HttpServer(http.server.SimpleHTTPRequestHandler):
    def __init__(self, *args, **kwargs):
        self.request_body = None
        self.request_path = None
        super().__init__(*args, **kwargs)


    def log_message(self, format, *args):
        return 


    def _sendResponse(self, body):
        self.send_response(200)
        self.send_header('Content-type', 'application/json; charset=utf-8')
        self.end_headers()
        self.wfile.write(json.dumps(body).encode('utf-8'))


    def do_GET(self):
        # The FMZ.COM custom protocol only send GET method request
        self._sendResponse({"error": "not support GET method."})


    def do_POST(self):
        """
        Returns:
            json: success, {"data": ...}
            json: error,   {"error": ...}
        """

        contentLen = int(self.headers['Content-Length'])
        self.request_body = self.rfile.read(contentLen)
        self.request_path = self.path
        exName = self.request_path.lstrip("/")

        # Print the request received from the FMZ.COM robot
        print(f"--------- request received from the FMZ.COM robot: --------- \n {self.requestline} | Body: {self.request_body} | Headers: {self.headers} \n")

        try:
            data = json.loads(self.request_body)
        except json.JSONDecodeError:
            data = {"error": self.request_body.decode('utf-8')}
            self._sendResponse(data)
            return 

        # fault tolerant
        if not all(k in data for k in ("access_key", "secret_key", "method", "params")):
            data = {"error": "missing required parameters"}
            self._sendResponse(data)
            return

        respData = {}
        accessKey = data["access_key"]
        secretKey = data["secret_key"]
        method = data["method"]
        params = data["params"]
        exchange = ProtocolFactory.createExWrapper("https://www.okx.com", accessKey, secretKey, exName)

        if method == "ticker":
            symbol = str(params["symbol"]).upper()
            respData = exchange.GetTicker(symbol)
        elif method == "depth":
            symbol = str(params["symbol"]).upper()
            respData = exchange.GetDepth(symbol)
        elif method == "trades":
            symbol = str(params["symbol"]).upper()
            respData = exchange.GetTrades(symbol)
        elif method == "records":
            symbol = str(params["symbol"]).upper()
            period = int(params["period"])
            limit = int(params["limit"])
            respData = exchange.GetRecords(symbol, period, limit)
        elif method == "accounts":
            respData = exchange.GetAccount()
        elif method == "assets":
            respData = exchange.GetAssets()
        elif method == "trade":
            amount = float(params["amount"])
            price = float(params["price"])
            symbol = str(params["symbol"])
            tradeType = str(params["type"])
            respData = exchange.CreateOrder(symbol, tradeType, price, amount)
        elif method == "orders":
            symbol = str(params["symbol"]).upper()
            respData = exchange.GetOrders(symbol)
        elif method == "order":
            orderId = str(params["id"])
            respData = exchange.GetOrder(orderId)
        elif method == "historyorders":
            symbol = str(params["symbol"])
            since = int(params["since"])
            limit = int(params["limit"])
            respData = exchange.GetHistoryOrders(symbol, since, limit)
        elif method == "cancel":
            orderId = str(params["id"])
            respData = exchange.CancelOrder(orderId)
        elif method[:6] == "__api_":
            respData = exchange.IO(self.headers["Http-Method"], method[6:], params)
        else:
            respData = {"error": f'invalid method: {method}'}

        # Print the response to send to FMZ.COM robot
        print(f"response to send to FMZ.COM robot: {respData} \n")

        self._sendResponse(respData)


if __name__ == "__main__":
    parser = argparse.ArgumentParser(description="Run a FMZ.COM custom protocol plugin.")
    parser.add_argument("--port", type=int, default=6666, help="Port to run the server on.")
    parser.add_argument("--address", type=str, default="localhost", help="Address to bind the server to.")
    args = parser.parse_args() 

    with socketserver.TCPServer((args.address, args.port), HttpServer) as httpd:
        print(f"running... {args.address}:{args.port}", "\n")
        httpd.serve_forever()

Thêm nữa