Die Ressourcen sind geladen. Beförderung...

FMZ Quant Trading Platform benutzerdefinierte Protokollzugriffsanleitung

Schriftsteller:FMZ~Lydia, Erstellt: 2024-11-08 16:37:25, aktualisiert: 2024-11-08 16:38:52

Wir FMZ Quant Trading Platform unterstützt viele Kryptowährungsbörsen und verkapselt die Mainstream-Börsen auf dem Markt. Es gibt jedoch immer noch viele Börsen, die nicht verkapselt sind. Für Benutzer, die diese Börsen verwenden müssen, können sie über das FMZ Quant Custom Protocol darauf zugreifen.RESTProtokoll oderFIXDas Protokoll kann auch aufgerufen werden.

Dieser Artikel wirdRESTProtokollzugriff als Beispiel, um zu erklären, wie man das benutzerdefinierte Protokoll der FMZ Quant Trading Platform verwendet, um die API der OKX-Börse zu verkapseln und darauf zuzugreifen.

  • Der Workflow des benutzerdefinierten Protokolls ist: Anforderungsprozess: Strategie-Instanz, die auf dem Docker ausgeführt wird -> benutzerdefiniertes Protokollprogramm -> Exchange API Antwortprozess: Exchange API -> benutzerdefiniertes Protokollprogramm -> Strategie-Instanz, die auf dem Docker ausgeführt wird

1. Konfigurieren Sie den Austausch

Die Seite zur Konfiguration der Börse auf der FMZ Quant Trading Platform:

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

img

  • Auswählen des Protokolls: Wählen Sie Custom Protocol.
  • Serviceadresse: Das benutzerdefinierte Protokollprogramm ist im Wesentlichen ein RPC-Dienst. Daher ist es notwendig, die Dienstadresse und den Port bei der Konfiguration des Austauschs klar anzugeben. Auf diese Weise weiß der Docker, wo er während des Prozesses auf das benutzerdefinierte Protokollprogramm zugreifen kannStrategy instance running on the docker -> Custom protocol program- Ich weiß. Zum Beispiel:http://127.0.0.1:6666/OKX, das benutzerdefinierte Protokollprogramm und der Docker werden normalerweise auf demselben Gerät (Server) ausgeführt, so dass die Serviceadresse als lokale Maschine (localhost) geschrieben wird und der Port als Port verwendet werden kann, der nicht vom System besetzt wird.
  • Zugriffsschlüssel: Die während des Prozesses übermittelten Informationen zur AustauschkonfigurationStrategy instance running on the docker -> Custom protocol program.
  • Geheimer Schlüssel: Die während des Prozesses übermittelten Informationen zur AustauschkonfigurationStrategy instance running on the docker -> Custom protocol program.
  • Tag: Die Kennzeichnung des Austauschobjekts auf der FMZ Quant Trading Platform, die zur Identifizierung eines bestimmten Austauschobjekts verwendet wird.

Der Screenshot der im Artikel beschriebenen OKX-Plug-in-Konfiguration ist wie folgt:

img

Informationen zur Konfiguration des geheimen Schlüssels des OKX-Austauschs:

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

2. Bereitstellung des Docker und des Custom Protocol Programms (Plugin)

    1. Docker Ein Docker muss eingesetzt werden, um eine Strategie auf der FMZ Quant Trading Platform auszuführen.
    1. Benutzerdefiniertes Protokollprogramm (Plugin) Docker und das benutzerdefinierte Protokoll werden normalerweise auf demselben Gerät bereitgestellt. Das benutzerdefinierte Protokoll (Service) Programm kann in jeder Sprache geschrieben werden. Dieser Artikel ist in Python3 geschrieben. Genau wie bei jedem Python-Programm können Sie es direkt ausführen (vorab verschiedene Konfigurationen der Python-Umgebung vornehmen). Natürlich unterstützt FMZ auch die Ausführung von Python-Programmen, und Sie können dieses benutzerdefinierte Protokoll auch als Live-Trading ausführen, um der FMZ Quant Trading Platform Unterstützung für unpakettierten Exchange-API-Zugriff zu bieten. Nachdem das benutzerdefinierte Protokollprogramm ausgeführt wurde, beginnt es zuzuhören:http://127.0.0.1:6666Das benutzerdefinierte Protokollprogramm kann bestimmte Pfade verarbeiten, wie/OKX.

3. Strategie Instanz Anfragen FMZ API Funktion

Wenn die (FMZ) Plattform-API-Funktion in der Strategie aufgerufen wird, erhält das benutzerdefinierte Protokollprogramm eine Anfrage vom Docker.

Debug-Tools Seite:

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

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

Funktion aufrufenexchange.GetTicker(), empfängt das benutzerdefinierte Protokollprogramm die Anfrage:

POST /OKX HTTP/1.1 
{
    "access_key":"xxx",
    "method":"ticker",
    "nonce":1730275031047002000,
    "params":{"symbol":"LTC_USDT"},
    "secret_key":"xxx"
}
  • access_key: Der in der Plattform Configure Exchange oben konfigurierte Austauschschlüssel
  • secret_key: Der in der Plattform Configure Exchange oben konfigurierte Austauschschlüssel
  • Methode: bezieht sich auf die Anrufinterface in der Strategie, wenn sie aufgerufen wirdexchange.GetTicker(), methodistticker.
  • Nonce: Der Zeitpunkt, zu dem die Anfrage eingegangen ist.
  • Parameter: Parameter im Zusammenhang mit dem Anruf der Schnittstelle in der Strategie, in diesem Beispiel beim Anrufenexchange.GetTicker(), sind die entsprechenden Parameter:{"symbol":"LTC_USDT"}.

4. Benutzerdefiniertes Protokollprogramm Zugriff auf Exchange-Schnittstelle

Wenn das benutzerdefinierte Protokollprogramm eine Anfrage vom Docker erhält, kann es Informationen wie die Plattform-API-Funktion (einschließlich Parameterinformationen), die von der Strategie angefordert wird, den Austauschschlüssel usw. basierend auf den in der Anfrage enthaltenen Informationen erhalten.

Auf der Grundlage dieser Informationen kann das benutzerdefinierte Protokollprogramm auf die Austauschoberfläche zugreifen, um die erforderlichen Daten zu erhalten oder bestimmte Operationen durchzuführen.

Normalerweise verfügt die Austauschoberfläche über Methoden wie GET/POST/PUT/DELETE, die in öffentliche Schnittstelle und private Schnittstelle unterteilt sind.

  • Öffentliche Schnittstelle: eine Schnittstelle, die keine Signaturverifizierung erfordert und direkt in einem benutzerdefinierten Protokollprogramm angefordert wird.
  • Private Schnittstelle: eine Schnittstelle, die die Überprüfung der Signatur erfordert. Die Signatur muss im benutzerdefinierten Protokollprogramm implementiert werden, um die API-Schnittstelle dieser Austauschstellen anzufordern.

Das benutzerdefinierte Protokollprogramm empfängt die Antwortdaten von der Austauschoberfläche und verarbeitet sie weiter, um die vom Docker erwarteten Daten zu konstruieren (nachfolgend beschrieben).GetTicker, GetAccountund andere Funktionen in der Implementierung der Klasse CustomProtocolOKX im Beispiel des benutzerdefinierten Python-Protokolls.

5. Das benutzerdefinierte Protokollprogramm antwortet auf die Daten an den Docker

Wenn das benutzerdefinierte Protokollprogramm auf die API-Schnittstelle des Austauschs zugreift, bestimmte Operationen durchführt oder bestimmte Daten erhält, muss es die Ergebnisse an den Docker zurückgeben.

Die Daten, die an den Docker zurückgegeben werden, variieren je nach der von der Strategie aufgerufenen Schnittstelle und werden zunächst in zwei Kategorien unterteilt:

  • Das benutzerdefinierte Protokollprogramm ruft die Austauschoberfläche erfolgreich an:
{
    "data": null,  // "data" can be of any type 
    "raw": null    // "raw" can be of any type 
}

Daten: Die spezifische Struktur dieses Feldes bezieht sich auf diemethodin der vom benutzerdefinierten Protokollprogramm empfangenen Anfrage, und wird verwendet, um die Datenstruktur zu konstruieren, die schließlich von der FMZ-Plattform-API-Funktion zurückgegeben wird. raw: Dieses Feld kann verwendet werden, um die Rohdaten der Exchange-API-Schnittstellenantwort, wie z. B. die von derexchange.GetTicker()Das Informationsfeld der Ticker-Struktur erfasst die Daten derrawDiedataEinige der API-Funktionen der Plattform benötigen diese Daten nicht.

  • Das benutzerdefinierte Protokollprogramm konnte die Austauschoberfläche nicht aufrufen (Geschäftsfehler, Netzwerkfehler usw.)
{
    "error": ""    // "error" contains an error message as a string
}

Fehler: Fehlerinformationen, die im Fehlerprotokoll im Protokollbereich der Plattform (FMZ) für den Live-Handel, das Debugging-Tool und andere Seiten angezeigt werden.

Zeigt die vom Strategieprogramm empfangenen benutzerdefinierten Protokollreaktionsdaten an:

// 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. Datenstrukturvereinbarung im benutzerdefinierten Protokoll

Das obige ist ein kurzer Prozess des benutzerdefinierten Protokollprogramms, das am Zugriff auf die (FMZ unpackaged) Exchange API teilnimmt.exchange.GetTicker()Folgendes erklärt die Interaktionsdetails aller Plattform-API-Funktionen im Detail.

Die Plattform verkapselt die gemeinsamen Funktionen verschiedener Börsen und vereinigt sie in eine bestimmte Funktion, wie die GetTicker-Funktion, die die aktuellen Marktinformationen eines bestimmten Produkts anfordert. Dies ist eine API, die im Grunde alle Börsen haben. Wenn Sie daher auf die von der Plattform in einer Strategie-Instanz verkapselte API-Schnittstelle zugreifen, sendet der Docker eine Anfrage an das Custom Protocol Plug-in-Programm (oben erwähnt):

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

