Les ressources ont été chargées... Je charge...

Guide d'accès au protocole personnalisé de la plateforme de trading quantique FMZ

Auteur:FMZ~Lydia, Créé à partir de 2024-11-08 16:37:25, Mis à jour à partir de 2024-11-19 13:31:01

img

Nous FMZ Quant Trading Platform prend en charge de nombreux échanges de crypto-monnaie et encapsule les échanges traditionnels sur le marché. Cependant, il existe encore de nombreux échanges qui ne sont pas encapsulés. Pour les utilisateurs qui ont besoin d'utiliser ces échanges, ils peuvent y accéder via le protocole FMZ Quant Custom.RESTprotocole ouLe nombre de personnesLe protocole peut également être consulté.

Cet article traiteraRESTle protocole d'accès comme exemple pour expliquer comment utiliser le protocole personnalisé de la plateforme de trading FMZ Quant pour encapsuler et accéder à l'API de l'échange OKX. Sauf indication contraire, cet article fait référence au protocole personnalisé REST.

  • Le flux de travail du protocole personnalisé est: Procédure de demande: Instance de stratégie exécutée sur le docker -> Programme de protocole personnalisé -> Exchange API Processus de réponse: Exchange API -> Programme de protocole personnalisé -> Instance de stratégie exécutée sur le docker

1. Configurer l'échange

La page de configuration de l'échange sur la plateforme de négociation quantitative FMZ:

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

img

  • Sélectionnez le protocole: Sélectionnez Protocole personnalisé.
  • Adresse du service: Le programme de protocole personnalisé est essentiellement un service RPC. Par conséquent, il est nécessaire de spécifier clairement l'adresse de service et le port lors de la configuration de l'échange.Strategy instance running on the docker -> Custom protocol program- Je ne sais pas. Par exemple:http://127.0.0.1:6666/OKX, le programme de protocole personnalisé et le docker sont généralement exécutés sur le même appareil (serveur), de sorte que l'adresse du service est écrite comme la machine locale (localhost), et le port peut être utilisé comme un port qui n'est pas occupé par le système.
  • Clé d' accès: Les informations de configuration d'échange transmises au cours du processusStrategy instance running on the docker -> Custom protocol program.
  • Clé secrète: Les informations de configuration d'échange transmises au cours du processusStrategy instance running on the docker -> Custom protocol program.
  • Le titre: Étiquette de l'objet d'échange sur la plateforme de négociation quantitative FMZ, utilisée pour identifier un certain objet d'échange.

La capture d'écran de la configuration du plug-in OKX décrite dans l'article est la suivante:

img

Informations de configuration de clé secrète OKX:

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

2. Déploiement du Docker et du programme de protocole personnalisé (plugin)

    1. Docker Un docker doit être déployé pour exécuter n'importe quelle stratégie sur la plateforme de trading FMZ Quant. Pour plus de détails sur la façon de déployer un docker, veuillez vous référer au tutoriel de la plateforme.
    1. Programme de protocole personnalisé (plugin) Docker et le protocole personnalisé sont généralement déployés sur le même appareil. Le programme de protocole (service) personnalisé peut être écrit dans n'importe quel langage. Cet article est écrit en Python3. Tout comme l'exécution de n'importe quel programme Python, vous pouvez l'exécuter directement (faire diverses configurations de l'environnement Python à l'avance). Bien sûr, FMZ prend également en charge l'exécution de programmes python, et vous pouvez également exécuter ce protocole personnalisé en tant que trading en direct pour fournir à la plate-forme de trading quantique FMZ un support pour l'accès à l'API d'échange non emballé. Après l'exécution du programme de protocole personnalisé, il commence à écouter:http://127.0.0.1:6666Le programme de protocole personnalisé peut traiter des chemins spécifiques, tels que/OKX.

3. L'instance de stratégie demande la fonction FMZ API

Lorsque la fonction API de la plateforme (FMZ) est appelée dans la stratégie, le programme de protocole personnalisé recevra une demande du docker. Vous pouvez également le tester en utilisant les outils de débogage de la plateforme, par exemple:

Page des outils de débogage:

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

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

Appel à la fonctionexchange.GetTicker(), le programme de protocole personnalisé reçoit la demande:

POST /OKX HTTP/1.1 
{
    "access_key":"xxx",
    "method":"ticker",
    "nonce":1730275031047002000,
    "params":{"symbol":"LTC_USDT"},
    "secret_key":"xxx"
}
  • access_key: La clé d'échange configurée dans la plateforme Configure Exchange ci-dessus
  • secret_key: La clé d'échange configurée dans la plateforme Configure Exchange ci-dessus
  • méthode: liée à l'interface d'appel dans la stratégie, lors de l'appelexchange.GetTicker(), methodestticker.
  • nonce: l'horodatage du moment où la demande a été effectuée.
  • Paramètres: Paramètres liés à l'appel de l'interface dans la stratégie, dans cet exemple, lors de l'appelexchange.GetTicker(), les paramètres correspondants sont:{"symbol":"LTC_USDT"}.

