avatar of 发明者量化-小小梦 发明者量化-小小梦
집중하다 사신
4
집중하다
1271
수행원

Inventor 양적 거래 플랫폼 일반 프로토콜 액세스 가이드

만든 날짜: 2024-10-29 14:37:56, 업데이트 날짜: 2024-11-12 21:58:55
comments   0
hits   865

[TOC]

Inventor 양적 거래 플랫폼 일반 프로토콜 액세스 가이드

Inventor 양적 거래 플랫폼은 많은 암호화폐 거래소를 지원하며, 시장의 주요 거래소를 포괄합니다. 그러나 여전히 패키징되지 않은 거래소가 많이 있습니다. 이러한 거래소를 사용해야 하는 사용자는 발명자가 정량화한 범용 프로토콜을 통해 접근할 수 있습니다. 암호화폐 거래소에 국한되지 않음REST동의 또는FIX해당 계약 플랫폼에도 접속이 가능합니다.

이 기사는REST프로토콜 액세스를 예로 들어, Inventor Quantitative Trading Platform의 일반 프로토콜을 사용하여 OKX Exchange의 API를 캡슐화하고 액세스하는 방법을 설명합니다. 달리 명시하지 않는 한, 이 문서는 REST 일반 프로토콜을 참조합니다.

  • 일반 프로토콜의 워크플로는 다음과 같습니다. 요청 프로세스: 보관소에서 실행되는 전략 인스턴스 -> 일반 프로토콜 프로그램 -> Exchange API 응답 프로세스: Exchange API -> 일반 프로토콜 프로그램 -> 보관소에서 실행되는 전략 인스턴스

1. 교환 구성

Inventor Quantitative Trading Platform에서 거래소를 구성하기 위한 페이지:

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

Inventor 양적 거래 플랫폼 일반 프로토콜 액세스 가이드

  • 프로토콜 선택: “일반 프로토콜”을 선택하세요.
  • 서비스 주소: 일반 프로토콜 프로그램은 본질적으로 RPC 서비스입니다. 따라서 거래소를 구성할 때 서비스 주소와 포트를 명확하게 지정하는 것이 필요합니다. 그래서托管者上运行的策略实例 -> 通用协议程序이 과정에서 호스트는 공통 프로토콜 프로그램에 액세스할 위치를 알게 됩니다. 예를 들어:http://127.0.0.1:6666/OKX일반적으로 공통 프로토콜 프로그램과 호스트는 동일한 장치(서버)에서 실행되므로 서비스 주소는 로컬 머신(localhost)으로 작성되고, 포트는 시스템에서 사용하지 않는 포트가 될 수 있습니다.
  • Access Key: 托管者上运行的策略实例 -> 通用协议程序프로세스 중에 전달된 교환 구성 정보입니다.
  • Secret Key: 托管者上运行的策略实例 -> 通用协议程序프로세스 중에 전달된 교환 구성 정보입니다.
  • 상표: Inventor Quantitative Trading Platform의 교환 대상 라벨은 특정 교환 대상을 식별하는 데 사용됩니다.

해당 기사에 공개된 OKX 플러그인 구성의 스크린샷은 다음과 같습니다.

Inventor 양적 거래 플랫폼 일반 프로토콜 액세스 가이드

OKX 교환 비밀 키 구성 정보:

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

2. 관리자 및 범용 프로토콜 프로그램(플러그인)의 배포

    1. 호스트 Inventor Quantitative Trading Platform에서 실시간 전략을 실행하려면 커스터디언을 배포해야 합니다. 커스터디언의 구체적인 배포에 대해서는 플랫폼 튜토리얼을 참조하시기 바랍니다. 여기서는 반복하지 않습니다.
    1. 일반 프로토콜 프로그램(플러그인) 호스트와 범용 프로토콜은 일반적으로 동일한 장치에 배포됩니다. 범용 프로토콜(서비스) 프로그램은 어떤 언어로든 작성할 수 있습니다. 이 문서는 Python3으로 작성되었습니다. 다른 Python 프로그램을 실행하는 것처럼 직접 실행할 수 있습니다(Python 환경을 미리 다양하게 구성). 물론 FMZ는 Python 프로그램 실행도 지원하며, 이 일반 프로토콜은 실제 디스크로 실행되어 발명가의 양적 거래 플랫폼에 패키지되지 않은 거래 API 액세스를 지원할 수 있습니다. 일반 프로토콜 프로그램이 실행된 후 모니터링을 시작합니다.http://127.0.0.1:6666일반 프로토콜 프로그램에서는 예를 들어 특정 경로를 지정할 수 있습니다./OKX처리됩니다.

