4
Подписаться
1271
Подписчики

Руководство по общему протоколу доступа к платформе количественной торговли Inventor

Создано: 2024-10-29 14:37:56, Обновлено: 2024-11-12 21:58:55
comments   0
hits   865

[TOC]

Руководство по общему протоколу доступа к платформе количественной торговли Inventor

Количественная торговая платформа Inventor поддерживает множество криптовалютных бирж и объединяет основные биржи на рынке. Однако все еще есть много бирж, которые не упакованы. Для пользователей, которым необходимо использовать эти биржи, они могут получить к ним доступ через универсальный протокол, квантифицированный изобретателем. Не ограничиваясь криптовалютными биржами, любыеRESTСоглашение илиFIXПлатформа соглашения также доступна.

Эта статья будетRESTНа примере доступа к протоколу объясняется, как использовать общий протокол платформы количественной торговли Inventor для инкапсуляции и доступа к API биржи OKX. Если не указано иное, в данной статье рассматривается общий протокол REST.

  • Рабочий процесс общего протокола следующий: Процесс запроса: Экземпляр стратегии, запущенный на кастодиане -> Программа общего протокола -> API обмена Процесс ответа: API обмена -> Программа общего протокола -> Экземпляр стратегии, запущенный на хранителе

1. Настройте обмен

Страница настройки биржи на платформе количественной торговли Inventor:

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

Руководство по общему протоколу доступа к платформе количественной торговли Inventor

  • Выберите протокол: Выберите «Общий протокол».
  • Адрес службы: Программа общего протокола по сути является службой RPC. Поэтому при настройке обмена необходимо четко указывать адрес и порт сервиса. Так что в托管者上运行的策略实例 -> 通用协议程序В ходе этого процесса хост знает, где получить доступ к программе общего протокола. Например:http://127.0.0.1:6666/OKXОбычно программа общего протокола и хост запускаются на одном и том же устройстве (сервере), поэтому адрес службы записывается как локальный компьютер (localhost), а порт может быть портом, который не занят системой.
  • Access Key: 托管者上运行的策略实例 -> 通用协议程序Информация о конфигурации обмена, передаваемая в ходе процесса.
  • Secret Key: 托管者上运行的策略实例 -> 通用协议程序Информация о конфигурации обмена, передаваемая в ходе процесса.
  • Этикетка: Метка объекта обмена на платформе количественной торговли Inventor используется для идентификации определенного объекта обмена.

Скриншот конфигурации плагина OKX, раскрытый в статье, выглядит следующим образом:

Руководство по общему протоколу доступа к платформе количественной торговли Inventor

Информация о конфигурации секретного ключа обмена OKX:

accessKey:  accesskey123    // accesskey123 这些并不是实际秘钥,仅仅是演示
secretKey:  secretkey123
passphrase: passphrase123

2. Развертывание программы-хранителя и универсального протокола (плагина)

    1. Хост Для запуска любой стратегии в реальном времени на платформе количественной торговли Inventor необходимо развернуть кастодиана. Для конкретного развертывания кастодиана, пожалуйста, обратитесь к руководству по платформе, которое не будет здесь повторяться.
    1. Общая протокольная программа (плагин) Хост и универсальный протокол обычно развертываются на одном устройстве. Программа универсального протокола (сервиса) может быть написана на любом языке. Эта статья написана на Python3. Как и любую программу Python, вы можете выполнить ее напрямую (заранее настроив различные конфигурации среды Python). Конечно, FMZ также поддерживает запуск программ Python, и этот общий протокол может быть запущен как настоящий диск, чтобы предоставить количественной торговой платформе изобретателя поддержку неупакованного доступа к API биржи. После запуска программы общего протокола начните мониторинг:http://127.0.0.1:6666,В программе общего протокола можно указать конкретные пути, например/OKXдля обработки.

3. Экземпляр стратегии запрашивает функцию API FMZ

При вызове функции API платформы (FMZ) в стратегии программа универсального протокола получает запрос от кастодиана. Вы также можете провести тестирование, используя инструменты отладки платформы, например:

