avatar of 发明者量化-小小梦 发明者量化-小小梦
Seguir Mensajes Privados
4
Seguir
1271
Seguidores

Guía de acceso al protocolo general de la plataforma de negociación cuantitativa Inventor

Creado el: 2024-10-29 14:37:56, Actualizado el: 2024-11-12 21:58:55
comments   0
hits   865

[TOC]

Guía de acceso al protocolo general de la plataforma de negociación cuantitativa Inventor

La plataforma de comercio cuantitativo Inventor admite muchos intercambios de criptomonedas y encapsula los principales intercambios del mercado. Sin embargo, todavía hay muchos exchanges que no están empaquetados. Los usuarios que necesiten utilizar estos exchanges pueden acceder a ellos a través del protocolo universal cuantificado por el inventor. No se limita a los intercambios de criptomonedas, cualquierRESTAcuerdo oFIXTambién se podrá acceder a la plataforma del acuerdo.

Este artículo le ayudará a:RESTTomando el acceso al protocolo como ejemplo, explica cómo utilizar el protocolo general de la Plataforma de Comercio Cuantitativo Inventor para encapsular y acceder a la API de OKX Exchange. A menos que se especifique lo contrario, este artículo se refiere al protocolo general REST.

  • El flujo de trabajo del protocolo general es: Proceso de solicitud: Instancia de estrategia que se ejecuta en el custodio -> Programa de protocolo general -> API de Exchange Proceso de respuesta: API de Exchange -> Programa de protocolo general -> Instancia de estrategia que se ejecuta en el custodio

1. Configurar el intercambio

La página para configurar el intercambio en la Plataforma de Comercio Cuantitativo de Inventor:

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

Guía de acceso al protocolo general de la plataforma de negociación cuantitativa Inventor

  • Seleccionar protocolo: Seleccione “Protocolo general”.
  • Dirección de servicio: El programa de protocolo general es esencialmente un servicio RPC Por lo tanto, es necesario especificar claramente la dirección del servicio y el puerto al configurar el intercambio. Así que en托管者上运行的策略实例 -> 通用协议程序Durante el proceso, el host sabe dónde acceder al programa de protocolo común. Por ejemplo:http://127.0.0.1:6666/OKXGeneralmente, el programa de protocolo común y el host se ejecutan en el mismo dispositivo (servidor), por lo que la dirección del servicio se escribe como la máquina local (localhost) y el puerto puede ser un puerto que no esté ocupado por el sistema.
  • Access Key: 托管者上运行的策略实例 -> 通用协议程序La información de configuración de intercambio pasada durante el proceso.
  • Secret Key: 托管者上运行的策略实例 -> 通用协议程序La información de configuración de intercambio pasada durante el proceso.
  • Etiqueta: La etiqueta del objeto de intercambio en la Plataforma de Comercio Cuantitativo de Inventor se utiliza para identificar un determinado objeto de intercambio.

La captura de pantalla de la configuración del complemento OKX revelada en el artículo es la siguiente:

Guía de acceso al protocolo general de la plataforma de negociación cuantitativa Inventor

Información de configuración de la clave secreta de intercambio OKX:

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

2. Despliegue del programa custodio y del protocolo universal (plugin)

    1. Anfitrión Para ejecutar cualquier estrategia en tiempo real en la plataforma de negociación cuantitativa Inventor, debe implementar un custodio. Para obtener información sobre la implementación específica del custodio, consulte el tutorial de la plataforma, que no se repetirá aquí.
    1. Programa de protocolo general (plugin) El host y el protocolo universal suelen implementarse en el mismo dispositivo. El programa (servicio) del protocolo universal puede escribirse en cualquier lenguaje. Este artículo está escrito en Python3. Al igual que al ejecutar cualquier programa Python, puedes ejecutarlo directamente (realizar varias configuraciones del entorno Python con antelación). Por supuesto, FMZ también admite la ejecución de programas Python, y este protocolo general se puede ejecutar como un disco real para proporcionar a la plataforma de comercio cuantitativo del inventor soporte para acceso a API de intercambio sin empaquetar. Una vez ejecutado el programa de protocolo general, comience a monitorear:http://127.0.0.1:6666En el programa de protocolo general, se pueden especificar rutas específicas, por ejemplo/OKXpara ser procesado.

3. La instancia de estrategia solicita la función API de FMZ

Cuando se llama a la función API de la plataforma (FMZ) en la estrategia, el Programa de Protocolo Universal recibe una solicitud del custodio. También puedes probar utilizando las herramientas de depuración de la plataforma, por ejemplo:

Página de herramientas de depuración:

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

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

Llamarexchange.GetTicker()Función, el programa de protocolo general recibe la solicitud:

POST /OKX HTTP/1.1 
{
    "access_key":"xxx",
    "method":"ticker",
    "nonce":1730275031047002000,
    "params":{"symbol":"LTC_USDT"},
    "secret_key":"xxx"
}
  • access_key: La clave de intercambio configurada en la plataforma “Configurar Intercambio” arriba
  • secret_key: La clave de intercambio configurada en la plataforma “Configurar Exchange” arriba
  • método: relacionado con la interfaz de llamada en la estrategia, llamadaexchange.GetTicker()hora,methodEso esticker
  • nonce: la marca de tiempo cuando se produjo la solicitud.
  • params: Parámetros relacionados con las llamadas de interfaz en la política.exchange.GetTicker()Al llamar, los parámetros relevantes son:{"symbol":"LTC_USDT"}