3. 전략 인스턴스는 FMZ API 기능을 요청합니다.

전략에서 (FMZ) 플랫폼 API 기능이 호출되면 Universal Protocol Program은 관리자로부터 요청을 받습니다. 다음과 같이 플랫폼의 디버깅 도구를 사용하여 테스트할 수도 있습니다.

디버깅 도구 페이지:

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

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

부르다exchange.GetTicker()기능, 일반 프로토콜 프로그램은 요청을 수신합니다:

POST /OKX HTTP/1.1 
{
    "access_key":"xxx",
    "method":"ticker",
    "nonce":1730275031047002000,
    "params":{"symbol":"LTC_USDT"},
    "secret_key":"xxx"
}
  • access_key: 위의 “Configure Exchange” 플랫폼에서 구성된 교환 키
  • secret_key: 위의 “Configure Exchange” 플랫폼에서 구성된 교환 키
  • 방법: 전략에서 호출 인터페이스와 관련된 호출exchange.GetTicker()시간,method그것은이다ticker
  • nonce: 요청이 발생한 타임스탬프.
  • params: 정책의 인터페이스 호출과 관련된 매개변수입니다.exchange.GetTicker()호출 시 관련 매개변수는 다음과 같습니다.{"symbol":"LTC_USDT"}

4. 교환 인터페이스에 대한 일반 프로토콜 프로그램 접근

일반 프로토콜 프로그램이 관리자로부터 요청을 받으면, 요청에 포함된 정보를 기반으로 전략에서 요청한 플랫폼 API 함수(매개변수 정보 포함), 교환 키 등의 정보를 얻을 수 있습니다.

이러한 정보를 바탕으로 일반 프로토콜 프로그램은 교환 인터페이스에 접근하여 필요한 데이터를 얻거나 특정 작업을 수행할 수 있습니다.

일반적으로 교환 인터페이스에는 GET/POST/PUT/DELETE와 같은 메소드가 있으며, 이는 공개 인터페이스와 개인 인터페이스로 구분됩니다.

  • 공개 인터페이스: 서명 검증이 필요하지 않고 일반 프로토콜 프로그램에서 직접 요청되는 인터페이스입니다.
  • 개인 인터페이스: 서명 검증이 필요한 인터페이스. 서명은 이러한 거래소의 API 인터페이스를 요청하기 위해 일반 프로토콜 프로그램에서 구현되어야 합니다.

일반 프로토콜 프로그램은 교환 인터페이스 응답 데이터를 수신하고, 이를 추가로 처리하여 보관자가 기대하는 데이터로 구성합니다(아래 설명). OKX 스팟 거래소, Python 일반 프로토콜 예제에서 CustomProtocolOKX 클래스 구현을 참조하세요.GetTickerGetAccount그리고 다른 기능들도요.

5. 일반 프로토콜 프로그램은 데이터를 보관자에게 응답합니다.

일반 프로토콜 프로그램이 거래소의 API 인터페이스에 접근하여 특정 작업을 수행하거나 특정 데이터를 얻는 경우, 그 결과를 보관인에게 피드백해야 합니다.