Bei Aufruf verschiedener in der Strategie (wie GetTicker) eingekapselter API-Funktionen der FMZ-Plattform unterscheidet sich auch das vom Docker an das benutzerdefinierte Protokoll gesendete Anforderungsformat.methodundparams. Bei der Entwicklung eines benutzerdefinierten Protokolls werden spezifische Operationen gemäß dem Inhalt der Methode durchgeführt.

Spot-Austausch

Das aktuelle Handelspaar ist beispielsweiseETH_USDTDie Daten, auf die der Docker erwartet, dass das benutzerdefinierte Protokoll antwortet, werden hauptsächlich im Datenfeld geschrieben, und ein Rohfeld kann auch hinzugefügt werden, um die ursprünglichen Daten der Austauschoberfläche aufzuzeichnen.

  • GetTicker

Methodenfeld: ticker Parameterfeld:

{"symbol":"ETH_USDT"}

Daten, die der Docker in der benutzerdefinierten Protokollantwort erwartet:

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

Methodenfeld: Tiefe Parameterfeld:

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

Daten, die der Docker in der benutzerdefinierten Protokollantwort erwartet:

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

Methodenfeld: Geschäfte Parameterfeld:

{"symbol":"eth_usdt"}

Daten, die der Docker in der benutzerdefinierten Protokollantwort erwartet:

{ 
    "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

Methodenfeld: Einträge Parameterfeld:

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

Daten, die der Docker in der benutzerdefinierten Protokollantwort erwartet:

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

Methodenfeld: Parameterfeld:

{}

Daten, die der Docker in der benutzerdefinierten Protokollantwort erwartet:

{}
  • GetTickersUmzusetzen

Methodenfeld: Parameterfeld:

{}

Daten, die der Docker in der benutzerdefinierten Protokollantwort erwartet:

{}
  • GetAccount

Methodenfeld: Rechnungen Parameterfeld:

{}

Daten, die der Docker in der benutzerdefinierten Protokollantwort erwartet:

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

Methodenfeld: Vermögenswerte Parameterfeld:

{}

Daten, die der Docker in der benutzerdefinierten Protokollantwort erwartet:

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

Methodenfeld: Handel Parameterfeld:

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

Daten, die der Docker in der benutzerdefinierten Protokollantwort erwartet:

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

Methodenfeld: Bestellungen Parameterfeld:

{"symbol":"ETH_USDT"}

Daten, die der Docker in der benutzerdefinierten Protokollantwort erwartet:

{
    "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

Feld für die Methode: order Parameterfeld:

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

Daten, die der Docker in der benutzerdefinierten Protokollantwort erwartet:

{ 
    "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.
    }
}
  • GetHistory-Orders

Methodenfeld: Verlauf der Bestellungen Parameterfeld:

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

Daten, die der Docker in der benutzerdefinierten Protokollantwort erwartet:

{
    "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"
        }, 
        // ...
    ]
}
  • Aufforderung stornieren

Methodenfeld: abbrechen Parameterfeld:

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

Daten, die der Docker in der benutzerdefinierten Protokollantwort erwartet:

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

Dieexchange.IODie Funktion wird verwendet, um direkt auf die Austauschoberfläche zuzugreifen.GET /api/v5/trade/orders-pending, parameters: instType=SPOT, instId=ETH-USDTDas ist ein Beispiel.

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

Methodenfeld:"__api_/api/v5/trade/orders-pending", beginnt das Methodenfeld mit _Apisäure, was darauf hindeutet, dass dies durch dieexchange.IOFunktionsaufruf in der Strategieinheit. Parameterfeld:

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

Daten, die der Docker in der benutzerdefinierten Protokollantwort erwartet:

{
    "data": {"code": "0", "data": [], "msg": ""}    // The data attribute value is the data of the exchange API: GET /api/v5/trade/orders-pending response
}
  • Andere Weitere in den Strategienbeispielen verwendete API-Funktionen der FMZ-Plattform, wie:exchange.Go(), exchange.GetRawJSON()und andere Funktionen nicht eingekapselt werden müssen, und die Anrufmethode und Funktion bleiben unverändert.

Futures-Börse

Zusätzlich zur Unterstützung aller Funktionen von Spotbörsen verfügen Futures-Börsen auch über einige API-Funktionen, die für Futures-Börsen einzigartig sind.

Umzusetzen

  • GetPositions
  • Margin-Level festlegen
  • GetFundings

Beispiel für benutzerdefiniertes Protokoll in Python Version

REST Custom Protocol - Zugriff auf die OKX Exchange REST API-Schnittstelle, die als Spot-Austauschobjekt eingekapselt ist. Implementiert eine öffentliche Anforderung und Antwort-Daten-Einkapselung. Implementierte eine private Schnittstellensignatur, Anforderungs- und Antwortdatenverkapselung. Dieses Beispiel dient hauptsächlich zum Testen und Lernen. Die anderen Schnittstellen verwenden simulierte Daten, um direkt auf den Docker zum Testen zu reagieren.

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

Mehr