Skip to content

Commit 9dd942e

Browse files
committed
refactoring, typing, tests
1 parent 029af53 commit 9dd942e

File tree

9 files changed

+2659
-397
lines changed

9 files changed

+2659
-397
lines changed

package-lock.json

Lines changed: 1843 additions & 12 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 12 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
11
{
2-
"name": "loopringmm",
2+
"name": "loopring-mm-bot",
33
"version": "1.0.0",
4-
"description": "",
4+
"description": "simple loopring market maker bot",
55
"main": "build/server.js",
66
"scripts": {
7-
"starts": "tsc && node build/server.js",
8-
"test": "echo \"Error: no test specified\" && exit 1"
7+
"start": "tsc && node build/server.js",
8+
"test": "mocha -r ts-node/register src/tests/**/*.test.ts"
99
},
1010
"author": "",
1111
"license": "ISC",
@@ -19,12 +19,18 @@
1919
"moment": "^2.29.1",
2020
"restify-clients": "^3.1.0",
2121
"snarkjs": "0.1.20",
22-
"typescript": "^4.1.3",
2322
"ws": "^7.4.2"
2423
},
2524
"devDependencies": {
2625
"@types/bn.js": "^5.1.0",
26+
"@types/chai": "^4.2.14",
2727
"@types/ethereumjs-util": "^6.1.0",
28-
"@types/ws": "^7.4.0"
28+
"@types/mocha": "^8.2.0",
29+
"@types/ws": "^7.4.0",
30+
"chai": "^4.3.0",
31+
"mocha": "^8.2.1",
32+
"nyc": "^15.1.0",
33+
"ts-node": "^9.1.1",
34+
"typescript": "^4.1.3"
2935
}
3036
}

src/marketState.ts