관리자에게 피드백되는 데이터는 전략에서 호출하는 인터페이스에 따라 달라지며, 먼저 두 가지 범주로 나뉩니다.

  • 일반 프로토콜 프로그램은 교환 인터페이스를 성공적으로 호출합니다.
  {
      "data": null,  // "data" can be of any type 
      "raw": null    // "raw" can be of any type 
  }
  • 데이터: 이 필드의 구체적인 구조는 일반 프로토콜 프로그램이 수신한 요청의 구조와 동일합니다.method다음은 FMZ 플랫폼 API 함수에서 반환된 데이터 구조를 구성하는 데 사용된 모든 인터페이스 목록입니다.

  • 원시: 이 필드는 예를 들어 교환 API 응답의 원시 데이터를 전달하는 데 사용할 수 있습니다.exchange.GetTicker()이 함수에서 반환된 Ticker 구조체에는 Ticker 구조체의 Info 필드에 기록된 다음 정보가 있습니다.raw필드 및data필드의 데이터입니다. 일부 플랫폼 API 함수에는 이 데이터가 필요하지 않습니다.

  • 일반 프로토콜 프로그램이 교환 인터페이스를 호출하지 못했습니다(비즈니스 오류, 네트워크 오류 등)

  {
      "error": ""    // "error" contains an error message as a string
  }
  • 오류: (FMZ) 플랫폼 실제 디스크, 디버깅 도구 및 기타 페이지의 로그 영역에 있는 오류 로그에 표시될 오류 정보입니다.

정책 프로그램에서 수신한 일반 프로토콜 응답 데이터를 보여줍니다.

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

Inventor 양적 거래 플랫폼 일반 프로토콜 액세스 가이드

6. 일반 프로토콜에서의 데이터 구조 합의

위의 내용은 일반 프로토콜 프로그램이 (FMZ unpackaged) 교환 API에 액세스하는 데 참여하는 방법에 대한 간략한 프로세스입니다. 이 프로세스는 (FMZ) 플랫폼 디버깅 도구를 호출하는 방법만 설명합니다.exchange.GetTicker()기능 프로세스. 다음으로, 모든 플랫폼 API 함수의 상호작용 세부 사항을 자세히 설명하겠습니다.

이 플랫폼은 다양한 거래소의 공통적인 기능을 캡슐화하여 특정 기능(예: 특정 상품의 현재 시장 정보를 요청하는 GetTicker 기능)으로 통합합니다. 이는 기본적으로 모든 거래소가 가지고 있는 API입니다. 따라서 플랫폼 캡슐화 API 인터페이스가 전략 인스턴스에서 액세스되면 관리자는 “Universal Protocol” 플러그인(위에 언급됨)에 ​​요청을 보냅니다.

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

전략에서 다양한 발명가 플랫폼 캡슐화 API 함수(예: GetTicker)를 호출할 때 관리자가 일반 프로토콜에 전송하는 요청 형식도 달라집니다. 본문의 데이터(JSON)는 다음에서만 다릅니다.method그리고params. 일반 프로토콜을 설계할 때,method콘텐츠에 따라 특정 작업을 수행할 수 있습니다. 다음은 모든 인터페이스에 대한 요청-응답 시나리오입니다.

스팟 교환

