-
Notifications
You must be signed in to change notification settings - Fork 6
/
tools.py
646 lines (566 loc) · 20.3 KB
/
tools.py
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
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
import inspect
import platform
from collections import OrderedDict
from datetime import datetime, timezone
from typing import Callable, Union
import functions
import services as service
from api.api import WS, Markets
from common.data import BotData, Bots, Instrument, MetaInstrument
from common.variables import Variables as var
from display.messages import ErrorMessage
def name(stack) -> str:
if ostype == "Windows":
bot_name = stack[1].filename.split("\\")[-2]
else:
bot_name = stack[1].filename.split("/")[-2]
return bot_name
class Bot(BotData):
def __init__(self) -> None:
bot_name = name(inspect.stack())
bot = Bots[bot_name]
self.__dict__ = bot.__dict__
def remove(self, clOrdID: str) -> None:
"""
Removes the open order by its clOrdID.
Parameters:
-----------
bot: Bot
An instance of a bot in the Bot class.
clOrdID: str
Order ID. Example: "1348642035.Super"
"""
if clOrdID in self.bot_orders:
order = self.bot_orders[clOrdID]
ws = Markets[order["market"]]
WS.remove_order(ws, order=self.bot_orders[clOrdID])
else:
message = "Removing. Order with clOrdID=" + clOrdID + " not found."
var.queue_info.put(
{
"market": "",
"message": message,
"time": datetime.now(tz=timezone.utc),
"warning": "warning",
"emi": self.name,
"bot_log": True,
}
)
def replace(self, clOrdID: str, price: float) -> Union[str, None]:
"""
Moves an open order to a new price using its clOrdID.
Parameters
----------
bot: Bot
An instance of a bot in the Bot class.
clOrdID: str
Order ID. Order ID. Example: "1348642035.Super"
price: float
New price to reset order to.
Returns
-------
str | None
On success, clOrdID is returned, otherwise an error type.
"""
if clOrdID in self.bot_orders:
order = self.bot_orders[clOrdID]
ws = Markets[order["market"]]
res = WS.replace_limit(
ws,
quantity=order["leavesQty"],
price=price,
orderID=order["orderID"],
symbol=order["symbol"],
)
if isinstance(res, dict):
return clOrdID
else:
message = "Replacing. Order with clOrdID=" + clOrdID + " not found."
var.queue_info.put(
{
"market": "",
"message": message,
"time": datetime.now(tz=timezone.utc),
"warning": "warning",
"emi": self.name,
"bot_log": True,
}
)
class Tool(Instrument):
def __init__(self, instrument: Instrument) -> None:
self.__dict__ = instrument.__dict__
self.symbol_tuple = (self.symbol, self.market)
self.instrument = instrument
def close_all(
self,
bot: Bot,
qty: float,
) -> None:
pass
def sell(
self,
bot: Bot,
qty: float = None,
price: float = None,
move: bool = False,
cancel: bool = False,
) -> Union[str, None]:
"""
Sets a sell order.
Parameters
----------
bot: Bot
An instance of a bot in the Bot class.
qty: float
Order quantity. If qty is omitted, then: qty is taken as
minOrderQty.
price: float
Order price. If price is omitted, then price is taken as the
current first offer in the order book.
move: bool
Checks for open sell orders for the current instrument for this
bot and if there are any, takes the last order and moves it to
the new price. If not, places a new order.
cancel: bool
If True, cancels all buy orders for the current instrument for
this bot.
Returns
-------
str | None
If successful, the clOrdID of this order is returned, otherwise
None.
"""
ws = Markets[self.market]
res = None
if not qty:
qty = self.minOrderQty
if not price:
price = self.asks[0][0]
if price:
qty = self._control_limits(side="Sell", qty=qty, bot_name=bot.name)
if qty:
clOrdID = None
if move is True:
clOrdID = self._get_latest_order(orders=bot.bot_orders, side="Sell")
if clOrdID is None:
clOrdID = service.set_clOrdID(emi=bot.name)
res = WS.place_limit(
ws,
quantity=-abs(qty),
price=price,
clOrdID=clOrdID,
symbol=self.symbol_tuple,
)
else:
order = bot.bot_orders[clOrdID]
if order["price"] != price:
res = WS.replace_limit(
ws,
quantity=order["leavesQty"],
price=price,
orderID=order["orderID"],
symbol=order["symbol"],
)
else:
self._empty_orderbook(qty=qty, price=price)
if cancel is not None:
self._remove_orders(orders=bot.bot_orders, side="Buy")
if isinstance(res, dict):
return clOrdID
def buy(
self,
bot: Bot,
qty: float = None,
price: float = None,
move: bool = False,
cancel: bool = False,
) -> Union[str, None]:
"""
Sets a buy order.
Parameters
----------
bot: Bot
An instance of a bot in the Bot class.
qty: float
Order quantity. If qty is omitted, then: qty is taken as
minOrderQty.
price: float
Order price. If price is omitted, then price is taken as the
current first bid in the order book.
move: bool
Checks for open buy orders for the current instrument for this
bot and if there are any, takes the last order and moves it to
the new price. If not, places a new order.
cancel: bool
If True, cancels all buy orders for the current instrument for
this bot.
Returns
-------
str | None
If successful, the clOrdID of this order is returned, otherwise
None.
"""
ws = Markets[self.market]
res = None
if not qty:
qty = self.minOrderQty
if not price:
price = self.bids[0][0]
if price:
qty = self._control_limits(side="Buy", qty=qty, bot_name=bot.name)
if qty:
clOrdID = None
if move is True:
clOrdID = self._get_latest_order(orders=bot.bot_orders, side="Buy")
if clOrdID is None:
clOrdID = service.set_clOrdID(emi=bot.name)
res = WS.place_limit(
ws,
quantity=qty,
price=price,
clOrdID=clOrdID,
symbol=self.symbol_tuple,
)
else:
order = bot.bot_orders[clOrdID]
if order["price"] != price:
res = WS.replace_limit(
ws,
quantity=order["leavesQty"],
price=price,
orderID=order["orderID"],
symbol=order["symbol"],
)
else:
self._empty_orderbook(qty=qty, price=price)
if cancel is not None:
self._remove_orders(orders=bot.bot_orders, side="Sell")
if isinstance(res, dict):
return clOrdID
def EMA(self, period: int) -> float:
pass
def add_kline(self) -> Callable:
"""
Adds kline (candlestick) data to the instrument for the time interval
specified in the bot parameters.
This function is called from each bot's strategy.py file. The time
frame is taken from the bot's parameters. After the bots are
initialized, the kline data is stored in the klines dictionary for
each market respectively. While Tmatic is starting or restarting
a specific market, the kline data is taken from the market's endpoint
according to the klines dictionary. The initial amount of data
loaded from the endpoint is equal to CANDLESTICK_NUMBER in
botinit/variables.py. Then, as the program runs, the data accumulates.
Parameters
----------
No parameters.
Returns
-------
Callable
A callable method that returns the kline data of the specified
instrument. If argumens to this method are omitted, all klines are
returned. The line with the latest date is designated as -1, the
line before the latest is designated -2, and so on. Each line is
a dictionary where:
"date": int
date yymmdd, example 240814
"time": int
time hhmmss, example 143200
"bid": float
first bid price at the beginning of the period
"ask": float
first ask price at the beginning of the period
"hi": float
highest price of the period
"lo": float
lowest price of the period
"funding": float
funding rate for perpetual instruments
"datetime": datetime
date and time in datetime format
Examples
--------
kl = Bybit["BTCUSD"].add_kline()
1. kl()
Return type: list
Returns all klines.
2. kl(-1)
Return type: dict
Returns latest kline data.
3. kl("bid", -1)
Return type: float
Returns open first bid price of the latest period. Possible
values of the first argument: "date", "time", "bid", "ask", "hi",
"lo", "funding", "datetime".
The time frame (timefr) is specified in the bot parameters.
"""
bot_name = name(inspect.stack())
timefr = Bots[bot_name].timefr
ws = Markets[self.market]
functions.add_new_kline(
ws, symbol=self.symbol_tuple, bot_name=bot_name, timefr=timefr
)
return lambda *args: self._kline(timefr, *args)
def set_limit(self, bot: Bot, limit: float) -> None:
"""
Limits bot position for the specified instrument.
Parameters
----------
bot: Bot
An instance of a bot in the Bot class.
limit: float
The limit of positions the bot is allowed to trade on this
instrument. If this parameter is less than the instrument's
minOrderQty, it becomes minOrderQty.
"""
if limit < self.instrument.minOrderQty:
limit = self.instrument.minOrderQty
position = self._get_position(bot_name=bot.name)
position["limits"] = limit
def limit(self, bot: Bot) -> float:
"""
Get bot position limit for the instrument.
Parameters
----------
bot: Bot
An instance of a bot in the Bot class.
Returns
-------
float
Bot position limit for the instrument.
"""
position = self._get_position(bot_name=bot.name)
return position["limits"]
def position(self, bot: Bot) -> float:
"""
Get the bot position for the instrument.
Parameters
----------
bot: Bot
An instance of a bot in the Bot class.
Returns
-------
float
The bot position value for the instrument.
"""
position = self._get_position(bot_name=bot.name)
return position["position"]
def orders(self, bot: Bot, side: str = None, descend: bool = False) -> OrderedDict:
"""
Get the bot orders for the given instrument filtered by instrument
and side.
Parameters
----------
bot: Bot
An instance of a bot in the Bot class.
side: str
The Sell or Buy side of the order. If the parameter is omitted,
both sides are returned.
descend: bool
If omitted, the data is sorted in ascending order by the value of
``transactTime``. If True, descending order is returned.
Returns
-------
OrderedDict
Orders are sorted by ``transactTime`` in the order specified in
the descend parameter. The OrderedDict key is the clOrdID value.
"""
filtered = self._filter_by_side(
orders=bot.bot_orders, side=side, descend=descend
)
return filtered
def _kline(self, timefr, *args) -> Union[dict, list, float]:
"""
Returns kline (candlestick) data.
Parameters
----------
First parameter: str
Can take values: "date", "time", "bid", "ask", "hi", "lo",
"funding", "datetime"
Second parameter: int
The line with the latest date is designated as -1, the
line before the latest is designated -2, and so on.
Returns
-------
dict | list | float
Kline data. For more information, see add_kline().
"""
ws = Markets[self.market]
if not args:
return ws.klines[self.symbol_tuple][timefr]["data"]
elif isinstance(args[0], int):
return ws.klines[self.symbol_tuple][timefr]["data"][args[0]]
else:
return ws.klines[self.symbol_tuple][timefr]["data"][args[1]][args[0]]
def _control_limits(self, side: str, qty: float, bot_name: str) -> float:
"""
When an order is submitted, does not allow the bot to exceed the set
limit for the instrument. Decreases quantity if the limit is exceeded
or returns 0 if the limit is completely exhausted. If the bot does not
have a position for this instrument, then such a position will be
added to the bot, and the limit is set as minOrderQty of the
instrument.
Parameters
----------
side: str
The Sell or Buy side of the order.
qty: float
The quantity of the order.
bot_name: str
Bot name.
"""
position = self._get_position(bot_name=bot_name)
if side == "Sell":
qty = min(max(0, position["position"] + position["limits"]), abs(qty))
else:
qty = min(max(0, position["limits"] - position["position"]), abs(qty))
qty = round(qty, self.precision)
if qty == 0:
message = (
"Position has reached the limit for ("
+ position["symbol"]
+ ", "
+ position["market"]
+ "), limit "
+ str(position["limits"])
+ ", current position "
+ str(position["position"])
+ ". "
+ side
+ " order rejected."
)
var.queue_info.put(
{
"market": "",
"message": message,
"time": datetime.now(tz=timezone.utc),
"warning": "warning",
"emi": bot_name,
"bot_log": True,
}
)
return qty
def _empty_orderbook(self, qty: float, price: float) -> None:
"""
Sends a warning message if the order book is empty.
"""
order = f"Buy qty={qty}, price={price}"
message = ErrorMessage.EMPTY_ORDERBOOK(ORDER=order, SYMBOL=self.symbol_tuple)
var.logger.warning(message)
var.queue_info.put(
{
"market": "",
"message": message,
"time": datetime.now(tz=timezone.utc),
"warning": "warning",
}
)
def _filter_by_side(
self, orders: OrderedDict, side: str = None, descend: bool = False
) -> OrderedDict:
"""
Finds all bot orders on the sell or buy side for a specific instrument.
Parameters
----------
orders: OrderedDict
Bot order dictionary, where the key is clOrdID
side: str
Buy or Sell
descend: bool
If omitted, the data is sorted in ascending order by the value of
``transactTime``. If True, descending order is returned.
Returns
-------
OrderedDict
Orders are sorted by ``transactTime`` in the order specified in
the descend parameter. The OrderedDict key is the clOrdID value.
"""
filtered = OrderedDict()
ord = orders.values()
if descend:
ord = sorted(ord, key=lambda x: x["transactTime"], reverse=True)
for value in ord:
if value["symbol"] == self.symbol_tuple:
if side:
if value["side"] == side:
filtered[value["clOrdID"]] = value
else:
filtered[value["clOrdID"]] = value
return filtered
def _remove_orders(self, orders: OrderedDict, side: str) -> None:
"""
Removes group of given orders by side.
Parameters
----------
orders: OrderedDict
Dictionary where key is clOrdID.
side: str
Buy or Sell
"""
orders = self._filter_by_side(orders=orders, side=side)
for order in orders.values():
ws = Markets[self.market]
WS.remove_order(ws, order=order)
def _get_latest_order(self, orders: OrderedDict, side: str) -> Union[str, None]:
"""
Finds the last order on a given side.
Parameters
----------
orders: OrderedDict
Dictionary where key is clOrdID.
side: str
Buy or Sell
Returns
-------
str | None
If an order is found, returns clOrdID of that order, otherwise None.
"""
orders = self._filter_by_side(orders=orders, side=side)
if orders:
clOrdID = list(orders)[-1]
return clOrdID
def _get_position(self, bot_name: str) -> dict:
"""
Returns the bot's position values.
Parameters
----------
bot_name: str
Bot name.
Returns
-------
dict
All bot's position values: "emi", "symbol", "category", "limits",
"ticker", "position", "volume", "sumreal", "commiss", "ltime",
"pnl", "lotSize", "currency", "limits".
"""
bot = Bots[bot_name]
if self.symbol_tuple not in bot.bot_positions:
service.fill_bot_position(
bot_name=bot_name,
symbol=self.symbol_tuple,
instrument=self.instrument,
user_id=Markets[self.market].user_id,
)
return bot.bot_positions[self.symbol_tuple]
class MetaTool(type):
objects = dict()
def __getitem__(self, item) -> Tool:
market = self.__qualname__
instrument = (item, market)
if instrument not in MetaInstrument.all:
raise ValueError(f"The instrument {instrument} not found.")
if instrument not in self.objects:
self.objects[instrument] = Tool(MetaInstrument.all[instrument])
return self.objects[instrument]
if platform.system() == "Windows":
ostype = "Windows"
elif platform.system() == "Darwin":
ostype = "Mac"
else:
ostype = "Linux"
class Bitmex(metaclass=MetaTool):
pass
class Bybit(metaclass=MetaTool):
pass
class Deribit(metaclass=MetaTool):
pass