Skip to content

Commit

Permalink
feat(woo): fetch orders
Browse files Browse the repository at this point in the history
  • Loading branch information
iam4x committed Mar 31, 2023
1 parent 5cdcf39 commit e817dc2
Show file tree
Hide file tree
Showing 3 changed files with 191 additions and 31 deletions.
90 changes: 64 additions & 26 deletions src/exchanges/woo/woo.api.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import type { AxiosRequestConfig } from 'axios';
import axios, { AxiosHeaders } from 'axios';
import retry, { isNetworkError } from 'axios-retry';
import createHmac from 'create-hmac';
Expand All @@ -8,16 +9,67 @@ import { virtualClock } from '../../utils/virtual-clock';

import { BASE_URL, RECV_WINDOW } from './woo.types';

const signV1 = (config: AxiosRequestConfig, options: ExchangeOptions) => {
const nextConfig = { ...config };

const data = config.data || config.params || {};
const asString = qs.stringify(data, {
arrayFormat: 'repeat',
sort: (a, b) => a.localeCompare(b),
});

const timestamp = virtualClock.getCurrentTime();
const signature = createHmac('sha256', options.secret)
.update(`${asString}|${timestamp}`)
.digest('hex');

const headers = new AxiosHeaders({
...nextConfig.headers,
'x-api-timestamp': timestamp,
'x-api-signature': signature,
});

return { ...nextConfig, headers };
};

const signV3 = (c: AxiosRequestConfig, options: ExchangeOptions) => {
// we need to uppercase the method and default to GET
const nextConfig = { ...c, method: c.method?.toUpperCase?.() || 'GET' };

// we do the serialization of params once here
// because we need it in the signature
if (nextConfig.params) {
nextConfig.url = `${nextConfig.url}?${qs.stringify(nextConfig.params)}`;
delete nextConfig.params;
}

const timestamp = virtualClock.getCurrentTime();
const textSign = [
timestamp,
nextConfig.method,
nextConfig.url,
nextConfig.data ? JSON.stringify(nextConfig.data) : '',
].join('');

const signature = createHmac('sha256', options.secret)
.update(textSign)
.digest('hex');

const headers = new AxiosHeaders({
...nextConfig.headers,
'x-api-timestamp': timestamp,
'x-api-signature': signature,
});

return { ...nextConfig, headers };
};

export const createAPI = (options: ExchangeOptions) => {
const xhr = axios.create({
baseURL: BASE_URL[options.testnet ? 'testnet' : 'livenet'],
timeout: RECV_WINDOW,
paramsSerializer: {
serialize: (params) => qs.stringify(params),
},
headers: {
'x-api-key': options.key,
},
paramsSerializer: { serialize: (params) => qs.stringify(params) },
headers: { 'x-api-key': options.key },
});

// retry requests on network errors instead of throwing
Expand All @@ -29,27 +81,13 @@ export const createAPI = (options: ExchangeOptions) => {
return config;
}

const nextConfig = { ...config };

const timestamp = virtualClock.getCurrentTime();
const textSign = [
timestamp,
config?.method?.toUpperCase?.() || 'GET',
config.url,
config.data ? JSON.stringify(config.data) : '',
].join('');

const signature = createHmac('sha256', options.secret)
.update(textSign)
.digest('hex');

const headers = new AxiosHeaders({
...nextConfig.headers,
'x-api-timestamp': timestamp,
'x-api-signature': signature,
});
// sign v1 endpoints requests
if (config.url?.includes('v1')) {
return signV1(config, options);
}

return { ...nextConfig, headers };
// sign v3 endpoints requests
return signV3(config, options);
});

return xhr;
Expand Down
116 changes: 111 additions & 5 deletions src/exchanges/woo/woo.exchange.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,22 @@
import type { Axios } from 'axios';
import rateLimit from 'axios-rate-limit';
import BigNumber from 'bignumber.js';
import { sumBy } from 'lodash';

import type { ExchangeOptions, Market, Position, Ticker } from '../../types';
import { PositionSide } from '../../types';
import type {
ExchangeOptions,
Market,
Order,
Position,
Ticker,
} from '../../types';
import { OrderStatus, PositionSide } from '../../types';
import { v } from '../../utils/get-key';
import { loop } from '../../utils/loop';
import { BaseExchange } from '../base';

import { createAPI } from './woo.api';
import { ENDPOINTS } from './woo.types';
import { ENDPOINTS, ORDER_SIDE, ORDER_TYPE } from './woo.types';
import { normalizeSymbol } from './woo.utils';
import { WooPublicWebsocket } from './woo.ws-public';

