Phemex is a new cryptocurrency derivatives exchange founded by a group of ex-Morgan Stanley developers. Its name comes from a combination of the Greek goddess of fame, Pheme, and "mercantile exchange." Its primary offering is what it terms a perpetual futures contract, which most closely resembles a CFD or contract for difference: you profit (or make a loss) based on the difference between the entry price and the current price of the future, which closely tracks the spot price. Furthermore, as a leveraged product there's a concept of margin, with an attendant liquidation risk: if spot goes far enough against your position, everything up to the full contents of your account can be liquidated by the exchange. You can trade these products on multiple exchanges, but Phemex's claim to fame is its speed: the matching engine is highly tuned and scalable, upwards of 300,000 transactions per second.
Given Phemex's reputation, properly this article ought to be about using C++ and FIX to talk to Phemex's high-speed order-entry interface, but for kicking the tires on this race car we can start with Python and the REST interface. I am also going to use this as an opportunity to publish my first package on PyPi, phemex.
Private API authentication
Like all the other cryptocurrency exchanges, Phemex has both public and private REST endpoints, with the private endpoints protected by API keys that you can generate on the site. If you want to just connect to a public endpoint you can create a PhemexConnection:
conn = PhemexConnection()
while if you want to authenticate:
credentials = AuthCredentials(api_key, secret_key)
conn = PhemexConnection(credentials)
where the secret_key is a padded base64-encoded byte array. I make this distinction because normally you could assume that you'd get a correctly-padded value, but at least at this time of writing Phemex does not pad the secret key it generates; you'll need to add sufficient "=" characters at the end to pad out the value length to a multiple of four.
Looking inside, AuthCredentials satisfies Phemex's requirements for encoding the URL path, JSON message body (if any) in order to create the necessary HTTP headers, e.g. x-phemex-request-signature
:
class AuthCredentials(PhemexCredentials):
"""
Credentials for private API access.
"""
def __init__(self, api_key, secret_key):
self.api_key = api_key
self.secret_key = secret_key
def __call__(self, request: PreparedRequest):
expiry = str(trunc(time.time()) + 60)
if '?' not in request.path_url:
(url, query_string) = request.path_url, ''
else:
(url, query_string) = request.path_url.split('?')
message = (url + query_string + expiry + (request.body or ''))
message = message.encode('utf-8')
hmac_key = base64.urlsafe_b64decode(self.secret_key)
signature = hmac.new(hmac_key, message, hashlib.sha256)
signature_b64 = signature.hexdigest()
request.headers.update({
'x-phemex-request-signature': signature_b64,
'x-phemex-request-expiry': expiry,
'x-phemex-access-token': self.api_key,
'Content-Type': 'application/json'
})
return request
Similar to Alex Contryman's coinbasepro package on which I modeled phemex
, the raw connection class uses the requests
library, and passes the credentials object in to "wrap" each HTTP call:
class PhemexConnection:
"""
Primary client entry point for Phemex API connection
"""
def __init__(self, credentials: PhemexCredentials = PublicCredentials(),
api_url='https://api.phemex.com',
request_timeout: int = 30):
self.credentials = credentials
self.api_url = api_url.rstrip('/')
self.request_timeout = request_timeout
self.session = Session()
def send_message(self, method: str, endpoint: str, params: Optional[Dict] = None,
data: Optional[str] = None):
url = self.api_url + endpoint
r = self.session.request(method,
url,
params=params,
data=data,
auth=self.credentials,
timeout=self.request_timeout)
return r.json(parse_float=Decimal)
so now that we have a means to call any REST API on phemex.com; the remaining scaffolding, while very useful, just serves to give us a higher-level API so you don't need to know the details of the JSON encoding, REST endpoint names, etc..
Getting products (public API)
conn = PhemexConnection()
products = conn.get_products()
Getting trades (private API)
from datetime import datetime
from cloudwall.phemex import PhemexConnection, AuthCredentials
api_key = '****'
secret_key = '****'
credentials = AuthCredentials(api_key=api_key, secret_key=secret_key)
conn = PhemexConnection(credentials)
start_date = datetime(2020, 1, 1)
end_date = datetime(2020, 12, 31)
print(conn.get_trades('BTCUSD', start_date, end_date))
Under the hood this call is defined as follows:
def get_trades(self, symbol: str, start_date: datetime, end_date: datetime):
start_dt_millis = trunc(start_date.timestamp() * 1000)
end_dt_millis = trunc(end_date.timestamp() * 1000)
return self.send_message('GET', '/exchange/order/trade',
{'symbol': symbol,
'start': start_dt_millis,
'end': end_dt_millis,
'limit': 100, 'offset': 0,
'withCount': True})
which is a pattern we'll follow elsewhere, building on our raw send_message() call.
Rate limiting
coinbasepro
package includes an elegant approach to ensuring the Python API complies with the REST API's rate limits based off the token bucket algorithm. Following his code and the Phemex API specification for rate limits, we'll do the same here. Unlike coinbasepro
, though, we'll reuse the token-bucket package on PyPi rather than copying the code for the token bucket and limiter. Given that, we can define a rate limiter inside our PhemexConnection:
self.storage = token_bucket.MemoryStorage()
self.limiter = token_bucket.Limiter(rate, capacity, self.storage)
and consume one token for every request:
self.limiter.consume('phemex', 1)
Note I struggled a bit with the correct rate & capacity defaults due to lack of examples, so you might find you need to fine-tune those two parameters for PhemexConnection to get optimal throughput.
Order model
Phemex's order-placing capabilities are fairly rich, and so we are going to want to have an object model for representing orders which lets us succinctly define orders while maximizing type safety:
We should also hide the details of the order placement API's as much as possible.
conn = PhemexConnection(credentials)
# set up order helper classes
order_placer = conn.get_order_placer()
order_factory = order_placer.get_order_factory()
# create a limit order
limit = order_factory.create_limit_order(Side.SELL, 1, 10000.0, Contract('BTCUSD'))
# create a market order for BTCUSD, "cross" (no leverage), sell / short
order = order_factory.create_market_order(Side.SELL, 1, Contract('BTCUSD'))
# build up a conditional that places the given market short sell order
# when last trade price touches 8800
conditional = ConditionalOrder(Condition.IF_TOUCHED,
Trigger.LAST_PRICE, 10000.0, order)
# place the orders
limit_hnd = order_placer.submit(limit)
cond_hnd = order_placer.submit(conditional)
# cancel them
limit_hnd.cancel()
cond_hnd.cancel()
To further constrain the API, we can define enumerations for the common values:
from enum import Enum, auto
class Side(Enum):
"""
Order side -- corresponds to long (buy) and short (sell) position.
"""
BUY = auto()
SELL = auto()
class TimeInForce(Enum):
"""
Enumeration of support TIF (time-in-force) values for the exchange.
"""
GTC = auto()
IOC = auto()
FOK = auto()
Please note: I have done only very limited testing on order placement and there are no automated unit tests yet, so you should be very careful using this early-release code for trading.
Websockets
I won't go into detail in this article, but note Phemex also has a rich Websockets API, which you can use to capture marketdata, get real-time position updates, and more. See the code reference below if you are interested in an example simple subscriber.
The code
You can find the latest code for the phemex package at https://github.com/cloudwall/phemex, while the marketdata recorder for Phemex in Serenity is located at https://github.com/cloudwall/serenity/blob/master/serenity-mdrecorder/src/cloudwall/serenity/mdrecorder/phemex.py. None of the code is complete, and pull requests, issues and suggestions are all welcome!