skltrading / connectorTrial

Test build
3 stars 18 forks source link

Comprehensive Guide to Building Exchange Connectors for Our Trading Infrastructure

Introduction

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.


Table of Contents

  1. Overview of Exchange Connectors
  2. Prerequisites
  3. Common Components
  4. Building a Public Connector
  5. Building a Private Connector
  6. Best Practices
  7. Conclusion
  8. Appendix

1. Overview of Exchange Connectors

Exchange connectors are divided into two categories:

Public Connectors

Private Connectors


2. Prerequisites


3. Common Components

Both public and private connectors share some common elements:


4. Building a Public Connector

1. Set Up the Connector Class

import {
    PublicExchangeConnector,
    ConnectorConfiguration,
    ConnectorGroup,
    Serializable,
} from 'skl-shared';

export class ExchangeNamePublicConnector implements PublicExchangeConnector {
    // Implementation
}

2. Define Constructor and Member Variables

constructor(
    private group: ConnectorGroup,
    private config: ConnectorConfiguration,
    private credential?: Credential,
) {
    this.exchangeSymbol = getExchangeSymbol(this.group, this.config);
    this.sklSymbol = getSklSymbol(this.group, this.config);
}

3. Establish the WebSocket Connection

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
    });
}

4. Subscribe to Market Data Channels

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));
}

5. Handle Incoming Messages

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
    }
}

6. Implement Event Type Determination

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;
}

7. Create Serializable Events

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(),
    };
}

8. Handle Authentication (If Required)

private signMessage(message: any): any {
    if (this.credential) {
        // Implement signing logic
        // For example, add authentication headers or parameters
    }
    return message;
}

9. Implement Reconnection Logic

this.websocket.on('close', () => {
    setTimeout(() => {
        this.connect(onMessage);
    }, 1000); // Reconnect after 1 second
});

10. Implement the stop Method

public 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();
}

11. Update the Main Public Connector

// In public-connector-main.ts
const connectorInstance: PublicExchangeConnector = ConnectorFactory.getPublicConnector(
    connectorGroup,
    connectorConfig,
    credential,
);

12. Testing and Validation


5. Building a Private Connector

1. Set Up the Connector Class

import {
    PrivateExchangeConnector,
    ConnectorConfiguration,
    ConnectorGroup,
    Credential,
    Serializable,
} from 'skl-shared';

export class ExchangeNamePrivateConnector implements PrivateExchangeConnector {
    // Implementation
}

2. Define Constructor and Member Variables

constructor(
    private group: ConnectorGroup,
    private config: ConnectorConfiguration,
    private credential: Credential,
) {
    this.exchangeSymbol = getExchangeSymbol(this.group, this.config);
    this.sklSymbol = getSklSymbol(this.group, this.config);
}

3. Establish the WebSocket and REST Connections

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
    });
}

4. Handle Authentication

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));
}

Handling Listen Keys or Session Tokens

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...
}

5. Subscribe to Private Channels

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));
}

6. Implement RPC Methods

Implement methods required by the PrivateExchangeConnector interface:

Place Orders

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 });
}

Implementing Order Batching

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;
}

Cancel Orders

public async deleteAllOrders(request: CancelOrdersRequest): Promise<any> {
    const endpoint = '/api/v3/openOrders';
    return await this.deleteRequest(endpoint, { symbol: this.exchangeSymbol });
}

Get Open Orders

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,
    }));
}

Get Balance

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(),
    };
}

7. Handle Incoming Messages

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;
}

8. Implement Reconnection Logic

Ping Mechanism and Cleanup

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
    });
}

9. Implement the stop Method

public 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();
}

10. Update the Main Private Connector

// In private-connector-main.ts
const connectorInstance: PrivateExchangeConnector = ConnectorFactory.getPrivateConnector(
    connectorGroup,
    connectorConfig,
    credential,
);

11. Testing and Validation


6. Best Practices


7. Conclusion

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!


8. Appendix

Example Code Repositories

Utility Functions and Mappings

Common Serializable Types


Note: This guide is intended for internal use within our development team. Please keep all proprietary code and information confidential.


End of Guide

Feel free to reach out if you have any questions or need further assistance. No question is a stupid question!