4. Accès au programme de protocole personnalisé à l'interface d'échange

Lorsque le programme de protocole personnalisé reçoit une requête du docker, il peut obtenir des informations telles que la fonction API de la plate-forme (y compris les informations de paramètres) demandées par la stratégie, la clé d'échange, etc. sur la base des informations contenues dans la requête.

Sur la base de ces informations, le programme de protocole personnalisé peut accéder à l'interface d'échange pour obtenir les données requises ou effectuer certaines opérations.

Habituellement, l'interface d'échange a des méthodes telles que GET/POST/PUT/DELETE, qui sont divisées en interface publique et interface privée.

  • Interface publique: une interface qui ne nécessite pas de vérification de signature et qui est directement demandée dans un programme de protocole personnalisé.
  • Interface privée: une interface qui nécessite la vérification de la signature. La signature doit être implémentée dans le programme de protocole personnalisé pour demander l'interface API de ces échanges.

Le programme de protocole personnalisé reçoit les données de réponse de l'interface d'échange et les traite davantage pour construire les données attendues par le docker (décrites ci-dessous).GetTicker, GetAccountet d'autres fonctions dans la mise en œuvre de la classe CustomProtocolOKX dans l'exemple de protocole personnalisé Python.

5. Le programme de protocole personnalisé répond aux données du Docker

Lorsque le programme de protocole personnalisé accède à l'interface API de l'échange, effectue certaines opérations ou obtient certaines données, il doit retourner les résultats au dock.

Les données renvoyées au docker varient en fonction de l'interface appelée par la stratégie, et sont d'abord divisées en deux catégories:

  • Le programme de protocole personnalisé appelle l'interface d'échange avec succès:
{
    "data": null,  // "data" can be of any type 
    "raw": null    // "raw" can be of any type 
}

données: la structure spécifique de ce champ est liée à lamethoddans la requête reçue par le programme de protocole personnalisé, et est utilisé pour construire la structure de données finalement retournée par la fonction API de la plateforme FMZ. Toutes les interfaces seront énumérées ci-dessous. raw: Ce champ peut être utilisé pour transmettre les données brutes de la réponse de l'interface API d'échange, telles que la structure de ticker renvoyée par leexchange.GetTicker()Le champ Info de la structure Ticker enregistre lesrawle champ et ledataDans certains cas, il est possible d'utiliser les données fournies par les utilisateurs.

  • Le programme de protocole personnalisé n'a pas pu appeler l'interface d'échange (erreur d'entreprise, erreur de réseau, etc.)
{
    "error": ""    // "error" contains an error message as a string
}

erreur: information sur l'erreur, qui sera affichée dans le journal des erreurs dans la zone du journal de la plateforme de trading en direct (FMZ), de l'outil de débogage et d'autres pages.

Démontrer les données de réponse au protocole personnalisées reçues par le programme de stratégie:

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

img

6. Accord sur la structure des données dans le protocole personnalisé

Ce qui précède est un bref processus du programme de protocole personnalisé participant à l'accès à l'API d'échange (FMZ non emballée).exchange.GetTicker()La fonction suivante explique en détail les détails d'interaction de toutes les fonctions API de la plateforme.

La plate-forme encapsule les fonctions communes de divers échanges et les unifie en une certaine fonction, comme la fonction GetTicker, qui demande les informations actuelles du marché d'un certain produit. Il s'agit d'une API dont disposent essentiellement tous les échanges. Par conséquent, lors de l'accès à l'interface API encapsulée par la plate-forme dans une instance de stratégie, le docker enverra une demande au programme de plug-in Custom Protocol (mentionné ci-dessus):

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

Lors de l'appel de différentes fonctions API encapsulées de la plateforme FMZ dans la stratégie (comme GetTicker), le format de demande envoyé par le docker au protocole personnalisé sera également différent.methodetparams. Lors de la conception d'un protocole personnalisé, effectuez des opérations spécifiques en fonction du contenu de la méthode. Voici les scénarios de requête-réponse pour toutes les interfaces.

Échange au comptant

Par exemple, la paire de négociation actuelle est:ETH_USDTLes données auxquelles le docker s'attend à ce que le protocole personnalisé réponde sont principalement écrites dans le champ de données, et un champ brut peut également être ajouté pour enregistrer les données originales de l'interface d'échange.

  • GetTicker

champ de méthode: ticker champ paramétrique:

{"symbol":"ETH_USDT"}

Données attendues par le docker dans la réponse du protocole personnalisé:

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

champ de la méthode: profondeur champ paramétrique:

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

Données attendues par le docker dans la réponse du protocole personnalisé:

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

champ de méthode: trades champ paramétrique:

{"symbol":"eth_usdt"}

Données attendues par le docker dans la réponse du protocole personnalisé:

{ 
    "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",
        }
        // ...
    ]
}
  • Obtenez des enregistrements

