FMZ Quant Trading Platform Custom Protocol Access Guide
We FMZ Quant Trading Platform supports many cryptocurrency exchanges and encapsulates the mainstream exchanges on the market. However, there are still many exchanges that are not encapsulated. For users who need to use these exchanges, they can access them through the FMZ Quant Custom Protocol. Not only limited to cryptocurrency exchanges, any platform that supports the REST protocol or FIX protocol can also be accessed.
This article will take REST protocol access as an example to explain how to use the custom protocol of the FMZ Quant Trading Platform to encapsulate and access the API of the OKX exchange. Unless otherwise specified, this article refers to the REST custom protocol.
- The workflow of the custom protocol is:
Request process: Strategy instance running on the docker -> Custom protocol program -> Exchange API
Response process: Exchange API -> Custom protocol program -> Strategy instance running on the docker
1. Configure the Exchange
The page for configuring the exchange on the FMZ Quant Trading Platform:
https://www.fmz.com/m/platforms/add
Select protocol: Select "Custom Protocol".
Service address: The custom protocol program is essentially an RPC service.
Therefore, it is necessary to specify the service address and port clearly when configuring the exchange. In this way, the docker knows where to access the custom protocol program during the processStrategy instance running on the docker -> Custom protocol program
.
For example:http://127.0.0.1:6666/OKX
, the custom protocol program and the docker are usually run on the same device (server), so the service address is written as the local machine (localhost), and the port can be used as a port that is not occupied by the system.Access Key:
The exchange configuration information passed during the processStrategy instance running on the docker -> Custom protocol program
.Secret Key:
The exchange configuration information passed during the processStrategy instance running on the docker -> Custom protocol program
.Tag:
The tag of the exchange object on the FMZ Quant Trading Platform, used to identify a certain exchange object.
The screenshot of the OKX plug-in configuration disclosed in the article is as follows:
OKX exchange secret key configuration information:
accessKey: accesskey123 // accesskey123, these are not actual keys, just for demonstration
secretKey: secretkey123
passphrase: passphrase123
2. Deployment of the Docker and the Custom Protocol Program (plugin)
- Docker
A docker must be deployed to run any strategy on the FMZ Quant Trading Platform. For details on how to deploy a docker, please refer to the platform tutorial. It will not go into details here.
- Docker
- Custom Protocol Program (plugin)
Docker and the custom protocol are usually deployed on the same device. The custom protocol (service) program can be written in any language. This article is written in Python3. Just like running any Python program, you can execute it directly (make various configurations of the Python environment in advance).
Of course, FMZ also supports running python programs, and you can also run this custom protocol as a live trading to provide the FMZ Quant Trading Platform with support for unpackaged exchange API access.
After the custom protocol program is running, it starts listening:http://127.0.0.1:6666
. The custom protocol program can process specific paths, such as/OKX
.
- Custom Protocol Program (plugin)
3. Strategy Instance Requests FMZ API Function
When the (FMZ) platform API function is called in the strategy, the custom protocol program will receive a request from the docker. You can also test it using the platform's debugging tools, for example:
Debugging tools page:
function main() {
return exchange.GetTicker("LTC_USDT")
}
Calling the function exchange.GetTicker()
, the custom protocol program receives the request:
POST /OKX HTTP/1.1
{
"access_key":"xxx",
"method":"ticker",
"nonce":1730275031047002000,
"params":{"symbol":"LTC_USDT"},
"secret_key":"xxx"
}
access_key: The exchange key configured in the platform "Configure Exchange" above
secret_key: The exchange key configured in the platform "Configure Exchange" above
method: related to the calling interface in the strategy, when calling
exchange.GetTicker()
,method
isticker
.nonce: The timestamp when the request occurred.
params: Parameters related to the interface call in the strategy, in this example, when calling
exchange.GetTicker()
, the related parameters are:{"symbol":"LTC_USDT"}
.
4. Custom Protocol Program Access to Exchange Interface
When the custom protocol program receives a request from the docker, it can obtain information such as the platform API function (including parameter information) requested by the strategy, the exchange key, etc. based on the information carried in the request.
Based on this information, the custom protocol program can access the exchange interface to obtain the required data or perform certain operations.
Usually the exchange interface has methods such as GET/POST/PUT/DELETE, which are divided into public interface and private interface.
Public interface: an interface that does not require signature verification and is directly requested in a custom protocol program.
Private interface: an interface that requires signature verification. The signature needs to be implemented in the custom protocol program to request the API interface of these exchanges.
The custom protocol program receives the response data from the exchange interface and further processes it to construct the data expected by the docker (described below). Refer to OKX spot exchange, the GetTicker
, GetAccount
and other functions in the CustomProtocolOKX class implementation in the Python custom protocol example.
5. The Custom Protocol Program Responds the Data to the Docker
When the custom protocol program accesses the exchange's API interface, performs certain operations or obtains certain data, it needs to feed back the results to the docker.
The data fed back to the docker varies according to the interface called by the strategy, and is first divided into two categories:
- The custom protocol program calls the exchange interface successfully:
{
"data": null, // "data" can be of any type
"raw": null // "raw" can be of any type
}
data: The specific structure of this field is related to the method
in the request received by the custom protocol program, and is used to construct the data structure finally returned by the FMZ platform API function. All interfaces will be listed below.
raw: This field can be used to pass in the raw data of the exchange API interface response, such as the Ticker structure returned by the exchange.GetTicker()
function. The Info field of the Ticker structure records the data of the raw
field and the data
field; some of the platform's API functions do not need this data.
- The custom protocol program failed to call the exchange interface (business error, network error, etc.)
{
"error": "" // "error" contains an error message as a string
}
error: error information, which will be displayed in the error log in the log area of the (FMZ) platform live trading, debugging tool and other pages.
Demonstrates the custom protocol response data received by the strategy program:
// 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"))
}
6. Data Structure Agreement in Custom Protocol
The above is a brief process of the custom protocol program participating in the access to the (FMZ unpackaged) exchange API. This process only explains the process when calling the exchange.GetTicker()
function in the (FMZ) platform debugging tool. The following will explain the interaction details of all platform API functions in detail.
The platform encapsulates the common functions of various exchanges and unifies them into a certain function, such as the GetTicker function, which requests the current market information of a certain product. This is an API that basically all exchanges have. Therefore, when accessing the API interface encapsulated by the platform in a strategy instance, the docker will send a request to the "Custom Protocol" plug-in program (mentioned above):
POST /OKX HTTP/1.1
{
"access_key": "xxx",
"method": "ticker",
"nonce": 1730275031047002000,
"params": {"symbol":"LTC_USDT"},
"secret_key": "xxx"
}
When calling different FMZ platform encapsulated API functions in the strategy (such as GetTicker), the request format sent by the docker to the custom protocol will also be different. The data (JSON) in the body only differs in method
and params
. When designing a custom protocol, perform specific operations according to the content of the method. The following are the request-response scenarios for all interfaces.
Spot Exchange
For example, the current trading pair is: ETH_USDT
, which will not be described in detail later. The data that the docker expects the custom protocol to respond to is mainly written in the data field, and a raw field can also be added to record the original data of the exchange interface.
- GetTicker
method field: "ticker"
params field:
{"symbol":"ETH_USDT"}
Data that the docker expects in the custom protocol response:
{
"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
method field: "depth"
params field:
{"limit":"30","symbol":"ETH_USDT"}
Data that the docker expects in the custom protocol response:
{
"data" : {
"time" : 1500793319499,
"asks" : [
[1000, 0.5], [1001, 0.23], [1004, 2.1]
// ...
],
"bids" : [
[999, 0.25], [998, 0.8], [995, 1.4]
// ...
]
}
}
- GetTrades
method field: "trades"
params field:
{"symbol":"eth_usdt"}
Data that the docker expects in the custom protocol response:
{
"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
method field: "records"
params field:
{
"limit":"500",
"period":"60", // 60 minutes
"symbol":"ETH_USDT"
}
Data that the docker expects in the custom protocol response:
{
"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
To be implemented
method field: ""
params field:
{}
Data that the docker expects in the custom protocol response:
{}
- GetTickers
To be implemented
method field: ""
params field:
{}
Data that the docker expects in the custom protocol response:
{}
- GetAccount
method field: "accounts"
params field:
{}
Data that the docker expects in the custom protocol response:
{
"data": [
{"currency": "TUSD", "free": "3000", "frozen": "0"},
{"currency": "BTC", "free": "0.2482982056277609", "frozen": "0"},
// ...
]
}
- GetAssets
method field: "assets"
params field:
{}
Data that the docker expects in the custom protocol response:
{
"data": [
{"currency": "TUSD", "free": "3000", "frozen": "0"},
{"currency": "BTC", "free": "0.2482982056277609", "frozen": "0"},
// ...
]
}
- CreateOrder / Buy / Sell
method field: "trade"
params field:
{"amount":"0.1","price":"1000","symbol":"BTC_USDT","type":"buy"}
Data that the docker expects in the custom protocol response:
{
"data": {
"id": "BTC-USDT,123456"
}
}
- GetOrders
method field: "orders"
params field:
{"symbol":"ETH_USDT"}
Data that the docker expects in the custom protocol response:
{
"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
method field: "order"
params field:
{
"id":"ETH-USDT,123456", // Calling in the strategy: exchange.GetOrder("ETH-USDT,123456")
"symbol":"ETH_USDT"
}
Data that the docker expects in the custom protocol response:
{
"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.
}
}
- GetHistoryOrders
method field: "historyorders"
params field:
{"limit":0,"since":0,"symbol":"ETH_USDT"}
Data that the docker expects in the custom protocol response:
{
"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
method field: "cancel"
params field:
{"id":"ETH-USDT,123456","symbol":"ETH_USDT"}
Data that the docker expects in the custom protocol response:
{
"data": true // As long as there is no error field in the JSON, the order cancellation is considered successful by default.
}
- IO
The exchange.IO function is used to access the exchange interface directly. For example, let's take
GET /api/v5/trade/orders-pending, parameters: instType=SPOT, instId=ETH-USDT
as an example.
// Called in the strategy instance
exchange.IO("api", "GET", "/api/v5/trade/orders-pending", "instType=SPOT&instId=ETH-USDT")
method field: "__api_/api/v5/trade/orders-pending"
, the method field starts with _api, indicating that this is triggered by the exchange.IO function call in the strategy instance.
params field:
{"instId":"ETH-USDT","instType":"SPOT"} // instType=SPOT&instId=ETH-USDT encoded parameters will be restored to JSON
Data that the docker expects in the custom protocol response:
{
"data": {"code": "0", "data": [], "msg": ""} // The data attribute value is the data of the exchange API: GET /api/v5/trade/orders-pending response
}
- Others
Other FMZ platform API functions used in the strategy examples, such as:exchange.Go()
,exchange.GetRawJSON()
and other functions do not need to be encapsulated, and the calling method and function remain unchanged.
Futures Exchange
In addition to supporting all the functions of spot exchanges, futures exchanges also have some API functions that are unique to futures exchanges.
To be implemented
GetPositions
SetMarginLevel
GetFundings
Custom Protocol Example in Python Version
REST Custom Protocol - Access to the OKX exchange REST API interface, encapsulated as a spot exchange object.
Implemented a public interface request and response data encapsulation.
Implemented a private interface signature, request and response data encapsulation.
This example is mainly for testing and learning. The other interfaces use simulated data to directly respond to the docker for testing.
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()