-
Notifications
You must be signed in to change notification settings - Fork 2
Expand file tree
/
Copy pathupbitAPIserver.js
More file actions
484 lines (450 loc) · 16.5 KB
/
upbitAPIserver.js
File metadata and controls
484 lines (450 loc) · 16.5 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
var express = require('express'),
http = require('http');
app = express();
bodyParser = require ("body-parser")
const jwt = require('jsonwebtoken')
const config = require('./config/config.json')
const database = require('./mongodb.js')
const { v4: uuidv4 } = require('uuid');
const BIDFEE = config.bidFee;
const ASKFEE = config.askFee;
const MARKETS = config.markets;
const MINPRICE = config.minPrice;
let _MARKETS_STATUS = {};
let _LOCAL_ALL_ACCOUNTS;
let _LOCAL_ALL_USERSAUTH;
let _CHANGED_ACCESSKEY = {}
setInterval(async ()=>{
for(var _access_key in _CHANGED_ACCESSKEY){
if(_CHANGED_ACCESSKEY[_access_key]){
try{
await database.saveAccount(_access_key, _LOCAL_ALL_ACCOUNTS[_access_key].accounts)
_CHANGED_ACCESSKEY[_access_key] = false
} catch(E){
console.log(E)
}
}
}
},10000)
async function init(){
for(var i in MARKETS){
_MARKETS_STATUS[MARKETS[i]] = {
"ask_price" : ""
,"ask_volume": ""
,"bid_price": ""
,"bid_volume": ""
,"realTimeStamp": ""
,"bid_power": ""
,"ask_power": ""
}
}
await database.init();
_LOCAL_ALL_ACCOUNTS = database.getSavedAccounts();
/*
{
TEST_ACCESSKEY3: {
_id: "TEST_ACCESSKEY3",
accessKey: "TEST_ACCESSKEY3",
accounts: [
{
currency: "ETH",
balance: "2",
avg_buy_price: "0",
unit_currency: "KRW",
timestamp: "6/25/2021, 3:26:39 AM",
},
],
},
TEST_ACCESSKEY1: {
_id: "TEST_ACCESSKEY1",
accessKey: "TEST_ACCESSKEY1",
accounts: [
{
currency: "ETH",
balance: "2",
avg_buy_price: "0",
unit_currency: "KRW",
timestamp: "6/25/2021, 3:29:57 AM",
},
],
},
}
*/
_LOCAL_ALL_USERSAUTH = database.getSavedUserAuth();
/*
{
TEST_ACCESSKEY3: "fGT0g89y6xclPkQNYALqg8ucra+SmT+oaW+EwxWjPXvUcChIXLqDBU0PcdR0ygXKRyWTm3eI4cOUoxtLNcYBIw==",
TEST_ACCESSKEY1: "5PsKOnR6EEr1jH6kLh45KXdHAWI8EfaKGrrU0NuOZdU=",
}
*/
for(var _access_key in _LOCAL_ALL_USERSAUTH){
_CHANGED_ACCESSKEY[_access_key] = false
}
orderbookWS(MARKETS)
}
var server = http.createServer(app);
server.listen(80); //1024 이하의 포트는 특정 cap 권한이 필요합니다.
//web 폴더 밑에 있는 파일들을 요청이 있을때 접근 가능하도록 합니다.
app.use(express.static(__dirname + '/web'));
app.use(bodyParser.json());
//////////////////////////////////// UPBIT API ////////////////
//{"access_key":"MYACCESS_KEY","nonce":"6d90e663-6efb-4123-bcb2-436e386ff66e","iat":1623767
//잘못된 경우 : 401 Unauthorized
////"{\"error\":{\"message\":\"잘못된 엑세스 키입니다.\",\"name\":\"invalid_access_key\"}}"
app.get('/v1/accounts',(req,res)=>{
let retJSON = verifyJWT(req)
let accessKey = retJSON.accessKey
if(typeof accessKey == "undefined") {
res.statusCode = 400;
res.send({error:{"message" : ret.message, "name":"Access Key가 없어요."}})
return;
}
console.log("[Server] /v1/accounts : "+accessKey)
if(retJSON.result){
res.statusCode = 200;
res.send(_LOCAL_ALL_ACCOUNTS[accessKey].accounts)
} else {
res.statusCode = 401;
message = "잘못된 엑세스 키입니다."
database.saveErrorLog(accessKey, message)
res.send({error:{"message" : message, "name":"invalid_access_key"}})
}
})
app.post('/v1/orders',(req,res)=>{
let retJSON = verifyJWT(req)
let accessKey = retJSON.accessKey
if(typeof accessKey == "undefined") {
res.statusCode = 400;
res.send({error:{"message" : ret.message, "name":"Access Key가 없어요."}})
return;
}
console.log("[Server] /v1/orders : "+accessKey)
if(retJSON.result){
ret = order(req, accessKey)
if(ret.result){
io.sockets.emit('UPDATE_LOCAL_ALL_ACCOUNTS', _LOCAL_ALL_ACCOUNTS)
res.send(ret.message)
} else {
res.statusCode = 400;
database.saveErrorLog(accessKey, ret.message)
res.send({error:{"message" : ret.message, "name":"virtualUpbitServer"}})
}
} else {
res.statusCode = 401;
message = "잘못된 엑세스 키입니다."
database.saveErrorLog(accessKey, message)
res.send({error:{"message" : message, "name":"invalid_access_key"}})
}
})
function order(req, access_Key){
try{
let market = req.body.market
let ord_type = req.body.ord_type
let price = parseFloat(req.body.price)
let side = req.body.side
let volume = parseFloat(req.body.volume)
if(!marketValidation(market)) return {result:false, message:"Fail : marketValidation : "+market};
if(!valueCheck([price, volume])) return {result:false, message:"Fail : price/volume Value Check"};
return sellOrBuy(market, side, ord_type, price, volume, access_Key);
} catch(E){
console.log(E);
return {result:false, message:"Fail : Internal Server Error"};
}
}
function valueCheck(arr){
for(var i in arr){
if(arr[i]>0) return true
}
return false;
}
function marketValidation(market){
for(var i in MARKETS){
if(MARKETS[i] == market) return true;
}
return false;
}
//구매 가격 단위로 나누어 떨어지지 않으면 못 산다.
//여기서 price는 호가이다.
//구매할때만 참고하면 됨.
function getTradeUnitKRW(price){
if(price < 10) return 0.01
if(price < 100) return 0.1
if(price < 1000) return 1
if(price < 10000) return 5
if(price < 100000) return 10
if(price < 500000) return 50
if(price < 1000000) return 100
if(price < 2000000) return 500
return 1000
}
//
function getBalanceAfterBuy(price, access_key){
//구매 후 밸런스(남은 돈) 계산.
let balance = 0;
let accounts = _LOCAL_ALL_ACCOUNTS[access_key].accounts
for(var i in accounts){
if(accounts[i].currency == "KRW"){
balance = accounts[i].balance;
break;
}
}
return parseFloat(balance) - price; // 산만큼 빼자. 0보다 작으면 에러.
}
function getVolumeAfterBuy(market, price){
//price 만큼 사고나면 얼마의 volume을 얻게 되는지.
//살 수 있는 가격은 판매되고 있는 가격이다.
ask_price = _MARKETS_STATUS[market].ask_price; // 1볼륨당 가격.
//필요없는 이유 : 시장가 구매하고 시장가 판매하기 때문에 얼마에 걸어놓는게 없음.
// tradeUnit = getTradeUnitKRW(ask_price);
// if(price % tradeUnit !=0){
// return 0 //가격 단위 안 맞음.
// }
volume = parseFloat(price) / parseFloat(ask_price)
return volume;
}
function getPriceAfterSell(market, volume){
//volume 만큼 팔고 나면 얼마의 KRW을 얻게 되는지.
//팔 수 있는 가격은 구매되고 있는 가격이다.
bid_price = _MARKETS_STATUS[market].bid_price
//가격이 개당 1000원이고 내가 5개 팔려고한다?
price = parseFloat(bid_price) * parseFloat(volume);
return price;
}
function sellOrBuy(market, side, ord_type, price, volume, access_key){
if(side == 'bid' && ord_type == "price"){
if(price < MINPRICE){
return {result:false, message:"Fail : Error minimum price : "+price};
}
//balance : 내가 사고나면 남는 돈.
balance = getBalanceAfterBuy(price, access_key);
//사고나면 생기는 볼륨
volume = getVolumeAfterBuy(market, price);
if(volume <= 0){
console.log("["+market+"][BUY]["+access_key+"] 가격 단위 안 맞음 : "+price)
return {result:false, message:"Fail : Error price unit : "+price};
}
return buy(market, volume, price, balance, access_key)
} else if(side == 'ask' && ord_type == 'market'){
//price : volume 만큼 팔고나면 생기는 KRW
price = getPriceAfterSell(market, volume, access_key);
if(price <= 0 || price < MINPRICE){
console.log("["+market+"][SELL]["+access_key+"] PRICE ERROR : "+price)
return {result:false, message:"Fail : Error minimum price : "+price};
}
return sell(market, volume, price, access_key)
//sell
//아직 수수료 계산 안함. 해당 볼륨만큼의 현재 가격을 가져와서 해야됨
} else {
console.log("["+market+"][?]["+access_key+"] ERROR side: "+side + " ord_type : "+ord_type)
//구매도 아니고 판매도 아님
return {result:false, message:"Fail : Error side/ord_type"};
}
}
function buy(market, volume, price, balance, access_key){
//access_key가 market에서 price만큼 사서 volume 만큼 생겼다.
//수수료 계산 필요. 만약 수수료 포함해서 계정에 돈이 모자라면 안산다.
timestamp = new Date().toLocaleString('en', {timeZone: "Asia/Seoul"})
volume = parseFloat(volume);
price = parseFloat(price);
let ask_price = parseFloat(_MARKETS_STATUS[market].ask_price) // 가격 고정 (변동 가능)
try{
fee = price*BIDFEE;
if(balance - fee < 0){
console.log("["+market+"][FEE]["+access_key+"] Not enough balance : "+balance+" / Fee : "+fee)
return {result:false, message:"Fail : Not enough balance"};
}
//돈 충분, 이제 구매하자
//마켓의 Volume은 증가하고, KRW의 돈은 감소한다.
let accounts = _LOCAL_ALL_ACCOUNTS[access_key].accounts
let newMarketFlag = true;
for(var i in accounts){
if(accounts[i].currency == "KRW"){
accounts[i].balance = parseFloat(accounts[i].balance) - price - fee // price 만큼 샀으니 KRW 없애고, 수수료도 빼줌.
accounts[i].timestamp = timestamp
}
if("KRW-"+accounts[i].currency == market){ // 해당 마켓 찾음. 구매해서 볼륨 생김.
//기존의 가격, 볼륨과 사는 가격, 볼륨의 평균
newMarketFlag = false;
accounts[i].avg_buy_price = getAvgPrice(parseFloat(accounts[i].avg_buy_price), parseFloat(accounts[i].balance), ask_price, volume)
//샀으니까 볼륨 수정
accounts[i].balance = parseFloat(accounts[i].balance) + volume;
accounts[i].timestamp = timestamp
}
}
if(newMarketFlag){ //갖고 있지 않은, 새로운 마켓이다.
accounts.push({
currency : market.split("-")[1],
balance : volume,
timestamp : new Date().toLocaleString('en', {timeZone: "Asia/Seoul"}),
avg_buy_price : ask_price,
})
}
}catch(E){
console.log("ERROR : "+access_key);
console.log(E)
return {result:false, message:"Fail : Internal Server Error : BUY"};
}
message = {
uuid : uuidv4(),
side : "bid",
ord_type : "price", //시장가 매수
price : ask_price, //주문당시 화폐가격
avg_price : ask_price, //체결 가격의 평균가격
state : "done", //주문 상태
market : market,
created_at : new Date().toLocaleString('en', {timeZone: "Asia/Seoul"}),
volume : volume, //매수해서 얼마나 생겼냐(사용자가 입력한 것)
remaining_volume : "0.0", //무조건 다 사짐
reserved_fee : "0.0", //수수료 다 써짐
remaining_fee : "0.0",
paid_fee : fee.toString(),
locked : "0.0",
executed_volume : volume, //매수해서 얼마나 생겼냐(실제 집행된 것)
trades_count : 1 // 한번에 무조건 다 사진다
}
_CHANGED_ACCESSKEY[access_key] = true; // 변경사항 존재
database.saveOrderLog(access_key, message);
return {result:true, message:message}
}
//기존의 가격, 볼륨과 사는 가격, 볼륨의 평균
function getAvgPrice(avgPrice1, volume1, avgPrice2, volume2 ){
return ((avgPrice1*volume1) + (avgPrice2*volume2)) / (volume1 + volume2);
}
function sell(market, volume, price, access_key){
let bid_price;
price = parseFloat(price);
volume = parseFloat(volume);
timestamp = new Date().toLocaleString('en', {timeZone: "Asia/Seoul"});
try{
bid_price = price / volume // 마켓의 개당 판매 가격
fee = price*ASKFEE;
//판매하자. 마켓의 Volume은 감소하고, KRW돈은 증가한다.
let accounts = _LOCAL_ALL_ACCOUNTS[access_key].accounts
let newMarketFlag = true;
let sellFlag = false;
for(var i in accounts){
if(accounts[i].currency == "KRW"){
sellFlag = true;
accounts[i].balance = parseFloat(accounts[i].balance) + price - fee // price 만큼 팔았으니 KRW 더해주고, 수수료는 빼줌.
accounts[i].timestamp = timestamp
} else if("KRW-"+accounts[i].currency == market){ // 해당 마켓 찾음. 판매해서 볼륨 사라짐.
//평균 가격은 변하지 않는다.
//팔았으니까 볼륨 감소
newMarketFlag = false;
let balance = parseFloat(accounts[i].balance) - volume;
if(balance < 0){
if(sellFlag){
// 해당 마켓에 갖고 있는게 없는데 계산을 해버렸다. back
accounts[0].balance = parseFloat(accounts[0].balance) - price + fee
return {result:false, message:"Fail : Not Enough Volume : SELL"};
}
//실제 팔지는 않았다.
return {result:false, message:"Fail : Not Enough Volume : SELL"};
}
accounts[i].timestamp = timestamp
accounts[i].balance = balance;
}
}
if(newMarketFlag && sellFlag){
// 해당 마켓에 갖고 있는게 없는데 계산을 해버렸다. back
accounts[0].balance = parseFloat(accounts[0].balance) - price + fee
return {result:false, message:"Fail : Not Enough Volume : SELL"};
}
}catch(E){
console.log("ERROR : "+access_key);
console.log(E)
return {result:false, message:"Fail : Internal Server Error : SELL"};
}
message = {
uuid : uuidv4(),
side : "ask",
ord_type : "market", //시장가 매도
price : bid_price, //주문당시 화폐가격
avg_price : bid_price, //체결 가격의 평균가격
state : "done", //주문 상태
market : market,
created_at : new Date().toLocaleString('en', {timeZone: "Asia/Seoul"}),
volume : volume, //매도해서 얼마나 생겼냐(사용자가 입력한 것)
remaining_volume : "0.0", //무조건 다 팔림
reserved_fee : "0.0", //수수료 다 써짐
remaining_fee : "0.0",
paid_fee : fee.toString(), //사용된 수수료
locked : "0.0",
executed_volume : volume, //매도해서 얼마나 생겼냐(실제 집행된 것)
trades_count : 1 // 한번에 무조건 다 사진다
}
_CHANGED_ACCESSKEY[access_key] = true; // 변경사항 존재
database.saveOrderLog(access_key, message);
return {result:true, message:message}
}
//return : {result : true/false, accessKey}
function verifyJWT(req){
let accessKey = "";
let retJSON = {result:true, accessKey:accessKey};
try{
token = req.headers.authorization.split(" ")[1]
info = token.split(".")[1]
body = Buffer.from(info, "base64").toString('utf8')
jsonBody = JSON.parse(body)
secretKey = _LOCAL_ALL_USERSAUTH[jsonBody.access_key] // TODO
jwt.verify(token, secretKey, (err, verifiedJwt) => {
if(err){
retJSON.result = false;
}else{
accessKey = verifiedJwt.access_key;
retJSON.result = true;
retJSON.accessKey = accessKey
}
})
}catch(E){
retJSON.result = false;
console.log(E)
}
return retJSON;
}
//////////////WEBSOCKET////////////
const WebSocket = require('ws')
function orderbookWS(markets){
ticket = uuidv4()
var ws = new WebSocket('wss://api.upbit.com/websocket/v1');
ws.on('open', ()=>{
ws.send('[{"ticket":"'+ticket+'"},{"type":"orderbook","codes":["'
+markets.join('","')+'"]},{"format":"SIMPLE"}]')
})
ws.on('close', ()=>{
setTimeout(function() {
orderbookWS(markets);
}, 1000);
})
ws.on('message', (data)=>{
try {
var str = data.toString('utf-8')
var json = JSON.parse(str)
market = json.cd
market_state = json.market_state
_MARKETS_STATUS[market].ask_price = json.obu[0].ap;
_MARKETS_STATUS[market].ask_volume = json.obu[0].as;
_MARKETS_STATUS[market].bid_price = json.obu[0].bp;
_MARKETS_STATUS[market].bid_volume = json.obu[0].bs;
timeStamp = new Date(json.tms).toLocaleString();
_MARKETS_STATUS[market].realTimeStamp = timeStamp
//io.sockets.emit("marketData",{cd:market, bp: json.obu[0].bp} )
} catch (e) {
//console.log(e)
}
})
}
var io = require('socket.io')(server);
io.on('connection', function (socket) {
console.log('connect');
socket.on('INIT_LOCAL_ALL_ACCOUNTS', function (data) {
console.log(data);
//io.sockets.in(roomName).emit('recMsg', _LOCAL_ALL_ACCOUNTS);
socket.emit('INIT_LOCAL_ALL_ACCOUNTS', _LOCAL_ALL_ACCOUNTS)
})
socket.on('INIT_MARKETS_STATUS',(data)=>{
console.log(data);
socket.emit('INIT_MARKETS_STATUS', _MARKETS_STATUS);
})
});
init()