Страница инструментов отладки:

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

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

Вызовexchange.GetTicker()Функция, программа общего протокола получает запрос:

POST /OKX HTTP/1.1 
{
    "access_key":"xxx",
    "method":"ticker",
    "nonce":1730275031047002000,
    "params":{"symbol":"LTC_USDT"},
    "secret_key":"xxx"
}
  • access_key: ключ обмена, настроенный на платформе «Настройка обмена» выше
  • secret_key: Ключ обмена, настроенный на платформе «Настройка обмена» выше
  • метод: связанный с вызывающим интерфейсом в стратегии, вызывающийexchange.GetTicker()час,methodТо естьticker
  • nonce: временная метка, когда произошел запрос.
  • params: Параметры, относящиеся к вызову интерфейса в политике.exchange.GetTicker()При вызове соответствующими параметрами являются:{"symbol":"LTC_USDT"}

4. Общий протокол доступа программы к интерфейсу обмена

Когда программа общего протокола получает запрос от кастодиана, она может получить такую ​​информацию, как функция API платформы (включая информацию о параметрах), запрошенную стратегией, ключ обмена и т. д., на основе информации, содержащейся в запросе.

На основе этой информации программа общего протокола может получить доступ к интерфейсу обмена для получения необходимых данных или выполнения определенных операций.

Обычно интерфейс обмена имеет такие методы, как GET/POST/PUT/DELETE, которые делятся на публичный интерфейс и приватный интерфейс.

  • Открытый интерфейс: интерфейс, не требующий проверки подписи и напрямую запрашиваемый в программе общего протокола.
  • Частный интерфейс: интерфейс, требующий проверки подписи. Подпись должна быть реализована в общей программе протокола для запроса интерфейса API этих бирж.

Программа общего протокола получает данные ответа интерфейса обмена, обрабатывает их и преобразует в данные, ожидаемые кастодианом (описано ниже). См. пример реализации класса CustomProtocolOKX в общем протоколе обмена OKX.GetTickerGetAccountИ другие функции.

5. Программа общего протокола отправляет данные хранителю.

Когда программа общего протокола обращается к API-интерфейсу биржи, выполняет определенные операции или получает определенные данные, ей необходимо передать результаты обратно кастодиану.

Данные, возвращаемые кастодиану, различаются в зависимости от интерфейса, вызываемого стратегией, и сначала делятся на две категории:

  • Программа общего протокола успешно вызывает интерфейс обмена:
  {
      "data": null,  // "data" can be of any type 
      "raw": null    // "raw" can be of any type 
  }
  • данные: Конкретная структура этого поля такая же, как и в запросе, полученном программой общего протокола.methodНиже приведен список всех интерфейсов, используемых для построения структуры данных, возвращаемой функцией API платформы FMZ.

  • Raw: Это поле можно использовать для передачи необработанных данных ответа API обмена, напримерexchange.GetTicker()Структура Ticker, возвращаемая функцией, содержит следующую информацию, записанную в поле Info структуры Ticker:rawПоля иdataДанные поля; некоторым функциям API платформы эти данные не нужны.

  • Программа общего протокола не смогла вызвать интерфейс обмена (бизнес-ошибка, сетевая ошибка и т. д.)

  {
      "error": ""    // "error" contains an error message as a string
  }
  • ошибка: информация об ошибке, которая будет отображаться в журнале ошибок в области журнала реального диска платформы (FMZ), инструмента отладки и других страниц.

Демонстрирует общие данные ответа протокола, полученные программой политики:

// FMZ平台的调试工具中测试
function main() {
    Log(exchange.GetTicker("USDT"))       // 交易对不完整,缺少BaseCurrency部分,需要通用协议插件程序返回报错信息: {"error": "..."}
    Log(exchange.GetTicker("LTC_USDT"))
}

Руководство по общему протоколу доступа к платформе количественной торговли Inventor

6. Согласование структуры данных в общем протоколе