예를 들어, 현재 거래 쌍은 다음과 같습니다.ETH_USDT, 나중에 자세한 내용을 설명하지 않겠습니다. 보관 기관이 일반 프로토콜에 대한 응답을 기대하는 데이터는 주로 데이터 필드에 기록되며, 교환 인터페이스의 원본 데이터를 기록하기 위해 원시 필드를 추가할 수도 있습니다.

  • GetTicker

    • 메서드 필드: “ticker”
    • 매개변수 필드:
    {"symbol":"ETH_USDT"}
    
    • 호스트가 일반 프로토콜 응답에서 기대하는 데이터:
    {
        "data": {
            "symbol": "ETH_USDT",      // 对应GetTicker函数返回的Ticker结构中的Symbol字段
            "buy": "2922.18",          // ...对应Buy字段
            "sell": "2922.19", 
            "high": "2955", 
            "low": "2775.15", 
            "open": "2787.72", 
            "last": "2922.18", 
            "vol": "249400.888156", 
            "time": "1731028903911"
        },
        "raw": {}                      // 可以增加一个raw字段记录交易所API接口应答的原始数据
    }
    
  • GetDepth

    • 메서드 필드: “깊이”
    • 매개변수 필드:
    {"limit":"30","symbol":"ETH_USDT"}
    
    • 호스트가 일반 프로토콜 응답에서 기대하는 데이터:
    {
        "data" : {
            "time" : 1500793319499,
            "asks" : [
                [1000, 0.5], [1001, 0.23], [1004, 2.1]
                // ... 
            ],
            "bids" : [
                [999, 0.25], [998, 0.8], [995, 1.4]
                // ... 
            ]
        }
    }
    
  • GetTrades

    • 메서드 필드: “거래”
    • 매개변수 필드:
    {"symbol":"eth_usdt"}
    
    • 호스트가 일반 프로토콜 응답에서 기대하는 데이터:
    { 
        "data": [
            {
                "id": 12232153,
                "time" : 1529919412968,
                "price": 1000,
                "amount": 0.5,
                "type": "buy",             // "buy"、"sell"、"bid"、"ask"
            }, {
                "id": 12545664,
                "time" : 1529919412900,
                "price": 1001,
                "amount": 1,
                "type": "sell",
            }
            // ...
        ]
    }
    
  • GetRecords

    • 메서드 필드: “레코드”
    • 매개변수 필드:
    {
        "limit":"500",
        "period":"60",          // 60分钟
        "symbol":"ETH_USDT"
    }
    
    • 호스트가 일반 프로토콜 응답에서 기대하는 데이터:
    {
        "data": [
                // "Time":1500793319000,"Open":1.1,"High":2.2,"Low":3.3,"Close":4.4,"Volume":5.5
                [1500793319, 1.1, 2.2, 3.3, 4.4, 5.5],
                [1500793259, 1.01, 2.02, 3.03, 4.04, 5.05],
                // ...
        ]
    }
    
  • GetMarkets 구현될 예정

    • 메서드 필드: “”
    • 매개변수 필드:
    {}
    
    • 호스트가 일반 프로토콜 응답에서 기대하는 데이터:
    {}
    
  • GetTickers 구현될 예정

    • 메서드 필드: “”
    • 매개변수 필드:
    {}
    
    • 호스트가 일반 프로토콜 응답에서 기대하는 데이터:
    {}
    
  • GetAccount

    • 방법 필드: “accounts”
    • 매개변수 필드:
    {}
    
    • 호스트가 일반 프로토콜 응답에서 기대하는 데이터:
    {
        "data": [
            {"currency": "TUSD", "free": "3000", "frozen": "0"}, 
            {"currency": "BTC", "free": "0.2482982056277609", "frozen": "0"}, 
            // ...
        ]
    }
    
  • GetAssets

    • 메서드 필드: “자산”
    • 매개변수 필드:
    {}
    
    • 호스트가 일반 프로토콜 응답에서 기대하는 데이터:
    {
        "data": [
            {"currency": "TUSD", "free": "3000", "frozen": "0"},
            {"currency": "BTC", "free": "0.2482982056277609", "frozen": "0"}, 
            // ...
        ]
    }
    
  • CreateOrder / Buy / Sell

    • 메서드 필드: “trade”
    • 매개변수 필드:
    {"amount":"0.1","price":"1000","symbol":"BTC_USDT","type":"buy"}
    
    • 호스트가 일반 프로토콜 응답에서 기대하는 데이터:
    {
        "data": {
            "id": "BTC-USDT,123456"
        }
    }
    
  • GetOrders

    • 메서드 필드: “주문”
    • 매개변수 필드:
    {"symbol":"ETH_USDT"}
    
    • 호스트가 일반 프로토콜 응답에서 기대하는 데이터:
    {
        "data": [
            {
                "id": "ETH-USDT,123456",
                "symbol": "ETH_USDT",
                "amount": 0.25,
                "price": 1005,
                "deal_amount": 0,
                "avg_price": "1000",
                "type": "buy",         // "buy"、"sell"
                "status": "pending",   // "pending", "pre-submitted", "submitting", "submitted", "partial-filled"
            }, 
            // ...
        ]
    }
    
  • GetOrder

    • 메서드 필드: “order”
    • 매개변수 필드:
    {
        "id":"ETH-USDT,123456",       // 策略中调用:exchange.GetOrder("ETH-USDT,123456")
        "symbol":"ETH_USDT"
    }
    
    • 호스트가 일반 프로토콜 응답에서 기대하는 데이터:
    { 
        "data": {
            "id": "ETH-USDT,123456",
            "symbol": "ETH_USDT"
            "amount": 0.15,
            "price": 1002,
            "status": "pending",    // "pending", "pre-submitted", "submitting", "submitted", "partial-filled", "filled", "closed", "finished", "partial-canceled", "canceled"
            "deal_amount": 0,
            "type": "buy",          // "buy"、"sell"
            "avg_price": 0,         // 如果交易所没有提供,在处理时可以赋值为0
        }
    }
    
  • GetHistoryOrders

    • 메서드 필드: “historyorders”
    • 매개변수 필드:
    {"limit":0,"since":0,"symbol":"ETH_USDT"}
    
    • 호스트가 일반 프로토콜 응답에서 기대하는 데이터:
    {
        "data": [
            {
                "id": "ETH-USDT,123456",
                "symbol": "ETH_USDT",
                "amount": 0.25,
                "price": 1005,
                "deal_amount": 0,
                "avg_price": 1000,
                "type": "buy",       // "buy"、"sell"
                "status": "filled",  // "filled"
            }, 
            // ...
        ]
    }
    
  • CancelOrder

    • 메서드 필드: “취소”
    • 매개변수 필드:
    {"id":"ETH-USDT,123456","symbol":"ETH_USDT"}
    
    • 호스트가 일반 프로토콜 응답에서 기대하는 데이터:
    {
        "data": true    // 只要该JSON中没有error字段,都默认为撤单成功
    }
    
  • IO

