-
Notifications
You must be signed in to change notification settings - Fork 4
/
LazyStarter.py
2058 lines (1955 loc) · 89.2 KB
/
LazyStarter.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
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
# -*- coding: utf-8 -*-
# Command Line interface to interact with poloniex
# if you don't get it, don't use it
import ccxt
import logging
import logging.handlers
import json
import sys
import os
import zebitexFormatted
from time import time, sleep
from copy import deepcopy
from decimal import *
from pathlib import Path
from datetime import datetime
from operator import itemgetter
class LazyStarter:
getcontext().prec = 15
def __init__(self):
# Without assigning it first, it always return true
self.script_position = os.path.dirname(sys.argv[0])
self.root_path = f'{self.script_position}/' if self.script_position else ''
self.keys_file = f'{self.root_path}keys.txt'
self.stratlog = self.logger_setup('stratlogs', 'strat.log',
'%(message)s', logging.DEBUG, logging.INFO)
self.applog = self.logger_setup('debugs', 'app.log',
'%(asctime)s - %(levelname)s - %(message)s', logging.DEBUG,
logging.DEBUG)
self.user_market_name_list = []
self.ccxt_exchanges_list = self.exchanges_list_init()
self.keys = self.keys_initialisation()
self.exchange = None
self.fees_coef = Decimal('0.9975')
self.user_balance = {}
self.selected_market = None
self.open_orders = {'sell': [], 'buy': []}
self.history = {'sell': [], 'buy': []}
self.params = {}
self.intervals = []
self.err_counter = 0
self.now = 0
self.safety_buy_value = Decimal('0.00000001')
self.safety_sell_value = Decimal('1')
self.max_sell_index = None
"""
########################## __INIT__ + MANDATORY ###########################
"""
def logger_setup(self, name, log_file, log_formatter, console_level,
file_level, logging_level=logging.DEBUG):
"""Generate logging systems which display any level on the console
and starting from INFO into logging file
name: string, name of the logger,
log_file: string, name of the file where to place the log datas.
log_formatter: string, how the log is formated. See Formatter logging
rules.
console_level: logging object, the logging level to display in the
console. Need to be superior to logging_level.
file_level: logging object, the logging level to put in the
logging file. Need to be superior to logging_level.
logging_level: logging object, optional, the level of logging to catch.
return: logging object, contain rules for logging.
"""
dir_name = f'{self.root_path}logfiles'
self.create_dir_when_none('logfiles')
log_file = f'{dir_name}/{log_file}'
logger = logging.getLogger(name)
logger.setLevel(logging_level)
formatter = logging.Formatter(log_formatter)
# Console handler stream
ch = logging.StreamHandler()
ch.setLevel(console_level)
ch.setFormatter(formatter)
# File Handler stream
fh = logging.FileHandler(log_file)
fh.setLevel(file_level)
fh.setFormatter(formatter)
logger.addHandler(ch)
logger.addHandler(fh)
handler = logging.handlers.RotatingFileHandler(
log_file, maxBytes=2000000, backupCount=20)
logger.addHandler(handler)
return logger
def exchanges_list_init(self):
"""Little hack to add zebitex to ccxt exchange list.
return: list, list of exchanges."""
ccxt_exchanges_list = ccxt.exchanges
return ccxt_exchanges_list + ['zebitex', 'zebitex_testnet']
def keys_initialisation(self): # Need to be refactored
"""Check if a key.txt file exist and create one if none.
return: dict, with all api keys found.
"""
if not os.path.isfile(self.keys_file):
Path(self.keys_file).touch()
msg = (
f'No file was found, an empty one has been created, '
f'please fill it as indicated in the documentation'
)
self.applog.critical(msg)
self.exit()
else:
keys = self.keys_file_reader()
if not keys:
msg = (
f'Your key.txt file is empty, please '
f'fill it as indicated to the documentation'
)
self.applog.critical(msg)
self.exit()
else:
return keys
def keys_file_reader(self): # Need to be refactored
"""Check the consistence of datas in key.txt.
return: dict, api keys
"""
keys = {}
with open(self.keys_file , mode='r', encoding='utf-8') as keys_file:
for line in keys_file:
line = line.replace('\n', '')
line = line.replace("'", '"')
try:
key = json.loads(line)
for k in key.keys():
if k in self.user_market_name_list:
msg = (
f'You already have a key for this '
f'marketplace, please RTFM'
)
raise KeyError(msg)
else:
self.user_market_name_list.append(k)
if k not in self.ccxt_exchanges_list:
raise NameError('The marketplace name is invalid!')
except Exception as e:
self.applog.critical(f'Something went wrong : {e}')
self.exit()
keys.update(key)
return keys
def select_marketplace(self):
"""Marketplace sélection menu, connect to the selected marketplace.
return: string, the name of the selected marketplace
"""
q = 'Please select a market:'
choice = self.ask_to_select_in_a_list(q, self.user_market_name_list)
if self.user_market_name_list[choice] == 'zebitex':
self.exchange = zebitexFormatted.ZebitexFormatted(
self.keys[self.user_market_name_list[choice]]['apiKey'],
self.keys[self.user_market_name_list[choice]]['secret'],
False)
elif self.user_market_name_list[choice] == 'zebitex_testnet':
self.exchange = zebitexFormatted.ZebitexFormatted(
self.keys[self.user_market_name_list[choice]]['apiKey'],
self.keys[self.user_market_name_list[choice]]['secret'],
True)
else:
msg = (
f'ccxt.{self.user_market_name_list[choice]}'
f'({str(self.keys[self.user_market_name_list[choice]])})'
)
self.exchange = eval(msg)
def select_market(self):
"""Market selection menu.
return: string, selected market.
"""
self.load_markets()
market_list = self.exchange.symbols
valid_choice = False
while valid_choice is False:
self.applog.info(f'Please enter the name of a market: {market_list}')
choice = input(' >> ').upper()
limitation = self.limitation_to_btc_market(choice)
if limitation is True:
if choice in market_list:
self.selected_market = choice
valid_choice = True
else:
self.applog.info(limitation)
return choice
"""
######################## DATA CHECKER/FORMATTER ###########################
"""
def log_file_reader(self):
"""Import the last 20 order from strat.log and organize it.
return: None or dict containing : list of exectuted buy,
list of executed sell,
dict of parameters
"""
strat_log_file = f'{self.root_path}logfiles/strat.log'
raw_data = []
logs_data = {'buy': [], 'sell': []}
# In case there is no log file
if not self.create_file_when_none(strat_log_file):
self.applog.warning("params.txt file have been created")
return
self.applog.debug("Reading the strat.log file")
nb_of_lines = self.file_line_counter(strat_log_file)
# In case the log file is empty
if not nb_of_lines:
self.applog.warning('Your strat.log file was empty')
return
target = nb_of_lines - 20 if nb_of_lines > 20 else 0
# Get the last 20 orders saved in log file
while target < nb_of_lines:
line = self.read_one_line(strat_log_file, nb_of_lines)
try:
line = json.loads(line)
raw_data.append(line)
except Exception as e:
target = target - 1 if target - 1 >= 0 else target
nb_of_lines -= 1
# It's better when it's pretty to display
for order in raw_data:
formated_order = self.format_log_order(
order['side'],
order['order_id'],
order['price'],
order['amount'],
order['timestamp'],
order['datetime'])
if order['side'] == 'buy'or\
order['side'] == 'canceled_buy':
logs_data['buy'].append(formated_order)
if order['side'] == 'sell' or\
order['side'] == 'canceled_sell':
logs_data['sell'].append(formated_order)
self.display_user_trades(logs_data)
return logs_data
def params_reader(self, file_path):
"""Check the integrity of all parameters and return False if it's not.
file_path: string, params.txt relative path.
return: dict with valid parameters, or False.
"""
if not self.create_file_when_none(file_path):
self.applog.warning('There was no params.txt. One have been created')
return
try:
params = json.loads(self.read_one_line(file_path, 0))
except Exception as e:
msg = f'Something went wrong when loading params: {e}'
self.applog.warning(msg)
return
try:
# Check if values exist
if not params['datetime']:
raise ValueError('Datetime isn\'t set')
if not params['market']:
raise ValueError('Market isn\'t set')
if not params['range_bot']:
raise ValueError('The bottom of the range isn\'t set')
if not params['range_top']:
raise ValueError('The top of the range isn\'t set')
if not params['spread_bot']:
raise ValueError('The bottom of the spread isn\'t set')
if not params['spread_top']:
raise ValueError('The bottom of the spread isn\'t set')
if not params['increment_coef']:
raise ValueError('Increment coeficient isn\'t set')
if not params['amount']:
raise ValueError('Amount isn\'t set')
if not params['stop_at_bot']:
raise ValueError('Stop at bottom isn\'t set')
if not params['stop_at_top']:
raise ValueError('Stop at top isn\'t set')
if not params['nb_buy_to_display']:
raise ValueError('Number of buy displayed isn\'t set')
if not params['nb_sell_to_display']:
raise ValueError('Number of sell displayed isn\'t set')
if not params['benef_alloc']:
raise ValueError('Benefices allocation isn\'t set')
# Convert values
error_message = f"params['range_bot'] is not a string:"
params['range_bot'] = self.str_to_decimal(
params['range_bot'], error_message)
error_message = f"params['range_top'] is not a string:"
params['range_top'] = self.str_to_decimal(
params['range_top'], error_message)
error_message = f"params['spread_bot'] is not a string:"
params['spread_bot'] = self.str_to_decimal(params['spread_bot'],
error_message)
error_message = f"params['spread_top'] is not a string:"
params['spread_top'] = self.str_to_decimal(params['spread_top'],
error_message)
error_message = f"params['increment_coef'] is not a string:"
params['increment_coef'] = self.str_to_decimal(
params['increment_coef'], error_message)
error_message = f"params['amount'] is not a string:"
params['amount'] = self.str_to_decimal(
params['amount'], error_message)
error_message = f"params['stop_at_bot'] is not a boolean:"
params['stop_at_bot'] = self.str_to_bool(params['stop_at_bot'],
error_message)
error_message = f"params['stop_at_top'] is not a boolean:"
params['stop_at_top'] = self.str_to_bool(params['stop_at_top'],
error_message)
error_message = f"params['nb_buy_to_display'] is not an int:"
params['nb_buy_to_display'] = self.str_to_int(
params['nb_buy_to_display'], error_message)
error_message = f"params['nb_sell_to_display'] is not an int:"
params['nb_sell_to_display'] = self.str_to_int(
params['nb_sell_to_display'], error_message)
error_message = f"params['benef_alloc'] is not an int:"
params['benef_alloc'] = self.str_to_decimal(params['benef_alloc'],
error_message)
self.applog.debug(f'param_checker, params: {params}')
# Test if values are correct
self.is_date(params['datetime'])
if params['market'] not in self.exchange.symbols:
msg = 'Market isn\'t set properly for this marketplace'
raise ValueError(msg)
if params['market'] != self.selected_market:
msg = (
f'self.selected_market: {self.selected_market}'
f'!= params["market"] {params["market"]}'
)
raise ValueError(msg)
market_test = self.limitation_to_btc_market(params['market'])
if market_test is not True:
raise ValueError(market_test[1])
self.param_checker_range_bot(params['range_bot'])
self.param_checker_range_top(params['range_top'])
self.param_checker_interval(params['increment_coef'])
self.intervals = self.interval_generator(params['range_bot'],
params['range_top'],
params['increment_coef'])
if self.intervals is False:
msg = (
f'Range top value is too low, or increment too '
f'high: need to generate at lease 6 intervals.'
)
raise ValueError(msg)
if params['spread_bot'] not in self.intervals:
raise ValueError('Spread_bot isn\'t properly configured')
spread_bot_index = self.intervals.index(params['spread_bot'])
if params['spread_top'] != self.intervals[spread_bot_index + 1]:
raise ValueError('Spread_top isn\'t properly configured')
self.param_checker_amount(params['amount'], params['spread_bot'])
self.param_checker_benef_alloc(params['benef_alloc'])
except Exception as e:
self.applog.warning(f'The LW parameters are not well configured: {e}')
return False
return params
def create_dir_when_none(self, dir_name):
"""Check if a directory exist or create one.
return: bool."""
if not os.path.isdir(dir_name):
os.makedirs(dir_name)
return False
else:
return True
def create_file_when_none(self, file_name): # Need to be refactored
"""Check if a file exist or create one.
return: bool.
"""
if not os.path.isfile(file_name):
Path(file_name).touch()
return False
else:
return True
def logfile_not_empty(self, file_name): # Need to be refactored
"""Check if there is data in the logfile.
return : bool.
"""
if os.path.getsize(file_name):
return True
else:
self.applog.info('Logfile is empty!')
return False
def read_one_line(self, file_name, line_nb):
"""Read and return a specific line in a file.
return: string."""
with open(file_name) as f:
return f.readlines()[line_nb].replace('\n', '').replace("'", '"')
def file_line_counter(self, file_name):
"""Line counter for any file.
return: int, number of line. Start at 0."""
try:
with open(file_name, mode='r', encoding='utf-8') as log_file:
for i, l in enumerate(log_file):
pass
return i
except NameError:
self.applog.info(f'{file_name} is empty')
return
def simple_file_writer(self, file_name, text):
"""Write a text in a file.
file_name: string, full path of the file.
text: string.
return: boolean.
"""
try:
with open(file_name, mode='w', encoding='utf-8') as file:
file.write(text)
return True
except Exception as e:
self.applog.critical(f'File writer error: {e}')
self.exit()
def str_to_decimal(self, s, error_message=None):
"""Convert a string to Decimal or raise an error.
s: string, element to convert
error_message: string, error message detail to display if fail.
return: Decimal."""
try:
return Decimal(str(s))
except Exception as e:
raise ValueError(f'{error_message} {e}')
def is_date(self, str_date):
"""Check if a date have a valid formating.
str_date: string
"""
try:
return datetime.strptime(str_date, '%Y-%m-%d %H:%M:%S.%f')
except Exception as e:
raise ValueError(f'{str_date} is not a valid date: {e}')
def str_to_bool(self, s, error_message=None): #Fancy things can be added
"""Convert a string to boolean or rise an error
s: string.
error_message: string, error message detail to display if fail.
return: bool.
"""
if s == 'True' or s == 'y':
return True
elif s == 'False' or s == 'n':
return False
else:
raise ValueError(f'{error_message} {e}')
def str_to_int(self, s, error_message=None):
"""Convert a string to an int or rise an error
s: string.
error_message: string, error message detail to display if fail.
return: int.
"""
try:
return int(s)
except Exception as e:
raise ValueError(f'{error_message} {e}')
def dict_to_str(self, a_dict):
"""Format dict into a string.
return: string, formated string for logfile."""
b_dict = deepcopy(a_dict)
for key, value in b_dict.items():
b_dict[key] = str(value)
b_dict = str(b_dict)
return b_dict.replace("'", '"')
def timestamp_formater(self):
"""Format time.time() into the same format as timestamp
used in ccxt: 13 numbers.
return: int, formated timestamp"""
timestamp = str(time()).split('.')
return int(timestamp[0] + timestamp[1][:3])
def limitation_to_btc_market(self, market):
"""Special limitation to BTC market : only ALT/BTC for now.
market: string, market name.
return: bool True or bool False + error message
"""
if market[-3:] != 'BTC':
return f'LW is limited to ALT/BTC markets : {market}'
return True
def param_checker_range_bot(self, range_bot):
"""Verifies the value of the bottom of the channel
range_bot: decimal"""
if range_bot < Decimal('0.00000001'):
raise ValueError('The bottom of the range is too low')
return True
def param_checker_range_top(self, range_top):
"""Verifies the value of the top of the channel
range_top: decimal"""
if range_top > Decimal('0.99'):
raise ValueError('The top of the range is too high')
return True
def param_checker_interval(self, interval):
"""Verifies the value of interval between orders
interval: decimal"""
if Decimal('1.01') > interval or interval > Decimal('1.50'):
raise ValueError('Increment is too low (<=1%) or high (>=50%)')
return True
def param_checker_amount(self, amount, minimum_amount):
"""Verifies the value of each orders
amount: decimal"""
if amount < minimum_amount or amount > Decimal('10000000'):
raise ValueError(f'Amount is too low (< {minimum_amount} \
) or high (>10000000)')
def param_checker_nb_to_display(self, nb):
"""Verifie the nb of order to display
nb: int"""
if nb > len(self.intervals) and nb < 0:
msg = (
f'The number of order to display is too low (<0) '
f'or high {len(self.intervals)}'
)
raise ValueError(msg)
return True
def param_checker_benef_alloc(self, nb):
"""Verifie the nb for benefice allocation
nb: int"""
if Decimal('0') <= nb >= Decimal('100'):
msg = (
f'The benefice allocation too low (<0) or high '
f'(>100) {nb}'
)
raise ValueError(msg)
return True
def multiplier(self, nb1, nb2, nb3=Decimal('1')):
"""Do a simple multiplication between Decimal.
nb1: Decimal.
nb2: Decimal.
nb3: Decimal, optional.
return: Decimal.
"""
return self.quantizator(nb1 * nb2 * nb3)
def quantizator(self, nb):
"""Format a Decimal object to 8 decimals
return: Decimal"""
return nb.quantize(Decimal('.00000001'), rounding=ROUND_HALF_EVEN)
def interval_generator(self, range_bottom, range_top, increment):
"""Generate a list of interval inside a range by incrementing values
range_bottom: Decimal, bottom of the range
range_top: Decimal, top of the range
increment: Decimal, value used to increment from the bottom
return: list, value from [range_bottom, range_top[
"""
intervals = [range_bottom]
intervals.append(self.multiplier(intervals[-1], increment))
if range_top <= intervals[1]:
raise ValueError('Range top value is too low')
while intervals[-1] <= range_top:
intervals.append(self.multiplier(intervals[-1], increment))
del intervals[-1]
if len(intervals) < 6:
msg = (
f'Range top value is too low, or increment too '
f'high: need to generate at lease 6 intervals. Try again!'
)
raise ValueError(msg)
return intervals
def increment_coef_buider(self, nb):
"""Formating increment_coef.
nb: int, the value to increment in percentage.
return: Decimal, formated value.
"""
try:
nb = Decimal(str(nb))
nb = Decimal('1') + nb / Decimal('100')
self.param_checker_interval(nb)
return nb
except Exception as e:
raise ValueError(e)
def check_for_enough_funds(self, params):
"""Check if the user have enough funds to run LW with he's actual
parameters.
Printed value can be negative!
Ask for params change if there's not.
params: dict, parameters for LW.
return: dict, params"""
is_valid = False
# Force user to set strategy parameters in order to have enough funds
# to run the whole strategy
while is_valid is False:
price = self.get_market_last_price(self.selected_market)
self.get_balances()
pair = self.selected_market.split('/')
sell_balance = self.str_to_decimal(self.user_balance[pair[0]]['free'])
buy_balance = self.str_to_decimal(self.user_balance[pair[1]]['free'])
spread_bot_index = self.intervals.index(params['spread_bot'])
spread_top_index = spread_bot_index + 1
try:
total_buy_funds_needed = self.calculate_buy_funds(
spread_bot_index, params['amount'])
total_sell_funds_needed = self.calculate_sell_funds(
spread_top_index, params['amount'])
msg = (
f'check_for_enough_funds total_buy_funds_needed: '
f'{total_buy_funds_needed}, buy_balance: {buy_balance}, '
f'total_sell_funds_needed: {total_sell_funds_needed}, '
f'sell_balance: {sell_balance}, price: {price}'
)
self.applog.debug(msg)
# When the strategy will start with spread bot inferior or
# equal to the actual market price
if params['spread_bot'] <= price:
incoming_buy_funds = Decimal('0')
i = spread_top_index
# When the whole strategy is lower than actual price
if params['range_top'] < price:
while i < len(self.intervals):
incoming_buy_funds += self.multiplier(
self.intervals[i], params['amount'],
self.fees_coef)
i +=1
# When only few sell orders are planned to be under the
# actual price
else:
while self.intervals[i] <= price:
incoming_buy_funds += self.multiplier(
self.intervals[i], params['amount'],
self.fees_coef)
i +=1
# It crash when price >= range_top
if i == len(self.intervals):
break
total_buy_funds_needed = total_buy_funds_needed -\
incoming_buy_funds
# When the strategy will start with spread bot superior to the
# actual price on the market
else:
incoming_sell_funds = Decimal('0')
i = spread_bot_index
# When the whole strategy is upper than actual price
if params['spread_bot'] > price:
while i >= 0:
incoming_sell_funds += self.multiplier(
params['amount'], self.fees_coef)
i -=1
# When only few buy orders are planned to be upper the
# actual price
else:
while self.intervals[i] >= price:
incoming_sell_funds += self.multiplier(
params['amount'], self.fees_coef)
i -=1
if i < 0:
break
total_sell_funds_needed = total_sell_funds_needed\
- incoming_sell_funds
msg = (
f'Your actual strategy require: {pair[1]} needed: '
f'{total_buy_funds_needed} and you have {buy_balance} '
f'{pair[1]}; {pair[0]} needed: {total_sell_funds_needed}'
f' and you have {sell_balance} {pair[0]}.'
)
self.applog.debug(msg)
# In case there is not enough funds, check if there is none stuck
# before asking to change params
if total_buy_funds_needed > buy_balance:
buy_balance = self.look_for_moar_funds(total_buy_funds_needed,
buy_balance, 'buy')
if total_sell_funds_needed > sell_balance:
sell_balance = self.look_for_moar_funds(
total_sell_funds_needed, sell_balance, 'sell')
if total_buy_funds_needed > buy_balance or\
total_sell_funds_needed > sell_balance:
raise ValueError('You don\'t own enough funds!')
is_valid = True
except ValueError as e:
self.stratlog.warning('%s\nYou need to change some parameters:', e)
params = self.change_params(params)
return params
def calculate_buy_funds(self, index, amount):
"""Calculate the buy funds required to execute the strategy
amount: Decimal, allocated ALT per order
return: Decimal, funds needed
"""
buy_funds_needed = Decimal('0')
i = 0
while i <= index:
buy_funds_needed += self.intervals[i] * amount
i += 1
return buy_funds_needed
def calculate_sell_funds(self, index, amount):
"""Calculate the sell funds required to execute the strategy
amount: Decimal, allocated ALT per order
return: Decimal, funds needed
"""
sell_funds_needed = Decimal('0')
i = len(self.intervals) -1
while i >= index:
sell_funds_needed += amount
i -= 1
return sell_funds_needed
def look_for_moar_funds(self, funds_needed, funds, side):
"""Look into open orders how much funds there is, offer to cancel orders not
in the strategy.
funds_needed: Decimal, how much funds are needed for the strategy.
funds: Decimal, sum of available funds for the strategy.
side: string, buy or sell.
return: Decimal, sum of available funds for the strategy."""
orders = self.orders_price_ordering(
self.get_orders(self.selected_market))
orders_outside_strat = []
# simple addition of funds stuck in open order and will be used for the
# strategy
if side == 'buy':
for order in orders['buy']:
if order[1] in self.intervals\
or order[1] == self.safety_buy_value:
funds += order[1] * order[2]
else:
orders_outside_strat.append(order)
else:
for order in orders['sell']:
if order[1] in self.intervals\
or order[1] == self.safety_sell_value:
funds += order[2]
else:
orders_outside_strat.append(order)
# If there is still not enough funds but there is open orders outside the
# strategy
if funds > Decimal('0'):
if orders_outside_strat:
is_valid = False
while is_valid is False:
if not orders_outside_strat:
is_valid = True
q = (
f'Do you want to remove some orders outside of the '
f'strategy to get enough funds to run it? (y or n)'
)
if self.simple_question(q):
q = 'Which order do you want to remove:'
rsp = self.ask_to_select_in_a_list(q,
orders_outside_strat)
del orders_outside_strat[rsp]
rsp = self.cancel_order(orders_outside_strat[rsp][0],
orders_outside_strat[rsp][1],
orders_outside_strat[rsp][4], side)
if rsp:
if side == 'buy':
funds += order[1] * order[2]
else:
funds += order[2]
self.stratlog.debug(
f'You have now {funds} {side} '
f'funds and you need {funds_needed}.')
else:
is_valid = True
return funds
"""
######################### USER INTERACTION ################################
"""
def simple_question(self, q): #Fancy things can be added
"""Simple question prompted and response handling.
q: string, the question to ask.
return: boolean True or None, yes of no
"""
while True:
self.applog.info(q)
choice = input(' >> ')
self.applog.debug(choice)
if choice == 'y':
return True
if choice == 'n':
return False
def ask_question(self, q, formater_func, control_func=None):
"""Ask any question to the user, control the value returned or ask again.
q: string, question to ask to the user.
formater_funct: function, format from string to the right datatype.
control_funct: optional function, allow to check that the user's choice is
within the requested parameters
return: formated (int, decimal, ...) choice of the user
"""
self.applog.info(q)
while True:
try:
choice = input(' >> ')
self.applog.debug(choice)
choice = formater_func(choice)
if control_func:
control_func(choice)
return choice
except Exception as e:
self.applog.info(f'{q} invalid choice: {choice} -> {e}')
def ask_to_select_in_a_list(self, q, a_list):
"""Ask to the user to choose between items in a list
a_list: list.
q: string.
return: int, the position of this item """
self.applog.info(q)
q = ''
for i, item in enumerate(a_list, start=1):
q += f'{i}: {item}, '
self.applog.info(q)
while True:
try:
choice = input(' >> ')
self.applog.debug(choice)
choice = self.str_to_int(choice)
if 0 < choice <= i:
return choice - 1
else:
msg = f'You need to enter a number between 1 and {i}'
self.applog.info(msg)
except Exception as e:
self.applog.info(f'{q} invalid choice: {choice} -> {e}')
return choice
def ask_param_range_bot(self):
"""Ask the user to enter a value for the bottom of the range.
return: decimal."""
q = (
f'Enter a value for the bottom of the range. It must be '
f'superior to 1 stats:')
return self.ask_question(q, self.str_to_decimal,
self.param_checker_range_bot)
def ask_param_range_top(self):
"""Ask the user to enter a value for the top of the range.
return: decimal."""
q = (
f'Enter a value for the top of the range. It must be '
f'inferior to 0.99 BTC:'
)
return self.ask_question(q, self.str_to_decimal,
self.param_checker_range_top)
def ask_param_amount(self, range_bot):
"""Ask the user to enter a value of ALT to sell at each order.
return: decimal."""
minimum_amount = Decimal('0.001') / range_bot
q = (
f'How much {self.selected_market[:4]} do you want to sell '
f'per order? It must be between {minimum_amount} and 10000000:')
while True:
try:
amount = self.ask_question(q, self.str_to_decimal)
self.param_checker_amount(amount, minimum_amount)
return amount
except Exception as e:
self.applog.warning(e)
def ask_param_increment(self):
"""Ask the user to enter a value for the spread between each order.
return: decimal."""
q = (
f'How much % of spread between two orders? It must be '
f'between 1% and 50%')
return self.ask_question(q, self.increment_coef_buider)
def ask_range_setup(self):
"""Ask to the user to enter the range and increment parameters.
return: dict, asked parameters."""
is_valid = False
while is_valid is False:
try:
range_bot = self.ask_param_range_bot()
range_top = self.ask_param_range_top()
increment = self.ask_param_increment()
intervals = self.interval_generator(range_bot, range_top,
increment)
is_valid = True
except Exception as e:
self.applog.warning(e)
self.intervals = intervals
return {'range_bot': range_bot, 'range_top': range_top,
'increment_coef': increment}
def ask_params_spread(self):
"""Ask to the user to choose between value for spread bot and setup
spread top automatically
return: dict, of decimal values
"""
price = self.get_market_last_price(self.selected_market)
msg = f'The actual price of {self.selected_market} is {price}'
self.applog.info(msg)
q = (
f'Please select the price of your highest buy order '
f'(spread_bot) in the list')
position = self.ask_to_select_in_a_list(q, self.intervals)
return {'spread_bot': self.intervals[position],
'spread_top': self.intervals[position + 1]} # Can be improved by suggesting a value
def ask_nb_to_display(self):
"""Ask how much buy and sell orders are going to be in the book.
return: dict, nb_buy_to_display + nb_sell."""
q = (
f'How many buy orders do you want to display? It must be '
f'less than {len(self.intervals)}. 0 value = '
f'{len(self.intervals)} :')
nb_buy_to_display = self.ask_question(q, self.str_to_int,
self.param_checker_nb_to_display)
q = (
f'How many sell orders do you want to display? It must be '
f'less than {len(self.intervals)}. 0 value = '
f'{len(self.intervals)} :')
nb_sell_to_display = self.ask_question(q, self.str_to_int,
self.param_checker_nb_to_display)
return {'nb_buy_to_display': nb_buy_to_display,
'nb_sell_to_display': nb_sell_to_display}
def ask_benef_alloc(self):
"""Ask for benefice allocation.
return: int."""
q = (
f'How do you want to allocate your benefice in %. It must '
f'be between 0 and 100, both included:')
benef_alloc = self.ask_question(q, self.str_to_int,
self.param_checker_benef_alloc)
return benef_alloc
def ask_for_params(self):
"""Allow user to use previous parameter if they exist and backup it.
At the end of this section, parameters are set and LW can be initialized.
"""
q = 'Do you want to check if a previous parameter is in params.txt?'
file_path = f'{self.root_path}params.txt'
if self.simple_question(q):
params = self.params_reader(file_path)
if params:
self.applog.info(f'Your previous parameters are: {params}')
q = 'Do you want to display history from logs?'
if self.simple_question(q):
self.log_file_reader()
q = 'Do you want to use those params?'
if self.simple_question(q):
self.params = self.check_for_enough_funds(params)
else:
msg = 'Your parameters are corrupted, please enter new one!'
self.applog.warning(msg)
if not self.params:
self.params = self.enter_params()
self.simple_file_writer(file_path, self.dict_to_str(self.params))
return True
def enter_params(self):
"""Series of questions to setup LW parameters.
return: dict, valid parameters """
params = {'datetime': datetime.now().strftime('%Y-%m-%d %H:%M:%S.%f'),
'market' : self.selected_market}
params.update(self.ask_range_setup())
params.update({'amount': self.ask_param_amount(params['range_bot'])})
params.update(self.ask_params_spread())
params = self.check_for_enough_funds(params)
q = 'Do you want to stop LW if range_bot is reach? (y) or (n) only.'
params.update({'stop_at_bot': self.ask_question(q, self.str_to_bool)})
q = 'Do you want to stop LW if range_top is reach? (y) or (n) only.'
params.update({'stop_at_top': self.ask_question(q, self.str_to_bool)})
params.update(self.ask_nb_to_display())
params.update({'benef_alloc': self.ask_benef_alloc()})
return params
def change_params(self, params):
"""Allow the user to change one LW parameter.
params: dict, all the parameter for LW.
return: dict."""
editable_params = (('range_bot', self.ask_param_range_bot),
('range_top', self.ask_param_range_top),
('increment_coef', self.ask_param_increment),
('amount', self.ask_param_amount))
question = 'What parameter do you want to change?'
question_list = ['The bottom of the range?', 'The top of the range?',
'The value between order?',
'The amount of alt per orders?',
'The value of your initial spread?',
'Add funds to your account']
is_valid = False
while is_valid is False:
try:
choice = self.ask_to_select_in_a_list(question, question_list)
if choice < 3:
params[editable_params[choice][0]] = \
editable_params[choice][1]()
self.intervals = self.interval_generator(
params['range_bot'], params['range_top'],
params['increment_coef'])
params = self.change_spread(params)
elif choice == 3:
params[editable_params[choice][0]] = \
editable_params[choice][1](params['range_bot'])
elif choice == 4:
params = self.change_spread(params)