Welcome to the development team! This guide is designed to help you build public and private exchange connectors for our market-making infrastructure. Exchange connectors are critical components that interact with exchanges to retrieve market data and execute trades.
This guide will walk you through the general structure and steps needed to implement both public and private connectors, using provided code examples and incorporating best practices observed from existing implementations. As you develop the connectors, you should also consult the exchange-specific API documentation to handle any nuances or unique requirements. Please note that this guide is not a "one size fits all" approach. Each Exchnage might require very diffrent approaches.
stop
Methodstop
MethodExchange connectors are divided into two categories:
skl-shared
library.Both public and private connectors share some common elements:
PublicExchangeConnector
or PrivateExchangeConnector
) to ensure consistency.Serializable
types.Logger
utility for logging.src/connectors/public/
.PublicExchangeConnector
interface.import {
PublicExchangeConnector,
ConnectorConfiguration,
ConnectorGroup,
Serializable,
} from 'skl-shared';
export class ExchangeNamePublicConnector implements PublicExchangeConnector {
// Implementation
}
Constructor Parameters:
group: ConnectorGroup
config: ConnectorConfiguration
credential?: Credential
(if authentication is required)Member Variables:
exchangeSymbol
, sklSymbol
)constructor(
private group: ConnectorGroup,
private config: ConnectorConfiguration,
private credential?: Credential,
) {
this.exchangeSymbol = getExchangeSymbol(this.group, this.config);
this.sklSymbol = getSklSymbol(this.group, this.config);
}
connect
method, which accepts a callback onMessage
to handle incoming data.public async connect(onMessage: (messages: Serializable[]) => void): Promise<void> {
this.websocket = new WebSocket(this.publicWebsocketUrl);
this.websocket.on('open', () => {
this.subscribeToChannels();
});
this.websocket.on('message', (data: string) => {
this.handleMessage(data, onMessage);
});
this.websocket.on('error', (error: Error) => {
// Handle errors
});
this.websocket.on('close', () => {
// Reconnect logic
});
}
private subscribeToChannels(): void {
const channels = [
`trades.${this.exchangeSymbol}`,
`orderbook.${this.exchangeSymbol}`,
`ticker.${this.exchangeSymbol}`,
];
const subscriptionMessage = {
method: 'SUBSCRIBE',
params: channels,
// Include authentication if required
};
this.websocket.send(JSON.stringify(subscriptionMessage));
}
Serializable
types.onMessage
callback.private handleMessage(data: string, onMessage: (messages: Serializable[]) => void): void {
const message = JSON.parse(data);
const eventType = this.getEventType(message);
if (eventType) {
const serializableMessages = this.createSerializableEvents(eventType, message);
if (serializableMessages.length > 0) {
onMessage(serializableMessages);
}
} else {
// Log unrecognized messages
}
}
SklEvent
types.private getEventType(message: any): SklEvent | null {
if (message.type === 'trade') {
return 'Trade';
} else if (message.type === 'orderbook') {
return 'TopOfBook';
} else if (message.type === 'ticker') {
return 'Ticker';
} else if (message.error) {
logger.error(`Error message received: ${message.error}`);
return null;
} else if (message.event === 'subscription') {
logger.info(`Subscription confirmed: ${JSON.stringify(message)}`);
return null;
}
return null;
}
private createSerializableEvents(eventType: SklEvent, message: any): Serializable[] {
switch (eventType) {
case 'Trade':
return [this.createTrade(message)];
case 'TopOfBook':
return [this.createTopOfBook(message)];
case 'Ticker':
return [this.createTicker(message)];
default:
return [];
}
}
private createTrade(message: any): Trade {
return {
symbol: this.sklSymbol,
connectorType: 'ExchangeName',
event: 'Trade',
price: parseFloat(message.price),
size: parseFloat(message.size),
side: mapExchangeSide(message.side),
timestamp: new Date(message.timestamp).getTime(),
};
}
private signMessage(message: any): any {
if (this.credential) {
// Implement signing logic
// For example, add authentication headers or parameters
}
return message;
}
close
events.this.websocket.on('close', () => {
setTimeout(() => {
this.connect(onMessage);
}, 1000); // Reconnect after 1 second
});
stop
Methodpublic async stop(): Promise<void> {
const unsubscribeMessage = {
method: 'UNSUBSCRIBE',
params: [
`trades.${this.exchangeSymbol}`,
`orderbook.${this.exchangeSymbol}`,
`ticker.${this.exchangeSymbol}`,
],
};
this.websocket.send(JSON.stringify(unsubscribeMessage));
this.websocket.close();
}
public-connector-main.ts
.// In public-connector-main.ts
const connectorInstance: PublicExchangeConnector = ConnectorFactory.getPublicConnector(
connectorGroup,
connectorConfig,
credential,
);
src/connectors/private/
.PrivateExchangeConnector
interface.import {
PrivateExchangeConnector,
ConnectorConfiguration,
ConnectorGroup,
Credential,
Serializable,
} from 'skl-shared';
export class ExchangeNamePrivateConnector implements PrivateExchangeConnector {
// Implementation
}
Constructor Parameters:
group: ConnectorGroup
config: ConnectorConfiguration
credential: Credential
(required for authentication)Member Variables:
exchangeSymbol
, sklSymbol
)constructor(
private group: ConnectorGroup,
private config: ConnectorConfiguration,
private credential: Credential,
) {
this.exchangeSymbol = getExchangeSymbol(this.group, this.config);
this.sklSymbol = getSklSymbol(this.group, this.config);
}
connect
method.public async connect(onMessage: (messages: Serializable[]) => void): Promise<void> {
this.websocket = new WebSocket(this.privateWebSocketUrl);
this.websocket.on('open', async () => {
await this.authenticate();
});
this.websocket.on('message', (data: string) => {
this.handleMessage(data, onMessage);
});
this.websocket.on('error', (error: Error) => {
// Handle errors
});
this.websocket.on('close', () => {
// Reconnection logic
});
}
private async authenticate(): Promise<void> {
// Authentication logic, e.g., sending login messages
const timestamp = Date.now();
const signature = this.generateSignature(timestamp);
const authMessage = {
op: 'login',
args: [this.credential.key, timestamp, signature],
};
this.websocket.send(JSON.stringify(authMessage));
}
listenKey
or session token.listenKey
as a query parameter in the WebSocket URL.listenKey
before expiration.Example:
private async getListenKey(): Promise<string> {
const response = await this.postRequest('/userDataStream', {});
return response.listenKey;
}
public async connect(onMessage: (messages: Serializable[]) => void): Promise<void> {
const listenKey = await this.getListenKey();
this.websocket = new WebSocket(`${this.privateWebSocketUrl}?listenKey=${listenKey}`);
this.websocket.on('open', () => {
this.startPingInterval();
this.subscribeToPrivateChannels();
});
// Continue with WebSocket setup...
}
Example:
private subscribeToPrivateChannels(): void {
const channels = [
'spot@private.deals.v3.api',
'spot@private.orders.v3.api',
];
const subscriptionMessage = {
method: 'SUBSCRIPTION',
params: channels,
};
this.websocket.send(JSON.stringify(subscriptionMessage));
}
Implement methods required by the PrivateExchangeConnector
interface:
public async placeOrders(request: BatchOrdersRequest): Promise<any> {
const orders = request.orders.map(order => {
return {
symbol: this.exchangeSymbol,
quantity: order.size.toFixed(8),
price: order.price.toFixed(8),
side: mapSide(order.side),
type: mapOrderType(order.type),
};
});
// Implement order batching if necessary
// ...
const endpoint = '/api/v3/order/batch';
return await this.postRequest(endpoint, { orders });
}
Example:
public async placeOrders(request: BatchOrdersRequest): Promise<any> {
const maxBatchSize = 20; // Example limit
const orders = request.orders.map(order => ({
// Map order fields...
}));
const batches = this.chunkArray(orders, maxBatchSize);
const promises = batches.map(batch => {
return this.postRequest('/batchOrders', { batchOrders: JSON.stringify(batch) });
});
const results = await Promise.all(promises);
return results;
}
private chunkArray(array: any[], chunkSize: number): any[] {
const results = [];
for (let i = 0; i < array.length; i += chunkSize) {
results.push(array.slice(i, i + chunkSize));
}
return results;
}
public async deleteAllOrders(request: CancelOrdersRequest): Promise<any> {
const endpoint = '/api/v3/openOrders';
return await this.deleteRequest(endpoint, { symbol: this.exchangeSymbol });
}
public async getCurrentActiveOrders(request: OpenOrdersRequest): Promise<OrderStatusUpdate[]> {
const endpoint = '/api/v3/openOrders';
const response = await this.getRequest(endpoint, { symbol: this.exchangeSymbol });
return response.map(order => ({
event: 'OrderStatusUpdate',
connectorType: 'ExchangeName',
symbol: this.sklSymbol,
orderId: order.orderId,
sklOrderId: order.clientOrderId,
state: mapOrderState(order.status),
side: mapExchangeSide(order.side),
price: parseFloat(order.price),
size: parseFloat(order.origQty),
notional: parseFloat(order.price) * parseFloat(order.origQty),
filled_price: parseFloat(order.price),
filled_size: parseFloat(order.executedQty),
timestamp: order.time,
}));
}
public async getBalancePercentage(request: BalanceRequest): Promise<BalanceResponse> {
const endpoint = '/api/v3/account';
const response = await this.getRequest(endpoint, {});
const baseAsset = this.group.name;
const quoteAsset = this.config.quoteAsset;
const base = response.balances.find(b => b.asset === baseAsset) || { free: '0', locked: '0' };
const quote = response.balances.find(b => b.asset === quoteAsset) || { free: '0', locked: '0' };
const baseBalance = parseFloat(base.free) + parseFloat(base.locked);
const quoteBalance = parseFloat(quote.free) + parseFloat(quote.locked);
const baseValue = baseBalance * request.lastPrice;
const totalValue = baseValue + quoteBalance;
const inventoryPercentage = (baseValue / totalValue) * 100;
return {
event: 'BalanceResponse',
symbol: this.sklSymbol,
baseBalance,
quoteBalance,
inventory: inventoryPercentage,
timestamp: Date.now(),
};
}
private handleMessage(data: string, onMessage: (messages: Serializable[]) => void): void {
const message = JSON.parse(data);
const eventType = this.getEventType(message);
if (eventType === 'OrderStatusUpdate') {
const orderStatusUpdate = this.createOrderStatusUpdate(message);
onMessage([orderStatusUpdate]);
} else {
// Handle other events or log unrecognized messages
}
}
private getEventType(message: any): SklEvent | null {
if (message.error) {
logger.error(`Error message received: ${message.error}`);
return null;
} else if (message.event === 'subscription') {
logger.info(`Subscription confirmed: ${JSON.stringify(message)}`);
return null;
} else if (message.type === 'orderUpdate') {
return 'OrderStatusUpdate';
}
// Additional event type checks...
return null;
}
close
events.Ping Mechanism:
setInterval
to send ping messages at the required interval.Cleanup on Reconnection:
Example:
private startPingInterval(): void {
this.pingInterval = setInterval(() => {
this.websocket.send(JSON.stringify({ method: 'PING' }));
}, 10000); // Ping every 10 seconds
}
private stopPingInterval(): void {
if (this.pingInterval) {
clearInterval(this.pingInterval);
this.pingInterval = null;
}
}
public async connect(onMessage: (messages: Serializable[]) => void): Promise<void> {
// Existing connection logic...
this.websocket.on('open', () => {
this.startPingInterval();
// Subscribe to channels...
});
this.websocket.on('close', () => {
this.stopPingInterval();
setTimeout(() => {
this.connect(onMessage);
}, 1000); // Reconnect after 1 second
});
}
stop
Methodpublic async stop(): Promise<void> {
// Unsubscribe from channels
const unsubscribeMessage = {
method: 'UNSUBSCRIBE',
params: [
`orders.${this.exchangeSymbol}`,
],
};
this.websocket.send(JSON.stringify(unsubscribeMessage));
// Optionally cancel all open orders
await this.deleteAllOrders({
symbol: this.sklSymbol,
event: 'CancelOrdersRequest',
timestamp: Date.now(),
connectorType: 'ExchangeName',
});
// Stop ping interval
this.stopPingInterval();
this.websocket.close();
}
private-connector-main.ts
.// In private-connector-main.ts
const connectorInstance: PrivateExchangeConnector = ConnectorFactory.getPrivateConnector(
connectorGroup,
connectorConfig,
credential,
);
try-catch
blocks in HTTP requests.By following this guide and leveraging the existing code examples, you should be able to implement both public and private connectors for any exchange. Remember to:
Welcome aboard, and happy coding!
getExchangeSymbol
, getSklSymbol
sideMap
, invertedSideMap
, MexcSideMap
, MexcInvertedSideMap
orderTypeMap
, MexcOrderTypeMap
Note: This guide is intended for internal use within our development team. Please keep all proprietary code and information confidential.
Feel free to reach out if you have any questions or need further assistance. No question is a stupid question!