Lines changed: 302 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,302 @@
1+
import BigNumber from "bignumber.js";
2+
import { EventEmitter } from 'events';
3+
import moment from "moment";
4+
import { RestClient } from "./restClient";
5+
import { Config, LoadableValue, Market, Notification, Order, OrderBook, Side, Token } from "./types";
6+
const exchange = require('./sign/exchange.js')
7+
8+
export declare interface MarketState {
9+
on(event: 'maxBidChanged', listener: (maxBid: BigNumber | undefined) => void): this;
10+
on(event: 'minAskChanged', listener: (minAsk: BigNumber | undefined) => void): this;
11+
on(event: 'baseTokenUnallocatedChanged', listener: (unallocated: BigNumber) => void): this;
12+
on(event: 'quoteTokenUnallocatedChanged', listener: (unallocated: BigNumber) => void): this;
13+
on(event: string, listener: Function): this;
14+
}
15+
16+
export class MarketState extends EventEmitter {
17+
private _restClient: RestClient;
18+
private _market: Market;
19+
private _orderBook: OrderBook | undefined;
20+
private _lastOrderBookVersion: number | undefined;
21+
private _maxBid: BigNumber | undefined;
22+
private _minAsk: BigNumber | undefined;
23+
private _openOrders: any;
24+
25+
private _config: Config;
26+
27+
private baseTokenUnit: BigNumber;
28+
private quoteTokenUnit: BigNumber;
29+
30+
private _initialized: boolean;
31+
32+
public baseTokenUnallocated: LoadableValue<BigNumber>;
33+
public quoteTokenUnallocated: LoadableValue<BigNumber>;
34+
public nextStorageIdbaseToken: LoadableValue<number>;
35+
public nextStorageIdquoteToken: LoadableValue<number>;
36+
37+
readonly maxBuyPrice: BigNumber;
38+
readonly minSellPrice: BigNumber;
39+
readonly baseToken: Token;
40+
readonly quoteToken: Token;
41+
42+
constructor(market: Market, baseToken: Token, quoteToken: Token, config: Config, restClient: RestClient) {
43+
super();
44+
this._config = config;
45+
46+
this._restClient = restClient;
47+
this._market = market;
48+
this.maxBuyPrice = new BigNumber(config.maxBuyPrice);
49+
this.minSellPrice = new BigNumber(config.minSellPrice);
50+
this.baseTokenUnallocated = new LoadableValue<BigNumber>();
51+
this.quoteTokenUnallocated = new LoadableValue<BigNumber>();
52+
this.nextStorageIdbaseToken = new LoadableValue<number>();
53+
this.nextStorageIdquoteToken = new LoadableValue<number>();
54+
55+
this.baseToken = baseToken;
56+
this.quoteToken = quoteToken;
57+
58+
this.baseTokenUnit = new BigNumber(10).exponentiatedBy(baseToken.decimals)
59+
this.quoteTokenUnit = new BigNumber(10).exponentiatedBy(quoteToken.decimals)
60+
61+
this._initialized = false;
62+
63+
if (this.maxBuyPrice.isNaN() || this.minSellPrice.isNaN()) {
64+
console.error('maxBuyPrice and minSellPrice MUST be configured.')
65+
process.exit()
66+
}
67+
}
68+
69+
get market(): Market {
70+
return this._market
71+
}
72+
73+
get orderBook(): OrderBook | undefined {
74+
return this._orderBook;
75+
}
76+
77+
get minAsk(): BigNumber | undefined {
78+
return this._minAsk;
79+
}
80+
81+
get maxBid(): BigNumber | undefined {
82+
return this._maxBid;
83+
}
84+
85+
updateOrderBook(version: number, orderbook: OrderBook | undefined) {
86+
if (orderbook && (!this._lastOrderBookVersion || version > this._lastOrderBookVersion)) {
87+
this._orderBook = orderbook;
88+
89+
let __maxBid: string | undefined = undefined;
90+
let __minAsk: string | undefined = undefined;
91+
if (orderbook.bids.length > 0) __maxBid = orderbook.bids[0][0]
92+
if (orderbook.asks.length > 0) __minAsk = orderbook.asks[0][0]
93+
94+
if ((__maxBid && this._maxBid && !this._maxBid.isEqualTo(__maxBid)) ||
95+
(!__maxBid && this._maxBid) ||
96+
(__maxBid && !this._maxBid)) {
97+
this._maxBid = __maxBid !== undefined ? new BigNumber(__maxBid) : undefined
98+
this.emit('maxBidChanged', __maxBid);
99+
}
100+
101+
if ((__minAsk && this._minAsk && !this._minAsk.isEqualTo(__minAsk)) ||
102+
(!__minAsk && this._minAsk) ||
103+
(__minAsk && !this._minAsk)) {
104+
this._minAsk = __minAsk !== undefined ? new BigNumber(__minAsk) : undefined
105+
this.emit('minAskChanged', __minAsk);
106+
}
107+
}
108+
}
109+
110+
get openOrders(): any {
111+
return this._openOrders;
112+
}
113+
114+
set openOrders(oo: any) {
115+
this._openOrders = oo;
116+
}
117+
118+
updateUnallocatedBalance(tokenId: number, total: BigNumber.Value, locked: BigNumber.Value) {
119+
const unallocated = new BigNumber(total).minus(locked);
120+
if (tokenId === this.baseToken.tokenId) {
121+
this.baseTokenUnallocated.set(unallocated)
122+
this.emit('baseTokenUnallocatedChanged', unallocated);
123+
} else if (tokenId === this.quoteToken.tokenId) {
124+
this.quoteTokenUnallocated.set(unallocated)
125+
this.emit('quoteTokenUnallocatedChanged', unallocated);
126+
}
127+
}
128+
129+
updateStorageId(tokenId: number, storageData: any) {
130+
if (storageData?.orderId) {
131+
if (tokenId === this.baseToken.tokenId)
132+
this.nextStorageIdbaseToken.set(storageData.orderId)
133+
else if (tokenId === this.quoteToken.tokenId)
134+
this.nextStorageIdquoteToken.set(storageData.orderId)
135+
console.log(`nextStorageId for ${tokenId} updated (${storageData.orderId})`)
136+
} else {
137+
if (tokenId === this.baseToken.tokenId) {
138+
this.nextStorageIdbaseToken.unset()
139+
} else if (tokenId === this.quoteToken.tokenId) {
140+
this.nextStorageIdquoteToken.unset()
141+
}
142+
}
143+
}
144+
145+
getCounterpartAmount(amount: BigNumber, price: BigNumber, type: Side): string {
146+
if (type === Side.Buy) {
147+
let p = amount.dividedBy(this.quoteTokenUnit);
148+
let t = p.dividedBy(price);
149+
let r = t.multipliedBy(this.baseTokenUnit).toFixed(0);
150+
return r;
151+
} else {
152+
let p = amount.dividedBy(this.baseTokenUnit);
153+
let t = p.multipliedBy(price);
154+
let r = t.multipliedBy(this.quoteTokenUnit).toFixed(0);
155+
return r;
156+
}
157+
}
158+
159+
initialize() {
160+
this._initialized = false;
161+
this.updateBaseTokenStorageId()
162+
this.updateQuoteTokenStorageId()
163+
this.updateBalances()
164+
this.updateOpenOrders()
165+
}
166+
167+
updateBaseTokenStorageId() {
168+
this.nextStorageIdbaseToken.update(async () => {
169+
return this._restClient.getStorageId(this.baseToken.tokenId)
170+
}).then(s => { console.log(`baseToken StorageId updated (${s})`) })
171+
}
172+
173+
updateQuoteTokenStorageId() {
174+
this.nextStorageIdquoteToken.update(async () => {
175+
return this._restClient.getStorageId(this.quoteToken.tokenId)
176+
}).then(s => { console.log(`quoteToken StorageId updated (${s})`) })
177+
}
178+
179+
updateBalances() {
180+
this._restClient.getBalances([this.baseToken.tokenId, this.quoteToken.tokenId])
181+
.then((obj: any) => {
182+
obj.forEach((bal: { tokenId: any; total: any; locked: any }) => {
183+
this.updateUnallocatedBalance(bal.tokenId, bal.total, bal.locked)
184+
});
185+
})
186+
.catch(err => {
187+
console.error('error updating balances', err);
188+
this.quoteTokenUnallocated.unset();
189+
this.baseTokenUnallocated.unset();
190+
})
191+
}
192+
193+
updateOpenOrders() {
194+
this._restClient.getOpenOrders(this.market)
195+
.then((obj: any) => {
196+
this.openOrders = obj.orders;
197+
console.log(`openOrders loaded (${this.openOrders.length})`);
198+
})
199+
.catch(err => {
200+
console.error('error getting open orders', err);
201+
this.openOrders = undefined;
202+
})
203+
}
204+
205+
consumeNotification(notification: Notification) {
206+
var topic = notification.topic.topic;
207+
var data = notification.data;
208+
209+
switch (topic) {
210+
case 'account':
211+
this.updateUnallocatedBalance(data.tokenId, data.totalAmount, data.amountLocked)
212+
break;
213+
case 'orderbook':
214+
if(this.market.market === notification.topic.market)
215+
this.updateOrderBook(notification.endVersion,data);
216+
217+
break;
218+
}
219+
}
220+
221+
prepareNewOrder(amount: BigNumber, price: BigNumber, type: Side) {
222+
let storageId: number;
223+
224+
storageId = type === Side.Buy ? this.nextStorageIdquoteToken.value : this.nextStorageIdbaseToken.value;
225+
226+
return this.prepareOrder(storageId,amount,price,type);
227+
}
228+
229+
prepareUpdateOrder(storageId: number, amount: BigNumber, price: BigNumber, type: Side) {
230+
// TODO
231+
return this.prepareOrder(storageId,amount,price,type);
232+
}
233+
234+
private prepareOrder(storageId: number, amount: BigNumber, price: BigNumber, type: Side): Order | undefined {
235+
let sellTokenId: string;
236+
let sellTokenVolume: string;
237+
let buyTokenId: string;
238+
let buyTokenVolume: string;
239+
240+
if (type === Side.Buy) {
241+
buyTokenId = String(this.baseToken.tokenId);
242+
sellTokenId = String(this.quoteToken.tokenId);
243+
if(amount.isGreaterThan(this.quoteTokenUnallocated.value)) {
244+
console.error('trying to use more than avaibable amount')
245+
return undefined;
246+
}
247+
sellTokenVolume = this.quoteTokenUnallocated.value.toFixed();
248+
buyTokenVolume = this.getCounterpartAmount(amount,price, type)
249+
} else {
250+
buyTokenId = String(this.quoteToken.tokenId);
251+
sellTokenId = String(this.baseToken.tokenId);
252+
if(amount.isGreaterThan(this.baseTokenUnallocated.value)) {
253+
console.error('trying to use more than avaibable amount')
254+
return undefined;
255+
}
256+
sellTokenVolume = this.baseTokenUnallocated.value.toFixed();
257+
buyTokenVolume = this.getCounterpartAmount(amount, price, type)
258+
}
259+
260+
let order: Order = {
261+
"exchange": this._config.account.exchangeAddress,
262+
"accountId": this._config.account.accountId,
263+
"storageId": storageId,
264+
"sellToken": {
265+
"tokenId": sellTokenId,
266+
"volume": sellTokenVolume
267+
},
268+
"buyToken": {
269+
"tokenId": buyTokenId,
270+
"volume": buyTokenVolume
271+
},
272+
"allOrNone": false,
273+
"fillAmountBOrS": type === Side.Buy,
274+
"validUntil": moment().add(2, 'month').utc().unix(),
275+
"maxFeeBips": 50,
276+
"orderType": "MAKER_ONLY"
277+
}
278+
279+
280+
return exchange.signOrder(order,
281+
{
282+
secretKey: this._config.account.privateKey,
283+
publicKeyX: this._config.account.publicKeyX,
284+
publicKeyY: this._config.account.publicKeyY
285+
}, this.baseToken, this.quoteToken);
286+
}
287+
288+
get initialized(): boolean {
289+
if (this._initialized) return true;
290+
291+
if (this.nextStorageIdbaseToken.isAvailable &&
292+
this.nextStorageIdbaseToken.isAvailable &&
293+
this.baseTokenUnallocated.isAvailable &&
294+
this.quoteTokenUnallocated.isAvailable) {
295+
296+
this._initialized = true
297+
return true
298+
}
299+
300+
return false;
301+
}
302+
}

0 commit comments

Comments
 (0)