avatar of 发明者量化-小小梦 发明者量化-小小梦
focar em Mensagem privada
4
focar em
1271
Seguidores

Guia de acesso ao protocolo geral da plataforma de negociação quantitativa Inventor

Criado em: 2024-10-29 14:37:56, atualizado em: 2024-11-12 21:58:55
comments   0
hits   865

[TOC]

Guia de acesso ao protocolo geral da plataforma de negociação quantitativa Inventor

A plataforma de negociação quantitativa Inventor oferece suporte a muitas bolsas de criptomoedas e engloba as principais bolsas do mercado. No entanto, ainda há muitas exchanges que não são empacotadas. Para usuários que precisam usar essas exchanges, eles podem acessá-las por meio do protocolo universal quantificado pelo inventor. Não limitado a trocas de criptomoedas, qualquerRESTAcordo ouFIXA plataforma do acordo também pode ser acessada.

Este artigo iráRESTTomando o acesso ao protocolo como exemplo, ele explica como usar o protocolo geral da Plataforma de Negociação Quantitativa do Inventor para encapsular e acessar a API da OKX Exchange. A menos que especificado de outra forma, este artigo se refere ao protocolo geral REST.

  • O fluxo de trabalho do protocolo geral é: Processo de solicitação: Instância de estratégia em execução no custodiante -> Programa de protocolo geral -> API de troca Processo de resposta: API de troca -> Programa de protocolo geral -> Instância de estratégia em execução no custodiante

1. Configurar a troca

A página para configurar a bolsa na Plataforma de Negociação Quantitativa do Inventor:

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

Guia de acesso ao protocolo geral da plataforma de negociação quantitativa Inventor

  • Selecionar protocolo: Selecione “Protocolo Geral”.
  • Endereço do serviço: O programa de protocolo geral é essencialmente um serviço RPC Portanto, é necessário especificar claramente o endereço e a porta do serviço ao configurar a troca. Então em托管者上运行的策略实例 -> 通用协议程序Durante o processo, o host sabe onde acessar o programa de protocolo comum. Por exemplo:http://127.0.0.1:6666/OKXNormalmente, o programa de protocolo comum e o host são executados no mesmo dispositivo (servidor), então o endereço do serviço é escrito como a máquina local (localhost), e a porta pode ser uma porta que não esteja ocupada pelo sistema.
  • Access Key: 托管者上运行的策略实例 -> 通用协议程序As informações de configuração de troca transmitidas durante o processo.
  • Secret Key: 托管者上运行的策略实例 -> 通用协议程序As informações de configuração de troca transmitidas durante o processo.
  • Rótulo: O rótulo do objeto de troca na Plataforma de Negociação Quantitativa do Inventor é usado para identificar um determinado objeto de troca.

A captura de tela da configuração do plug-in OKX divulgada no artigo é a seguinte:

Guia de acesso ao protocolo geral da plataforma de negociação quantitativa Inventor

Informações de configuração da chave secreta da troca OKX:

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

2. Implantação do programa custodiante e do protocolo universal (plugin)

    1. Anfitrião Para executar qualquer estratégia em tempo real na Inventor Quantitative Trading Platform, você deve implementar um custodiante. Para implementação específica do custodiante, consulte o tutorial da plataforma, que não será repetido aqui.
    1. Programa de protocolo geral (plugin) O host e o protocolo universal são geralmente implantados no mesmo dispositivo. O programa do protocolo universal (serviço) pode ser escrito em qualquer linguagem. Este artigo foi escrito em Python3. Assim como executar qualquer programa Python, você pode executá-lo diretamente (fazer várias configurações do ambiente Python com antecedência). Claro, o FMZ também suporta a execução de programas Python, e esse protocolo geral também pode ser executado como um disco real para fornecer à plataforma de negociação quantitativa do inventor suporte para acesso à API de câmbio não empacotada. Após a execução do programa de protocolo geral, inicie o monitoramento:http://127.0.0.1:6666No programa de protocolo geral, caminhos específicos podem ser especificados, por exemplo/OKXpara ser processado.

3. A instância de estratégia solicita a função FMZ API

Quando a função da API da plataforma (FMZ) é chamada na estratégia, o Programa de Protocolo Universal recebe uma solicitação do custodiante. Você também pode testar usando as ferramentas de depuração da plataforma, por exemplo:

Página de ferramentas de depuração:

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

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

Chamarexchange.GetTicker()Função, o programa de protocolo geral recebe a solicitação:

POST /OKX HTTP/1.1 
{
    "access_key":"xxx",
    "method":"ticker",
    "nonce":1730275031047002000,
    "params":{"symbol":"LTC_USDT"},
    "secret_key":"xxx"
}
  • access_key: A chave de troca configurada na plataforma “Configurar Exchange” acima
  • secret_key: A chave de troca configurada na plataforma “Configurar Exchange” acima
  • método: relacionado à interface de chamada na estratégia, chamandoexchange.GetTicker()hora,methodAquilo éticker
  • nonce: O registro de data e hora em que a solicitação ocorreu.
  • params: Parâmetros relacionados à chamada de interface na política.exchange.GetTicker()Ao chamar, os parâmetros relevantes são:{"symbol":"LTC_USDT"}

4. Programa de protocolo geral de acesso à interface de troca

Quando o programa de protocolo geral recebe uma solicitação do custodiante, ele pode obter informações como a função da API da plataforma (incluindo informações de parâmetros) solicitadas pela estratégia, a chave de troca, etc. com base nas informações contidas na solicitação.

Com base nessas informações, o programa de protocolo geral pode acessar a interface de troca para obter os dados necessários ou executar determinadas operações.

Normalmente, a interface de troca tem métodos como GET/POST/PUT/DELETE, que são divididos em interface pública e interface privada.

  • Interface pública: uma interface que não requer verificação de assinatura e é solicitada diretamente em um programa de protocolo geral.
  • Interface privada: uma interface que requer verificação de assinatura. A assinatura precisa ser implementada no programa de protocolo geral para solicitar a interface API dessas trocas.

O programa de protocolo geral recebe os dados de resposta da interface de troca, processa-os posteriormente e os constrói nos dados esperados pelo custodiante (descritos abaixo). Consulte a troca de pontos OKX, implementação da classe CustomProtocolOKX no exemplo de protocolo geral PythonGetTickerGetAccountE outras funções.

5. O programa de protocolo geral responde os dados ao custodiante

Quando o programa de protocolo geral acessa a interface API da exchange, executa determinadas operações ou obtém determinados dados, ele precisa enviar os resultados ao custodiante.

Os dados retornados ao custodiante variam de acordo com a interface chamada pela estratégia e são primeiramente divididos em duas categorias:

  • O programa de protocolo geral chama com sucesso a interface de troca:
  {
      "data": null,  // "data" can be of any type 
      "raw": null    // "raw" can be of any type 
  }
  • dados: A estrutura específica deste campo é a mesma da solicitação recebida pelo programa de protocolo geral.methodA seguir está uma lista de todas as interfaces usadas para construir a estrutura de dados retornada pela função API da plataforma FMZ.

  • Bruto: Este campo pode ser usado para passar os dados brutos da resposta da API de troca, por exemploexchange.GetTicker()A estrutura Ticker retornada pela função tem as seguintes informações registradas no campo Info da estrutura Ticker:rawCampos edataOs dados do campo; algumas funções da API da plataforma não precisam desses dados.

  • O programa de protocolo geral falhou ao chamar a interface de troca (erro comercial, erro de rede, etc.)

  {
      "error": ""    // "error" contains an error message as a string
  }
  • erro: informações de erro, que serão exibidas no log de erros na área de log do disco real da plataforma (FMZ), ferramenta de depuração e outras páginas.

Demonstra os dados gerais de resposta do protocolo recebidos pelo programa de políticas:

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

Guia de acesso ao protocolo geral da plataforma de negociação quantitativa Inventor

6. Acordo de estrutura de dados no protocolo geral

O acima é um breve processo de como o programa de protocolo geral participa do acesso à API de troca (FMZ unpackaged). Este processo explica apenas como chamar a ferramenta de depuração da plataforma (FMZ).exchange.GetTicker()Processo de função. A seguir, os detalhes de interação de todas as funções da API da plataforma serão explicados em detalhes.

A plataforma encapsula as funções comuns de várias exchanges e as unifica em uma determinada função, como a função GetTicker, que solicita as informações atuais do mercado de um determinado produto. Esta é basicamente uma API que todas as exchanges têm. Então, quando a interface da API encapsulada na plataforma é acessada na instância de estratégia, o custodiante enviará uma solicitação ao plug-in “Universal Protocol” (mencionado acima):

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

Ao chamar diferentes funções de API encapsuladas da plataforma do inventor na estratégia (como GetTicker), o formato de solicitação enviado pelo custodiante ao protocolo geral também será diferente. Os dados (JSON) no corpo diferem apenas emmethodeparams. Ao projetar um protocolo geral,methodVocê pode executar operações específicas com base no conteúdo. A seguir estão cenários de solicitação-resposta para todas as interfaces.

Troca à vista

