Skip to content

Commit

Permalink
added foxbit orderbook and all tickers
Browse files Browse the repository at this point in the history
  • Loading branch information
itxtoledo committed Sep 9, 2022
1 parent 3030283 commit d3b6ac4
Show file tree
Hide file tree
Showing 4 changed files with 235 additions and 29 deletions.
5 changes: 4 additions & 1 deletion examples/foxbit.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,7 @@ import { foxbit } from "../src/index";

const ex = new foxbit();

ex.subscribeBook("BTC", "BRL");
ex.getBook("BTC", "BRL").then((book) => console.log("book from foxbit", book));
ex.getAllTickers().then((tickers) =>
console.log("tickers from foxbit", tickers),
);
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "@coinsamba/js-exchanges-connector",
"description": "Collection of JavaScript implementations of cryptocurrency exchange APIs",
"version": "1.3.1",
"version": "1.4.0",
"repository": "[email protected]:coinsambacom/js-exchanges-connector.git",
"author": "Gustavo <[email protected]>",
"license": "MIT",
Expand Down
30 changes: 30 additions & 0 deletions src/connectors/foxbit.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,20 @@ import { alphapoint } from "../interfaces/alphapoint";
import { IExchangeImplementationConstructorArgs } from "../interfaces/exchange";
import { ITicker } from "../types/common";

interface IFoxbitAllTickers {
symbol: string;
price: number;
priceChange24hPercent: number;
name: string;
icon: string;
rank: number;
volume: number;
low: number;
high: number;
marketcap: number;
lastUpdateTimestamp: number;
}