champ de méthode: enregistrements champ paramétrique:

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

Données attendues par le docker dans la réponse du protocole personnalisé:

{
    "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À mettre en œuvre

champ de méthode: champ paramétrique:

{}

Données attendues par le docker dans la réponse du protocole personnalisé:

{}
  • - Je ne sais pas.À mettre en œuvre

champ de méthode: champ paramétrique:

{}

Données attendues par le docker dans la réponse du protocole personnalisé:

{}
  • Compte de prise de vue

champ de méthode: comptes champ paramétrique:

{}

Données attendues par le docker dans la réponse du protocole personnalisé:

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

champ de méthode: actifs champ paramétrique:

{}

Données attendues par le docker dans la réponse du protocole personnalisé:

{
    "data": [
        {"currency": "TUSD", "free": "3000", "frozen": "0"},
        {"currency": "BTC", "free": "0.2482982056277609", "frozen": "0"}, 
        // ...
    ]
}
  • Créer ordre / acheter / vendre

champ de méthode: échange champ paramétrique:

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

Données attendues par le docker dans la réponse du protocole personnalisé:

{
    "data": {
        "id": "BTC-USDT,123456"
    }
}
  • Obtenir des ordres

champ de méthode: ordres champ paramétrique:

{"symbol":"ETH_USDT"}

Données attendues par le docker dans la réponse du protocole personnalisé:

{
    "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"
        }, 
        // ...
    ]
}
  • Obtenir un ordre

champ de méthode: ordre champ paramétrique:

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

Données attendues par le docker dans la réponse du protocole personnalisé:

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

champ de méthode: historique des commandes champ paramétrique:

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

Données attendues par le docker dans la réponse du protocole personnalisé:

{
    "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"
        }, 
        // ...
    ]
}
  • Annuler la commande

champ de méthode: annuler champ paramétrique:

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

Données attendues par le docker dans la réponse du protocole personnalisé:

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

Leexchange.IOCette fonction est utilisée pour accéder directement à l'interface d'échange.GET /api/v5/trade/orders-pending, parameters: instType=SPOT, instId=ETH-USDTcomme exemple.

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

champ de méthode:"__api_/api/v5/trade/orders-pending", le champ méthode commence par _apes, indiquant que cela est déclenché par leexchange.IOl'appel de la fonction dans l'instance de stratégie. champ paramétrique:

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

Données attendues par le docker dans la réponse du protocole personnalisé:

{
    "data": {"code": "0", "data": [], "msg": ""}    // The data attribute value is the data of the exchange API: GET /api/v5/trade/orders-pending response
}
  • Autres Autres fonctions API de la plateforme FMZ utilisées dans les exemples de stratégie, telles que:exchange.Go(), exchange.GetRawJSON()et d'autres fonctions n'ont pas besoin d'être encapsulées, et la méthode d'appel et la fonction restent inchangées.

Échange de contrats à terme

En plus de prendre en charge toutes les fonctions des bourses au comptant, les bourses à terme disposent également de certaines fonctions d'API qui sont uniques aux bourses à terme.

À mettre en œuvre

  • Obtenez des positions
  • Définir le niveau de marge
  • GetFundings

Exemple de protocole personnalisé dans la version Python

Protocole personnalisé REST - Accès à l'interface REST API de l'échange OKX, encapsulée sous forme d'objet d'échange spot. Mise en œuvre d'une requête d'interface publique et encapsulation des données de réponse. Mise en œuvre d'une signature d'interface privée, encapsulation des données de demande et de réponse. Cet exemple est principalement destiné aux tests et à l'apprentissage. Les autres interfaces utilisent des données simulées pour répondre directement au docker pour les tests.

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

from datetime import datetime

ssl._create_default_https_context = ssl._create_unverified_context

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

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


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

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

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

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

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

        return data
    

    def GetTickers(self):
        return self.ERR_NOT_SUPPORT


    def GetMarkets(self):
        return self.ERR_NOT_SUPPORT


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


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


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


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


    def GetAssets(self):
        return self.ERR_NOT_SUPPORT


    def GetAccount(self):
        return self.ERR_NOT_SUPPORT


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


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


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


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


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


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


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


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


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


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


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

    # TODO: add information.
    """

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


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


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


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


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

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

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

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

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

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

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


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


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

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

        return {"data": ret_data}


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

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

        return {"data": ret_data}


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

        ret_data = {}

        return {"data": ret_data}


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

        ret_data = {}

        return {"data": ret_data}


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

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

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


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

        return {"data": ret_data}


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

        return {"data": ret_data}

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

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

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

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

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

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

        return {"data": ret_data}


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

        return {"data": ret_data}


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

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

        return {"data": ret_data}


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

        # Mock data for testing.
        ret_data = True

        return {"data": ret_data}


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

        return {"data": data}


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


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


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


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


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

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

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

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

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

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

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

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

        self._sendResponse(respData)


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

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

Plus de