4. Protocolo general de acceso del programa a la interfaz de intercambio

Cuando el programa de protocolo general recibe una solicitud del custodio, puede obtener información como la función API de la plataforma (incluida la información de parámetros) solicitada por la estrategia, la clave de intercambio, etc., basándose en la información incluida en la solicitud.

Con base en esta información, el programa de protocolo general puede acceder a la interfaz de intercambio para obtener los datos requeridos o realizar determinadas operaciones.

Generalmente, la interfaz de intercambio tiene métodos como GET/POST/PUT/DELETE, que se dividen en interfaz pública e interfaz privada.

  • Interfaz pública: una interfaz que no requiere verificación de firma y se solicita directamente en un programa de protocolo general.
  • Interfaz privada: interfaz que requiere verificación de firma. La firma debe implementarse en el programa de protocolo general para solicitar la interfaz API de estos intercambios.

El programa de protocolo general recibe los datos de respuesta de la interfaz de intercambio, los procesa y los convierte en los datos esperados por el custodio (descritos a continuación). Consulte el intercambio al contado OKX, implementación de la clase CustomProtocolOKX en el ejemplo de protocolo general de PythonGetTickerGetAccountY otras funciones.

5. El programa de protocolo general responde los datos al custodio.

Cuando el programa de protocolo general accede a la interfaz API del intercambio, realiza determinadas operaciones u obtiene determinados datos, necesita enviar los resultados al custodio.

Los datos que se envían al custodio varían según la interfaz llamada por la estrategia y se dividen primero en dos categorías:

  • El programa de protocolo general llama con éxito a la interfaz de intercambio:
  {
      "data": null,  // "data" can be of any type 
      "raw": null    // "raw" can be of any type 
  }
  • datos: La estructura específica de este campo es la misma que la de la solicitud recibida por el programa de protocolo general.methodLa siguiente es una lista de todas las interfaces utilizadas para construir la estructura de datos devuelta por la función API de la plataforma FMZ.

  • Sin procesar: este campo se puede utilizar para pasar los datos sin procesar de la respuesta de la API de intercambio, por ejemploexchange.GetTicker()La estructura Ticker devuelta por la función tiene la siguiente información registrada en el campo Información de la estructura Ticker:rawCampos ydataLos datos del campo; algunas funciones de la API de la plataforma no necesitan estos datos.

  • El programa de protocolo general no pudo llamar a la interfaz de intercambio (error comercial, error de red, etc.)

  {
      "error": ""    // "error" contains an error message as a string
  }
  • error: información de error, que se mostrará en el registro de errores en el área de registro del disco real de la plataforma (FMZ), la herramienta de depuración y otras páginas.

Demuestra los datos de respuesta del protocolo general recibidos por el programa de políticas:

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

Guía de acceso al protocolo general de la plataforma de negociación cuantitativa Inventor

6. Acuerdo de estructura de datos en protocolo general

Lo anterior es un breve proceso de cómo el programa de protocolo general participa en el acceso a la API de intercambio (FMZ sin empaquetar). Este proceso solo explica cómo llamar a la herramienta de depuración de la plataforma (FMZ).exchange.GetTicker()Proceso funcional. A continuación, se explicarán en detalle los detalles de interacción de todas las funciones API de la plataforma.

La plataforma encapsula las funciones comunes de varios exchanges y las unifica en una función determinada, como la función GetTicker, que solicita la información actual del mercado de un determinado producto. Se trata básicamente de una API que tienen todos los exchanges. Entonces, cuando se accede a la interfaz API encapsulada de la plataforma en la instancia de estrategia, el custodio enviará una solicitud al complemento “Protocolo universal” (mencionado anteriormente):

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

Al llamar a diferentes funciones API encapsuladas de la plataforma de inventor en la estrategia (como GetTicker), el formato de solicitud enviada por el custodio al protocolo general también será diferente. Los datos (JSON) en el cuerpo difieren solo enmethodyparams. Al diseñar un protocolo general,methodPuede realizar operaciones específicas en función del contenido. Los siguientes son escenarios de solicitud-respuesta para todas las interfaces.

Bolsa de valores al contado