Por exemplo, o par de negociação atual é:ETH_USDT, Não entrarei em detalhes mais tarde. Os dados que o custodiante espera que o protocolo geral responda são principalmente escritos no campo de dados, e um campo bruto também pode ser adicionado para registrar os dados originais da interface de troca.

  • GetTicker

    • campo de método: “ticker”
    • campo params:
    {"symbol":"ETH_USDT"}
    
    • Dados que o host espera na resposta do protocolo geral:
    {
        "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: “profundidade”
    • campo params:
    {"limit":"30","symbol":"ETH_USDT"}
    
    • Dados que o host espera na resposta do protocolo geral:
    {
        "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: “negociações”
    • campo params:
    {"symbol":"eth_usdt"}
    
    • Dados que o host espera na resposta do protocolo geral:
    { 
        "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"
    }
    
    • Dados que o host espera na resposta do protocolo geral:
    {
        "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 A ser implementado

    • campo de método: “”
    • campo params:
    {}
    
    • Dados que o host espera na resposta do protocolo geral:
    {}
    
  • GetTickers A ser implementado

    • campo de método: “”
    • campo params:
    {}
    
    • Dados que o host espera na resposta do protocolo geral:
    {}
    
  • GetAccount

    • campo de método: “contas”
    • campo params:
    {}
    
    • Dados que o host espera na resposta do protocolo geral:
    {
        "data": [
            {"currency": "TUSD", "free": "3000", "frozen": "0"}, 
            {"currency": "BTC", "free": "0.2482982056277609", "frozen": "0"}, 
            // ...
        ]
    }
    
  • GetAssets

    • campo de método: “ativos”
    • campo params:
    {}
    
    • Dados que o host espera na resposta do protocolo geral:
    {
        "data": [
            {"currency": "TUSD", "free": "3000", "frozen": "0"},
            {"currency": "BTC", "free": "0.2482982056277609", "frozen": "0"}, 
            // ...
        ]
    }
    
  • CreateOrder / Buy / Sell

    • campo de método: “comércio”
    • campo params:
    {"amount":"0.1","price":"1000","symbol":"BTC_USDT","type":"buy"}
    
    • Dados que o host espera na resposta do protocolo geral:
    {
        "data": {
            "id": "BTC-USDT,123456"
        }
    }
    
  • GetOrders

    • campo de método: “pedidos”
    • campo params:
    {"symbol":"ETH_USDT"}
    
    • Dados que o host espera na resposta do protocolo geral:
    {
        "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: “order”
    • campo params:
    {
        "id":"ETH-USDT,123456",       // 策略中调用:exchange.GetOrder("ETH-USDT,123456")
        "symbol":"ETH_USDT"
    }
    
    • Dados que o host espera na resposta do protocolo geral:
    { 
        "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"}
    
    • Dados que o host espera na resposta do protocolo geral:
    {
        "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"}
    
    • Dados que o host espera na resposta do protocolo geral:
    {
        "data": true    // 只要该JSON中没有error字段,都默认为撤单成功
    }
    
  • IO

A função exchange.IO é usada para acessar diretamente a interface de troca. Por exemplo, usamosGET /api/v5/trade/orders-pending, 参数:instType=SPOT,instId=ETH-USDTPor exemplo.

  // 策略实例中调用
  exchange.IO("api", "GET", "/api/v5/trade/orders-pending", "instType=SPOT&instId=ETH-USDT")
  • campo método:"__api_/api/v5/trade/orders-pending"O campo do método começa com _api, indicando que ele é acionado pela chamada de função exchange.IO na instância da estratégia.

  • campo params:

    {"instId":"ETH-USDT","instType":"SPOT"}   // instType=SPOT&instId=ETH-USDT编码的参数会被还原为JSON
    
  • Dados que o host espera na resposta do protocolo geral:

    {
        "data": {"code": "0", "data": [], "msg": ""}    // data属性值为交易所API:GET /api/v5/trade/orders-pending 应答的数据
    }
    
  • outro Outras funções da API da Plataforma Inventor usadas nos exemplos de estratégia, como: exchange.Go()exchange.GetRawJSON()Funções como essas não precisam ser encapsuladas, e o método de chamada e a funcionalidade permanecem inalterados.

Bolsa de Futuros

Além de oferecer suporte a todas as funções das bolsas à vista, as bolsas de futuros também têm algumas funções de API exclusivas das bolsas de futuros.

A ser implementado

  • GetPositions
  • SetMarginLevel
  • GetFundings

Versão Python do exemplo de protocolo geral

Protocolo geral REST - acesso à interface da API REST de troca OKX e encapsulamento como um objeto de troca pontual. Implementou uma interface comum para encapsulamento de dados de solicitação e resposta. Implementou uma assinatura de interface privada, encapsulamento de dados de solicitação e resposta. Este exemplo é principalmente para teste e aprendizado. As outras interfaces usam dados simulados para responder diretamente ao host para teste.

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