Expand Down Expand Up @@ -60,15 +67,25 @@ export class Woo extends BaseExchange {
const tickers = await this.fetchTickers();
if (this.isDisposed) return;

this.log(`Loaded ${Math.min(tickers.length, markets.length)} Woo markerts`);
this.log(
`Loaded ${Math.min(tickers.length, markets.length)} Woo X markerts`
);

this.store.tickers = tickers;
this.store.loaded.tickers = true;

await this.tick();
if (this.isDisposed) return;

this.log(`Ready to trade on Woo`);
this.log(`Ready to trade on Woo X`);

const orders = await this.fetchOrders();
if (this.isDisposed) return;

this.log(`Loaded Woo X orders`);

this.store.orders = orders;
this.store.loaded.orders = true;
};

tick = async () => {
Expand Down Expand Up @@ -262,4 +279,93 @@ export class Woo extends BaseExchange {
return this.store.positions;
}
};

fetchOrders = async () => {
const limitOrders = await this.fetchLimitOrders();
const algoOrders = await this.fetchAlgoOrders();
return [...limitOrders, ...algoOrders];
};

private fetchLimitOrders = async () => {
try {
const { data } = await this.xhr.get<{ rows: Array<Record<string, any>> }>(
ENDPOINTS.ORDERS,
{ params: { status: 'INCOMPLETE' } }
);

const orders: Order[] = data.rows.reduce<Order[]>((acc, o) => {
const symbol = normalizeSymbol(o.symbol);
const market = this.store.markets.find((m) => m.symbol === symbol);

if (!market) {
return acc;
}

const order: Order = {
id: v(o, 'order_id'),
status: OrderStatus.Open,
symbol,
type: ORDER_TYPE[o.type],
side: ORDER_SIDE[o.side],
price: o.price,
amount: o.quantity,
reduceOnly: v(o, 'reduce_only'),
filled: o.executed,
remaining: new BigNumber(o.quantity).minus(o.executed).toNumber(),
};

return [...acc, order];
}, []);

return orders;
} catch (err: any) {
this.emitter.emit('error', err?.response?.data?.message || err?.message);
return [];
}
};

private fetchAlgoOrders = async () => {
try {
const {
data: {
data: { rows },
},
} = await this.xhr.get<{
data: { rows: Array<Record<string, any>> };
}>(ENDPOINTS.ALGO_ORDERS, { params: { status: 'INCOMPLETE' } });

const orders = rows.reduce<Order[]>((acc, o) => {
const symbol = normalizeSymbol(o.symbol);
const market = this.store.markets.find((m) => m.symbol === symbol);

if (!market) {
return acc;
}

const childOrders = o.childOrders.map((co: Record<string, any>) => {
const filled = v(co, 'totalExecutedQuantity');

return {
id: v(co, 'algoOrderId'),
status: OrderStatus.Open,
symbol,
type: ORDER_TYPE[v(co, 'algoType')],
side: ORDER_SIDE[co.side],
price: v(co, 'triggerPrice'),
amount: co.quantity,
reduceOnly: v(co, 'reduceOnly'),
filled,
remaining: new BigNumber(co.quantity).minus(filled).toNumber(),
};
});

return [...acc, ...childOrders];
}, []);

return orders;
} catch (err: any) {
this.emitter.emit('error', err?.response?.data?.message || err?.message);
return [];
}
};
}
16 changes: 16 additions & 0 deletions src/exchanges/woo/woo.types.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import { OrderSide, OrderType } from '../../types';

export const RECV_WINDOW = 5000;

export const BASE_URL = {
Expand All @@ -21,7 +23,21 @@ export const ENDPOINTS = {
ACCOUNT: '/v3/accountinfo',
BALANCE: '/v3/balances',
POSITIONS: '/v3/positions',
ALGO_ORDERS: '/v3/algo/orders',
// v1
MARKETS: '/v1/public/info',
TICKERS: '/v1/public/futures',
ORDERS: '/v1/orders',
};

export const ORDER_TYPE: Record<string, OrderType> = {
LIMIT: OrderType.Limit,
MARKET: OrderType.Market,
TAKE_PROFIT: OrderType.TakeProfit,
STOP_LOSS: OrderType.StopLoss,
};

export const ORDER_SIDE: Record<string, OrderSide> = {
BUY: OrderSide.Buy,
SELL: OrderSide.Sell,
};

0 comments on commit e817dc2

Please sign in to comment.