Выше представлен краткий процесс того, как программа общего протокола участвует в доступе к API обмена (FMZ unpackaged). Этот процесс только объясняет, как вызвать инструмент отладки платформы (FMZ).exchange.GetTicker()Функциональный процесс. Далее будут подробно объяснены детали взаимодействия всех функций API платформы.

Платформа инкапсулирует общие функции различных бирж и объединяет их в определенную функцию, например, функцию GetTicker, которая запрашивает текущую рыночную информацию определенного продукта. Это по сути API, который есть у всех бирж. Таким образом, когда в экземпляре стратегии осуществляется доступ к API-интерфейсу, инкапсулированному платформой, кастодиан отправляет запрос подключаемому модулю «Universal Protocol» (упомянутому выше):

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

При вызове различных инкапсулированных функций API платформы Inventor в стратегии (например, GetTicker) формат запроса, отправляемого кастодианом в общий протокол, также будет отличаться. Данные (JSON) в теле отличаются толькоmethodиparams. При разработке общего протоколаmethodВы можете выполнять определенные операции на основе содержимого. Ниже приведены сценарии запроса-ответа для всех интерфейсов.

Спотовый обмен

Например, текущая торговая пара:ETH_USDT, Я не буду вдаваться в подробности позже. Данные, на которые, по мнению кастодиана, должен отреагировать общий протокол, в основном записываются в поле данных, а также может быть добавлено необработанное поле для записи исходных данных интерфейса обмена.

  • GetTicker

    • поле метода: “тикер”
    • поле параметров:
    {"symbol":"ETH_USDT"}
    
    • Данные, которые хост ожидает в ответе общего протокола:
    {
        "data": {
            "symbol": "ETH_USDT",      // 对应GetTicker函数返回的Ticker结构中的Symbol字段
            "buy": "2922.18",          // ...对应Buy字段
            "sell": "2922.19", 
            "high": "2955", 
            "low": "2775.15", 
            "open": "2787.72", 
            "last": "2922.18", 
            "vol": "249400.888156", 
            "time": "1731028903911"
        },
        "raw": {}                      // 可以增加一个raw字段记录交易所API接口应答的原始数据
    }
    
  • GetDepth

    • поле метода: “глубина”
    • поле параметров:
    {"limit":"30","symbol":"ETH_USDT"}
    
    • Данные, которые хост ожидает в ответе общего протокола:
    {
        "data" : {
            "time" : 1500793319499,
            "asks" : [
                [1000, 0.5], [1001, 0.23], [1004, 2.1]
                // ... 
            ],
            "bids" : [
                [999, 0.25], [998, 0.8], [995, 1.4]
                // ... 
            ]
        }
    }
    
  • GetTrades

    • поле метода: “торги”
    • поле параметров:
    {"symbol":"eth_usdt"}
    
    • Данные, которые хост ожидает в ответе общего протокола:
    { 
        "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

    • поле метода: “записи”
    • поле параметров:
    {
        "limit":"500",
        "period":"60",          // 60分钟
        "symbol":"ETH_USDT"
    }
    
    • Данные, которые хост ожидает в ответе общего протокола:
    {
        "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],
                // ...
        ]
    }
    
  • GetMarkets Будет реализовано

    • поле метода: “”
    • поле параметров:
    {}
    
    • Данные, которые хост ожидает в ответе общего протокола:
    {}
    
  • GetTickers Будет реализовано

    • поле метода: “”
    • поле параметров:
    {}
    
    • Данные, которые хост ожидает в ответе общего протокола:
    {}
    
  • GetAccount

    • поле метода: “счета”
    • поле параметров:
    {}
    
    • Данные, которые хост ожидает в ответе общего протокола:
    {
        "data": [
            {"currency": "TUSD", "free": "3000", "frozen": "0"}, 
            {"currency": "BTC", "free": "0.2482982056277609", "frozen": "0"}, 
            // ...
        ]
    }
    
  • GetAssets

    • поле метода: “активы”
    • поле параметров:
    {}
    
    • Данные, которые хост ожидает в ответе общего протокола:
    {
        "data": [
            {"currency": "TUSD", "free": "3000", "frozen": "0"},
            {"currency": "BTC", "free": "0.2482982056277609", "frozen": "0"}, 
            // ...
        ]
    }
    
  • CreateOrder / Buy / Sell

    • поле метода: “торговля”
    • поле параметров:
    {"amount":"0.1","price":"1000","symbol":"BTC_USDT","type":"buy"}
    
    • Данные, которые хост ожидает в ответе общего протокола:
    {
        "data": {
            "id": "BTC-USDT,123456"
        }
    }
    
  • GetOrders

    • поле метода: “заказы”
    • поле параметров:
    {"symbol":"ETH_USDT"}
    
    • Данные, которые хост ожидает в ответе общего протокола:
    {
        "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

    • поле метода: “заказ”
    • поле параметров:
    {
        "id":"ETH-USDT,123456",       // 策略中调用:exchange.GetOrder("ETH-USDT,123456")
        "symbol":"ETH_USDT"
    }
    
    • Данные, которые хост ожидает в ответе общего протокола:
    { 
        "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,         // 如果交易所没有提供,在处理时可以赋值为0
        }
    }
    
  • GetHistoryOrders

    • поле метода: “historyorders”
    • поле параметров:
    {"limit":0,"since":0,"symbol":"ETH_USDT"}
    
    • Данные, которые хост ожидает в ответе общего протокола:
    {
        "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"
            }, 
            // ...
        ]
    }
    
  • CancelOrder

    • поле метода: «отмена»
    • поле параметров:
    {"id":"ETH-USDT,123456","symbol":"ETH_USDT"}
    
    • Данные, которые хост ожидает в ответе общего протокола:
    {
        "data": true    // 只要该JSON中没有error字段,都默认为撤单成功
    }
    
  • IO