exchange.IO 함수는 교환 인터페이스에 직접 액세스하는 데 사용됩니다. 예를 들어, 다음을 사용합니다.GET /api/v5/trade/orders-pending, 参数:instType=SPOT,instId=ETH-USDT예를 들어.

  // 策略实例中调用
  exchange.IO("api", "GET", "/api/v5/trade/orders-pending", "instType=SPOT&instId=ETH-USDT")
  • 메서드 필드:"__api_/api/v5/trade/orders-pending"메서드 필드는 __api_로 시작하며, 이는 전략 인스턴스의 exchange.IO 함수 호출에 의해 트리거됨을 나타냅니다.

  • 매개변수 필드:

    {"instId":"ETH-USDT","instType":"SPOT"}   // instType=SPOT&instId=ETH-USDT编码的参数会被还原为JSON
    
  • 호스트가 일반 프로토콜 응답에서 기대하는 데이터:

    {
        "data": {"code": "0", "data": [], "msg": ""}    // data属性值为交易所API:GET /api/v5/trade/orders-pending 应答的数据
    }
    
  • 다른 전략 예제에 사용된 기타 Inventor Platform API 기능은 다음과 같습니다. exchange.Go()exchange.GetRawJSON()이러한 함수는 캡슐화할 필요가 없으며 호출 방법과 기능은 변경되지 않습니다.

선물거래소

선물 거래소는 현물 거래소의 모든 기능을 지원하는 것 외에도 선물 거래소에서만 제공되는 몇 가지 API 기능도 갖추고 있습니다.

구현될 예정

  • GetPositions
  • SetMarginLevel
  • GetFundings

일반 프로토콜 예제의 Python 버전

REST 일반 프로토콜 - OKX 교환 REST API 인터페이스에 접근하여 이를 스팟 교환 객체로 캡슐화합니다. 요청 및 응답 데이터 캡슐화를 위한 공통 인터페이스를 구현했습니다. 개인 인터페이스 서명, 요청 및 응답 데이터 캡슐화를 구현했습니다. 이 예는 주로 테스트 및 학습을 위한 것입니다. 다른 인터페이스는 시뮬레이션된 데이터를 사용하여 테스트를 위해 호스트에 직접 응답합니다.

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