Por ejemplo, el par comercial actual es:ETH_USDTNo entraré en detalles más adelante. Los datos que el custodio espera que responda el protocolo general se escriben principalmente en el campo de datos, y también se puede agregar un campo sin procesar para registrar los datos originales de la interfaz de intercambio.

  • GetTicker

    • campo de método: “ticker”
    • campo params:
    {"symbol":"ETH_USDT"}
    
    • Datos que el host espera en la respuesta del protocolo general:
    {
        "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

    • campo de método: “profundidad”
    • campo params:
    {"limit":"30","symbol":"ETH_USDT"}
    
    • Datos que el host espera en la respuesta del protocolo general:
    {
        "data" : {
            "time" : 1500793319499,
            "asks" : [
                [1000, 0.5], [1001, 0.23], [1004, 2.1]
                // ... 
            ],
            "bids" : [
                [999, 0.25], [998, 0.8], [995, 1.4]
                // ... 
            ]
        }
    }
    
  • GetTrades

    • Campo de método: “operaciones”
    • campo params:
    {"symbol":"eth_usdt"}
    
    • Datos que el host espera en la respuesta del protocolo general:
    { 
        "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

    • campo de método: “registros”
    • campo params:
    {
        "limit":"500",
        "period":"60",          // 60分钟
        "symbol":"ETH_USDT"
    }
    
    • Datos que el host espera en la respuesta del protocolo general:
    {
        "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 Para implementar

    • campo de método: “”
    • campo params:
    {}
    
    • Datos que el host espera en la respuesta del protocolo general:
    {}
    
  • GetTickers Para implementar

    • campo de método: “”
    • campo params:
    {}
    
    • Datos que el host espera en la respuesta del protocolo general:
    {}
    
  • GetAccount

    • campo de método: “cuentas”
    • campo params:
    {}
    
    • Datos que el host espera en la respuesta del protocolo general:
    {
        "data": [
            {"currency": "TUSD", "free": "3000", "frozen": "0"}, 
            {"currency": "BTC", "free": "0.2482982056277609", "frozen": "0"}, 
            // ...
        ]
    }
    
  • GetAssets

    • campo de método: “activos”
    • campo params:
    {}
    
    • Datos que el host espera en la respuesta del protocolo general:
    {
        "data": [
            {"currency": "TUSD", "free": "3000", "frozen": "0"},
            {"currency": "BTC", "free": "0.2482982056277609", "frozen": "0"}, 
            // ...
        ]
    }
    
  • CreateOrder / Buy / Sell

    • Campo de método: “comercio”
    • campo params:
    {"amount":"0.1","price":"1000","symbol":"BTC_USDT","type":"buy"}
    
    • Datos que el host espera en la respuesta del protocolo general:
    {
        "data": {
            "id": "BTC-USDT,123456"
        }
    }
    
  • GetOrders

    • Campo de método: “pedidos”
    • campo params:
    {"symbol":"ETH_USDT"}
    
    • Datos que el host espera en la respuesta del protocolo general:
    {
        "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

    • campo de método: “orden”
    • campo params:
    {
        "id":"ETH-USDT,123456",       // 策略中调用:exchange.GetOrder("ETH-USDT,123456")
        "symbol":"ETH_USDT"
    }
    
    • Datos que el host espera en la respuesta del protocolo general:
    { 
        "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

    • Campo de método: “historyorders”
    • campo params:
    {"limit":0,"since":0,"symbol":"ETH_USDT"}
    
    • Datos que el host espera en la respuesta del protocolo general:
    {
        "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

    • campo de método: “cancelar”
    • campo params:
    {"id":"ETH-USDT,123456","symbol":"ETH_USDT"}
    
    • Datos que el host espera en la respuesta del protocolo general:
    {
        "data": true    // 只要该JSON中没有error字段,都默认为撤单成功
    }
    
  • IO

La función exchange.IO se utiliza para acceder directamente a la interfaz de Exchange. Por ejemplo, utilizamosGET /api/v5/trade/orders-pending, 参数:instType=SPOT,instId=ETH-USDTPor ejemplo.

  // 策略实例中调用
  exchange.IO("api", "GET", "/api/v5/trade/orders-pending", "instType=SPOT&instId=ETH-USDT")
  • campo de método:"__api_/api/v5/trade/orders-pending"El campo de método comienza con _api, lo que indica que esto se activa mediante la llamada a la función exchange.IO en la instancia de estrategia.

  • campo params:

    {"instId":"ETH-USDT","instType":"SPOT"}   // instType=SPOT&instId=ETH-USDT编码的参数会被还原为JSON
    
  • Datos que el host espera en la respuesta del protocolo general:

    {
        "data": {"code": "0", "data": [], "msg": ""}    // data属性值为交易所API:GET /api/v5/trade/orders-pending 应答的数据
    }
    
  • otro Otras funciones de la API de Inventor Platform utilizadas en los ejemplos de estrategia, como: exchange.Go()exchange.GetRawJSON()Funciones como estas no necesitan encapsularse, y el método de llamada y la funcionalidad permanecen inalterados.

Bolsa de futuros

Además de admitir todas las funciones de los intercambios al contado, los intercambios de futuros también tienen algunas funciones API que son exclusivas de los intercambios de futuros.

Para implementar

  • GetPositions
  • SetMarginLevel
  • GetFundings

Versión Python del ejemplo de protocolo general

Protocolo general REST: acceso a la interfaz API REST de intercambio OKX y encapsularla como un objeto de intercambio spot. Se implementó una interfaz común para la encapsulación de datos de solicitud y respuesta. Se implementó una firma de interfaz privada y una encapsulación de datos de solicitud y respuesta. Este ejemplo es principalmente para pruebas y aprendizaje. Las demás interfaces utilizan datos simulados para responder directamente al host para realizar pruebas.

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()