Функция exchange.IO используется для прямого доступа к интерфейсу обмена. Например, мы используемGET /api/v5/trade/orders-pending, 参数:instType=SPOT,instId=ETH-USDTНапример.

  // 策略实例中调用
  exchange.IO("api", "GET", "/api/v5/trade/orders-pending", "instType=SPOT&instId=ETH-USDT")
  • поле метода:"__api_/api/v5/trade/orders-pending"Поле метода начинается с _api, что указывает на то, что оно запускается вызовом функции exchange.IO в экземпляре стратегии.

  • поле параметров:

    {"instId":"ETH-USDT","instType":"SPOT"}   // instType=SPOT&instId=ETH-USDT编码的参数会被还原为JSON
    
  • Данные, которые хост ожидает в ответе общего протокола:

    {
        "data": {"code": "0", "data": [], "msg": ""}    // data属性值为交易所API:GET /api/v5/trade/orders-pending 应答的数据
    }
    
  • другой Другие функции API платформы Inventor, используемые в примерах стратегий, такие как: exchange.Go()exchange.GetRawJSON()Подобные функции не требуют инкапсуляции, а метод вызова и функциональность остаются неизменными.

Фьючерсная биржа

Помимо поддержки всех функций спотовых бирж, фьючерсные биржи также имеют некоторые функции API, которые являются уникальными для фьючерсных бирж.

Будет реализовано

  • GetPositions
  • SetMarginLevel
  • GetFundings

Версия Python примера общего протокола

Общий протокол REST — доступ к интерфейсу REST API обмена OKX и его инкапсуляция в качестве объекта спотового обмена. Реализован общий интерфейс для инкапсуляции данных запросов и ответов. Реализована подпись закрытого интерфейса, инкапсуляция данных запросов и ответов. Этот пример в основном для тестирования и обучения. Другие интерфейсы используют смоделированные данные для прямого ответа хосту для тестирования.

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'
        if httpMethod == "GET":
            jsonStr = json.dumps(params, separators=(',', ':')) if len(params) > 0 else ""
        else:
            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()