export class foxbit<T> extends alphapoint<T> {
constructor(args?: IExchangeImplementationConstructorArgs<T>) {
super({
Expand All @@ -11,6 +25,8 @@ export class foxbit<T> extends alphapoint<T> {
opts: args?.opts,
limiter: args?.limiter,
});

this.tickerUrl = "https://foxbit.com.br/_api/ticker?cache=0";
}

normalizeAsset(asset: string | number): string | number {
Expand Down Expand Up @@ -42,4 +58,18 @@ export class foxbit<T> extends alphapoint<T> {
vol: res.vol,
};
}

async getAllTickers(): Promise<ITicker[]> {
const res = await this.fetch<IFoxbitAllTickers[]>(this.tickerUrl);

return res.map((ticker) => ({
exchangeId: this.id,
base: ticker.symbol,
quote: "BRL",
last: ticker.price,
ask: ticker.price,
bid: ticker.price,
vol: ticker.volume,
}));
}
}
227 changes: 200 additions & 27 deletions src/interfaces/alphapoint.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,9 @@ import { IOrderbook, IOrderbookOrder, ITicker } from "../types/common";
import { Exchange, IExchangeBaseConstructorArgs } from "./exchange";
import WebSocket from "ws";
import crypto from "crypto";
import * as sort from "fast-sort";

const WEBSOCKET_TIMEOUT_MS = 5000;

interface IAlphapointConstructorArgs<T>
extends IExchangeBaseConstructorArgs<T> {
Expand All @@ -22,6 +25,28 @@ interface IMessageFrame {

type IRawMessageFrame = Omit<IMessageFrame, "i" | "o"> & { o: any };

type SubscribeLevel2 = [
number, // MDUpdateId
number, // Number of Accounts
number, // ActionDateTime in Posix format X 1000
number, // ActionType 0 (New), 1 (Update), 2(Delete)
number, // LastTradePrice
number, // Number of Orders
number, // Price
number, // ProductPairCode
number, // Quantity
// Side 0 bid, 1 ask
number,
];

enum ALPHAPOINT_METHOD {
REQUEST = 0,
REPLY = 1,
SUBSCRIBE = 2,
EVENT = 3,
UNSUBSCRIBE_FROM_EVENT = 4,
}

export class alphapoint<T> extends Exchange<T> {
public baseUrl: any;
public websocketUrl?: string;
Expand All @@ -30,36 +55,60 @@ export class alphapoint<T> extends Exchange<T> {
public wsReady?: boolean;
private wsPingInterval?: NodeJS.Timer;
private resolveMap?: Map<number, (value: unknown) => void>;
private wsBooks!: Map<number, AlphapointOrderbook>;
private wsBooksCbs!: Map<number, ((book: IOrderbook) => void)[]>;

constructor(args: IAlphapointConstructorArgs<T>) {
super({ ...args });
this.eraseWebsocket();
}

private async ensureWebsocket() {
if (!this.ws) {
await this.initWebsocket();
}
}

while (this.ws?.readyState === this.ws?.CONNECTING) {
// wait connection
private eraseWebsocket() {
this.ws = undefined;
this.wsReady = false;
if (this.wsPingInterval) {
clearTimeout(this.wsPingInterval);
}
this.resolveMap = new Map();
this.wsBooks = new Map();
this.wsBooksCbs = new Map();
}

private initWebsocket(): Promise<boolean> {
return new Promise((resolve) => {
return new Promise((resolve, reject) => {
const rejectTimer = setTimeout(() => {
reject("websocket connection timeout");
}, WEBSOCKET_TIMEOUT_MS);

this.ws = new WebSocket(this.websocketUrl!);
this.ws.on("message", (data) => {
//data contém o payload de resposta
const parsedData = JSON.parse(data.toString()) as IMessageFrame;

if (this.id === "foxbit" && parsedData.m === 0) {
parsedData.m = 1;
}

if (parsedData.n === "Ping") {
this.resolveMap!.delete(parsedData.i);
}
} else if (parsedData.m === ALPHAPOINT_METHOD.REPLY) {
const prms = this.resolveMap?.get(parsedData.i);

console.log(parsedData);
if (prms) {
this.resolveMap?.delete(parsedData.i);
prms(parsedData.o ? JSON.parse(parsedData.o) : null);
}
} else if (parsedData.m === ALPHAPOINT_METHOD.EVENT) {
this.parseEvent(parsedData);
}
});
this.resolveMap = new Map();
this.ws.on("open", () => {
this.ws.once("open", () => {
this.wsPingInterval = setInterval(() => {
this.sendFrameToWs({
m: 0,
Expand All @@ -69,18 +118,64 @@ export class alphapoint<T> extends Exchange<T> {
}, 15000);

this.wsReady = true;

clearTimeout(rejectTimer);
resolve(this.wsReady);
});

this.ws.once("close", () => this.eraseWebsocket());
this.ws.once("error", () => this.eraseWebsocket());
});
}

private parseEvent(frame: IMessageFrame) {
if (frame.n === "Level2UpdateEvent") {
const parsedO = JSON.parse(frame.o) as SubscribeLevel2[];
for (const o of parsedO) {
const InstrumentId = o[7];
const book = this.wsBooks.get(InstrumentId);
book!.parseL2(o);
const cbs = this.wsBooksCbs.get(InstrumentId) ?? [];

cbs.forEach((cb) => cb(book!.parsedBook));
}
}
}

// eslint-disable-next-line @typescript-eslint/no-unused-vars
public async subscribeBook(base: string, _quote: string) {
public async subscribeBook(
base: string,
quote: string,
cb: (book: IOrderbook) => void,
): Promise<() => Promise<void>> {
const InstrumentId = this.normalizeAsset(base) as number;
await this.ensureWebsocket();
this.sendFrameToWs({
m: 2,
this.sendFrameToWs<number>({
m: ALPHAPOINT_METHOD.SUBSCRIBE,
n: "SubscribeLevel2",
o: { OMSId: 1, InstrumentId, Depth: 500 },
});

this.wsBooks.set(InstrumentId, new AlphapointOrderbook());

if (!this.wsBooksCbs.has(InstrumentId)) {
this.wsBooksCbs.set(InstrumentId, []);
}

this.wsBooksCbs.get(InstrumentId)!.push(cb);

return async () => {
this.unsubscribeBook(InstrumentId);
};
}

private async unsubscribeBook(InstrumentId: number) {
this.wsBooks.delete(InstrumentId);
// this.wsBooksCbs.clear();
await this.sendFrameToWs({
m: ALPHAPOINT_METHOD.UNSUBSCRIBE_FROM_EVENT,
n: "SubscribeLevel2",
o: { OMSId: 1, InstrumentId: this.normalizeAsset(base), Depth: 50 },
o: {},
});
}

Expand All @@ -93,17 +188,22 @@ export class alphapoint<T> extends Exchange<T> {
};
}

private async sendFrameToWs(rawFrame: IRawMessageFrame) {
const promise = new Promise((resolve, reject) => {
private async sendFrameToWs<T>(rawFrame: IRawMessageFrame): Promise<T> {
const promise = new Promise<T>((resolve, reject) => {
const frame = this.buildMessageFrame(rawFrame);

this.resolveMap!.set(frame.i, resolve);
if (rawFrame.m === ALPHAPOINT_METHOD.REQUEST) {
this.resolveMap!.set(frame.i, (v) => resolve(v as T));
}

const rejectTimer = setTimeout(() => {
if (rawFrame.m === 0 && this.resolveMap!.delete(frame.i)) {
reject("Websocket reply timeout");
if (
rawFrame.m === ALPHAPOINT_METHOD.REQUEST &&
this.resolveMap!.delete(frame.i)
) {
reject("websocket reply timeout");
}
}, 5000);
}, WEBSOCKET_TIMEOUT_MS);

this.ws?.send(JSON.stringify(frame), (err) => {
if (err) {
Expand Down Expand Up @@ -149,25 +249,98 @@ export class alphapoint<T> extends Exchange<T> {

// eslint-disable-next-line @typescript-eslint/no-unused-vars
async getBook(base: string, quote: string): Promise<IOrderbook> {
await this.ensureWebsocket();

base = this.normalizeAsset(base) as string;

const res = await this.fetch<IAlphapointOrderbookRes>(
`${this.baseUrl}/GetL2Snapshot?OMSId=1&InstrumentId=${base}&Depth=50`,
);
const res = await this.sendFrameToWs<IAlphapointOrderbookRes>({
m: ALPHAPOINT_METHOD.REQUEST,
n: "GetL2Snapshot",
o: { OMSId: 1, InstrumentId: base, Depth: 100 },
});

const normalizedBook = {
asks: [] as IOrderbookOrder[],
bids: [] as IOrderbookOrder[],
};

res.forEach((order) => {
if (order[9] === 1) {
normalizedBook.asks.push(this.parseOrder(order));
} else {
normalizedBook.bids.push(this.parseOrder(order));
}
});
if (Array.isArray(res)) {
res.forEach((order) => {
if (order[9] === 1) {
normalizedBook.asks.push(this.parseOrder(order));
} else {
normalizedBook.bids.push(this.parseOrder(order));
}
});
}

return normalizedBook;
}
}

class AlphapointOrderbook {
private book: { asks: SubscribeLevel2[]; bids: SubscribeLevel2[] };

constructor() {
this.book = {
asks: [],
bids: [],
};
}

parseL2(l2: SubscribeLevel2) {
if (l2[3] === 0) {
this.insertOrder(l2);
} else if (l2[3] === 1) {
this.updateOrder(l2);
} else if (l2[3] === 2) {
this.removeOrder(l2);
}
}

private insertOrder(order: SubscribeLevel2) {
if (order[9] === 0) {
this.book.bids.push(order);
sort.sort(this.book.bids).desc((o) => o[6]);
} else {
this.book.asks.push(order);
sort.sort(this.book.asks).asc((o) => o[6]);
}
}

private removeOrder(order: SubscribeLevel2) {
if (order[9] === 0) {
this.book.bids = this.book.bids.filter((o) => o[0] != o[0]);
} else {
this.book.asks = this.book.asks.filter((o) => o[0] != o[0]);
}
}

private updateOrder(order: SubscribeLevel2) {
let orderFound: SubscribeLevel2 | undefined;
if (order[9] === 0) {
orderFound = this.book.bids.find((o) => o[0] === order[0]);
} else {
orderFound = this.book.asks.find((o) => o[0] === order[0]);
}

if (orderFound) {
orderFound[8] = order[8];
orderFound[6] = order[6];
}
}

private parseSingleOrder(order: SubscribeLevel2): IOrderbookOrder {
return {
amount: order[8],
price: order[6],
};
}

get parsedBook() {
return {
asks: this.book.asks.map(this.parseSingleOrder),
bids: this.book.bids.map(this.parseSingleOrder),
};
}
}

0 comments on commit d3b6ac4

Please sign in to comment.