-
Notifications
You must be signed in to change notification settings - Fork 10
/
Copy pathapplication.py
653 lines (576 loc) · 23.6 KB
/
application.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
# import necessary libraries
from OpenSSL.SSL import SysCallError
from bs4 import BeautifulSoup
from collections import deque
from library import get_live_price
from library import Notify
from library import master_logger, trader_logger
from colorama import init
from time import sleep
import requests
import argparse
import holidays
import datetime
import pytz
import json
import os
import sys
# setup for coloured output
init()
##############################################################
HEADING = '''
__ __ ____
___ ____/ /___ ______/ /_/ __ \\__ __
/ _ \\/ __ / __ `/ ___/ __/ /_/ / / / /
/ __/ /_/ / /_/ / / / /_/ ____/ /_/ /
\\___/\\__,_/\\__,_/_/ \\__/_/ \\__, /
/____/
'''
Notify.heading(HEADING)
##############################################################
# set time zone
TZ = pytz.timezone('Asia/Calcutta')
# set holidays
INDIA_HOLIDAYS = holidays.India()
# set market open time
OPEN_TIME = datetime.time(hour=9, minute=15, second=0)
# set market close time
CLOSE_TIME = datetime.time(hour=15, minute=30, second=0)
##############################################################
# only those stocks will be considered whose price is above threshold
PENNY_STOCK_THRESHOLD = 50
# number of stocks to select relevant ones from
NUM_OF_STOCKS_TO_SEARCH = 100
# number of stocks to focus trading on
NUM_OF_STOCKS_TO_FOCUS = 5
# percentage buffer to be set for stop loss/trade exit
BUFFER_PERCENT = 0.06
# number of observations of prices during initialisation phase, minimum value of 80
DATA_LIMIT = 80
# interval of each period, in seconds
PERIOD_INTERVAL = 60
# percentage of account_balance to be considered for trading
FEASIBLE_PERCENT = 0.2 # 20%
##############################################################
# time delay to check if market is open, in seconds
DELAY = 300
# delay in idle phase, in seconds
IDLE_DELAY = 1800
# time to stop trading
PACK_UP = datetime.time(hour=15, minute=15, second=0)
##############################################################
TODAY = datetime.date.today().strftime("%d-%m-%Y")
if not os.path.exists(f"database/{TODAY}"):
os.mkdir(f"database/{TODAY}")
ml = master_logger(f'database/{TODAY}/master.log')
ml.info("-"*76)
ml.info("-"*27 + " NEW SESSION DETECTED " + "-"*27)
sys.stderr = open(f"database/{TODAY}/errorStream.txt", "a")
##############################################################
try:
ACCOUNT = json.loads(open("database/user_info.json").read())["account_balance"] * FEASIBLE_PERCENT
except FileNotFoundError:
Notify.fatal('User info not found, Aborting.')
ml.critical("User info not found")
quit(0)
ml.info("Successfully loaded user_info.json")
ml.info("-"*76)
##############################################################
HEADERS = {
"user-agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/80.0.3987.163 Safari/537.36"
}
##############################################################
parser = argparse.ArgumentParser(prog="application.py",
description="A fully automated Pythonic trading bot\n\nAuthor : Ashwin A Nayar",
epilog="Time for some real money !",
formatter_class=argparse.RawTextHelpFormatter
)
parser.add_argument("--delay", type=int, default=IDLE_DELAY,
help="Duration of Idle Phase, in seconds")
parser.add_argument("-nd", action="store_true",
help="Skip Idle Phase, not recommended")
parser.add_argument("-np", action="store_true",
help="Set period interval to zero, not recommended")
parser.add_argument("-t", action="store_true",
help='Run script in trial mode, for debugging purposes')
args = parser.parse_args()
if args.nd:
if args.delay != IDLE_DELAY:
Notify.fatal("Invalid set of arguments given. Aborting")
ml.critical("Received no delay and custom delay")
quit(0)
else:
IDLE_DELAY = 0
ml.warning("[ MODE ] Zero delay")
else:
IDLE_DELAY = args.delay
ml.info(f"Idle delay set to {IDLE_DELAY}")
if args.np:
PERIOD_INTERVAL = 0
ml.warning("[ MODE ] Zero period interval")
if args.t:
IDLE_DELAY = 1
PERIOD_INTERVAL = 0
Notify.warn("Running in Test Mode, meant for debugging and demonstration purposes only.")
ml.warning("[ MODE ] TEST")
print("")
# developer mode
DEV_MODE = args.nd and args.np
if DEV_MODE:
PENNY_STOCK_THRESHOLD = 0
ml.warning("[ MODE ] DEVELOPER")
##############################################################
def print_progress_bar(iteration, total, prefix='', suffix='', decimals=1, length=100, fill='█', print_end="\r"):
"""
Call in a loop to create terminal progress bar
@params:
iteration - Required : current iteration (Int)
total - Required : total iterations (Int)
prefix - Optional : prefix string (Str)
suffix - Optional : suffix string (Str)
decimals - Optional : positive number of decimals in percent complete (Int)
length - Optional : character length of bar (Int)
fill - Optional : bar fill character (Str)
printEnd - Optional : end character (e.g. "\r", "\r\n") (Str)
"""
percent = ("{0:." + str(decimals) + "f}").format(100 * (iteration / float(total)))
filled_length = int(length * iteration // total)
bar = fill * filled_length + ' ' * (length - filled_length)
print('\r%s |%s| %s%% %s' % (prefix, bar, percent, suffix), end=print_end)
# print new line on complete
if iteration == total:
print()
def is_open():
"""
Function to check if market is open at the moment
Returns:
True if market is open, False otherwise
"""
global ml
now = datetime.datetime.now(TZ)
# if a holiday
if now.strftime('%Y-%m-%d') in INDIA_HOLIDAYS:
ml.error("Holiday ! ")
return False
# if before opening or after closing
if (now.time() < OPEN_TIME) or (now.time() > CLOSE_TIME):
ml.error("Market closed.")
return False
# if it is a weekend
if now.date().weekday() > 4:
ml.error("Weekday !")
return False
return True
def fetch_stocks():
"""
Find relevant stocks to focus on for trading
Returns:
Deque of tickers of relevant stocks
"""
global ml
# url to grab data from
url = f'https://in.finance.yahoo.com/gainers?count={NUM_OF_STOCKS_TO_SEARCH}'
# request header
headers = {
'user-agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/79.0.3945.130 Safari/537.36'}
try:
src = requests.get(url=url, headers=headers).content
except Exception as e:
src = None
Notify.fatal("Trade abort due to unexpected error. Check activity log for details")
ml.critical("Encountered error : ", e)
quit(0)
# soup object of source code
soup = BeautifulSoup(src, "html.parser")
rows = soup.find('table').tbody.find_all('tr')
# initialisations
stocks_temp = dict()
# check previous day's closing status
prev_data = json.loads(open("database/user_info.json").read())
for ticker in prev_data["stocks_to_sell"]:
stock_name, stock_ex = ticker.split(".")
stocks_temp[stock_name] = stock_ex
for ticker in prev_data["stocks_to_buy_back"]:
stock_name, stock_ex = ticker.split(".")
stocks_temp[stock_name] = stock_ex
# set counter
count = len(stocks_temp)
stocks = deque()
# iterate over rows in web page
for tr in rows:
# exit if
if count == NUM_OF_STOCKS_TO_FOCUS:
break
else:
row_data = tr.find_all('td')
ticker = row_data[0].text.strip()
price = get_live_price(ticker)
# split ticker for checking if same stock of different stock exchange is selected or not
stock_name, stock_ex = ticker.split(".")
if price >= PENNY_STOCK_THRESHOLD and stock_name not in stocks_temp:
stocks_temp[stock_name] = stock_ex
count += 1
# get back ticker
for stock in stocks_temp:
stocks.append(f"{stock}.{stocks_temp[stock]}")
# return deque of stocks to focus on
return stocks
def get_value(ref: list, x_src: list, x: float) -> float:
"""
Helper function for traders, used to find Ichimoku components corresponding to entry from other components or price
Args:
ref: iterable from which corresponding entry should be found
x_src: iterable containing param x
x: an item, maybe a component value, maybe price
Returns:
"""
return ref[x_src.index(x)]
class Trader:
def __init__(self, number, ticker):
self.number = number
self.ticker = ticker
# store x values, equivalent to time
self.time = [i for i in range(-25, DATA_LIMIT + 27)]
# list for storing live price
self.price = []
# lists for storing Ichimoku params
self.tenkan_data = []
self.kijun_data = []
self.chikou_data = []
self.senkou_A_data = []
self.senkou_B_data = []
# x values for senkou A and senkou B
self.x5 = []
self.x6 = []
# database to save activity of trader
self.database = dict()
self.database["Ticker"] = self.ticker
self.database["Activity"] = dict()
# other params used within trader class
self.IN_SHORT_TRADE = False
self.IN_LONG_TRADE = False
self.STOCKS_TO_SELL = 0
self.STOCKS_TO_BUY_BACK = 0
self.price_for_buffer = 0
self.sold_price = 0
self.bought_price = 0
# set params in accordance with previous day's data
prev_data = json.loads(open("../user_info.json").read())
# check if allotted stock has been bought the previous day or not, long trade
if self.ticker in prev_data["stocks_to_sell"]:
price = prev_data["stocks_to_sell"][self.ticker]["buffer_price"]
self.IN_LONG_TRADE = True
self.price_for_buffer = price
# check if allotted stock has been sold the previous day or not, short trade
if self.ticker in prev_data["stocks_to_buy_back"]:
price = prev_data["stocks_to_buy_back"][self.ticker]["buffer_price"]
self.IN_SHORT_TRADE = True
self.price_for_buffer = price
self.logger = trader_logger(self.ticker)
self.logger.info("-" * 76)
self.logger.info("-" * 27 + " NEW SESSION DETECTED " + "-" * 27)
self.logger.info("-" * 76)
def get_initial_data(self):
try:
self.price.append(get_live_price(self.ticker))
self.logger.debug("Successfully fetched live price")
except SysCallError:
Notify.warn(f"[Trader #{self.number} {self.ticker}]: Encountered SysCallError while initialising parameters, trying recursion")
self.logger.warning("Encountered SysCallError, trying recursion")
self.get_initial_data()
except Exception as e:
Notify.warn(f"[Trader #{self.number} {self.ticker}]: Exception in getting initial data, trying recursion")
self.logger.error("Trying recursion due to uncommon Exception : ", e)
self.get_initial_data()
def buy(self, price, trade):
global ACCOUNT
now = datetime.datetime.now(TZ).strftime('%H:%M:%S')
self.bought_price = price
self.logger.info("Bought stock, in ", trade, " trade, for ", price, " INR")
ACCOUNT -= price
self.database['Activity'][now] = {
"trade": trade,
"bought at": price
}
def sell(self, price, trade):
global ACCOUNT
now = datetime.datetime.now(TZ).strftime('%H:%M:%S')
self.sold_price = price
self.logger.info("Sold stock, in ", trade, " trade, for ", price, " INR")
ACCOUNT += price
self.database['Activity'][now] = {
"trade": trade,
"sold at": price
}
def update_price(self):
try:
new_price = get_live_price(self.ticker)
self.price.append(new_price)
self.logger.info("Successfully fetched price, local database updated")
except SysCallError:
Notify.warn(f"[Trader #{self.number} {self.ticker}] : Encountered SysCallError in updating price, trying recursion")
self.logger.warning("Encountered SysCallError while fetching live price, trying recursion")
self.update_price()
except Exception as e:
Notify.warn(f"[Trader #{self.number} {self.ticker}] : Exception in updating price, trying recursion")
self.logger.error("Trying recursion, encountered uncommon exception : ", e)
self.update_price()
def update_data(self):
self.update_price()
self.time.append(self.time[-1] + 1)
del self.time[0], self.price[0]
# observe indicator and decide buy and sell
def make_decision(self):
global ACCOUNT
# update tenkan data
self.tenkan_data = []
for i in range(DATA_LIMIT - 9):
tenkan_src = self.price[i:i + 9]
self.tenkan_data.append((max(tenkan_src) + min(tenkan_src)) / 2)
# update kijun data
self.kijun_data = []
for i in range(DATA_LIMIT - 26):
kijun_src = self.price[i:i + 26]
self.kijun_data.append((max(kijun_src) + min(kijun_src)) / 2)
# update x values for senkou A and senkou B
self.x5 = self.time[78:78 + DATA_LIMIT - 26]
self.x6 = self.time[104:104 + DATA_LIMIT - 52]
# update senkou A data
self.senkou_A_data = [(self.tenkan_data[i + 17] + self.kijun_data[i]) / 2 for i in range(DATA_LIMIT - 26)]
# update senkou B data
self.senkou_B_data = []
for i in range(DATA_LIMIT - 52):
senkou_B_src = self.price[i:i + 52]
self.senkou_B_data.append((max(senkou_B_src) + min(senkou_B_src)) / 2)
# get Ichimoku params for comparison
x = self.time[26:26 + DATA_LIMIT][-1]
curr_price = self.price[-1]
tenkan = self.tenkan_data[-1]
kijun = self.kijun_data[-1]
sen_A = get_value(self.senkou_A_data, self.x5, x)
sen_B = get_value(self.senkou_B_data, self.x6, x)
self.logger.info(f"Current status - Price : {curr_price}, Tenkan : {tenkan}, Kijun : {kijun}, Senkou A : {sen_A}, Senkou B : {sen_B}")
# conditions for long trade entry
# If Kumo cloud is green and current price is above kumo, strong bullish signal
cond1 = (sen_A > sen_B) and (curr_price >= sen_A)
if cond1:
self.logger.debug("Sensing strong bullish signal")
# conditions for short trade entry
# If Kumo cloud is red and current price is below kumo, strong bearish signal
cond2 = (sen_A < sen_B) and (curr_price <= sen_A)
if cond2:
self.logger.debug("Sensing strong bearish signal")
# check allocated money
cond3 = curr_price < ACCOUNT
# IF all conditions are right, long trade entry
if cond1 and not self.IN_LONG_TRADE and cond3:
self.buy(curr_price, "LONG")
self.price_for_buffer = curr_price
self.IN_LONG_TRADE = True
self.STOCKS_TO_SELL += 1
if not cond3:
Notify.fatal(f"[Trader #{self.number} {self.ticker}] : Oops! Out of cash!")
self.logger.critical("Trader out of cash to buy stocks!")
# If all conditions are right, short trade entry
if cond2 and not self.IN_SHORT_TRADE:
self.sell(curr_price, "SHORT")
self.price_for_buffer = curr_price
self.IN_SHORT_TRADE = True
self.STOCKS_TO_BUY_BACK += 1
# setup buffer for stop loss and trade exit
buffer = self.price_for_buffer * BUFFER_PERCENT
cond4 = abs(curr_price - kijun) >= buffer
# Get stopped out as the price moves through the buffer area beyond the Kijun
if self.IN_LONG_TRADE:
if cond4:
self.sell(curr_price, "LONG")
self.IN_LONG_TRADE = False
self.STOCKS_TO_SELL -= 1
if self.IN_SHORT_TRADE:
if cond4 and cond3:
self.buy(curr_price, "SHORT")
self.IN_SHORT_TRADE = False
self.STOCKS_TO_BUY_BACK -= 1
if not cond3:
Notify.fatal(f"[Trader #{self.number} {self.ticker}] : Oops! Out of cash!")
self.logger.critical("Trader out of cash to buy back stock !")
# group update and decision call for convenience
def run(self):
self.update_data()
self.make_decision()
def __del__(self):
with open(self.ticker + ".json", "w") as fp:
fp.write(json.dumps(self.database, indent=4))
self.logger.critical("Trader killed")
# Manages all the traders
class Master:
def __init__(self):
self.traders = deque()
# check if required directories exist, if not, make them
@staticmethod
def validate_repo():
today = datetime.date.today().strftime("%d-%m-%Y")
if not os.path.exists(".\\database"):
os.mkdir("database")
os.chdir("database")
if not os.path.exists(today):
os.mkdir(today)
os.chdir(today)
# allocate tickers to traders
def lineup_traders(self, tickers):
global ml
count = 1
for ticker in tickers:
self.traders.append(Trader(count, ticker))
Notify.info(f"Successfully connected Trader #{count} to {ticker}", delay=0.01)
count += 1
ml.info("Trader lineup complete")
print("")
# initialise traders
def init_traders(self, Tmode=False):
global ml
Notify.info("Traders are in Observation phase")
ml.info("Traders entered Observation Phase")
if not Tmode:
print_progress_bar(0, 80, prefix='\tProgress:', suffix='Complete', length=40)
for i in range(DATA_LIMIT):
for trader in self.traders:
trader.get_initial_data()
print_progress_bar(i + 1, 80, prefix='\tProgress:', suffix='Complete', length=40)
sleep(PERIOD_INTERVAL)
Notify.info("\tStatus : Complete")
ml.info("Observation Phase complete")
print("")
# trading begins
def start_trading(self, Tmode=False):
global ml
now = datetime.datetime.now(TZ)
Notify.info("Trading has begun")
ml.info("Trading has begun")
count = 1
if not Tmode:
while now.time() < PACK_UP or DEV_MODE:
try:
for trader in self.traders:
trader.run()
ml.info("Completed round #", count)
sleep(PERIOD_INTERVAL)
except Exception as e:
Notify.fatal("Trading has been aborted")
ml.critical("Trade abort due to unexpected error : ", e)
quit(0)
finally:
now = datetime.datetime.now(TZ)
count += 1
else:
Notify.info("Confirming access to live stock price...")
ml.info("Confirming access to live stock price...")
for trader in self.traders:
try:
get_live_price(trader.ticker)
except Exception as e:
Notify.fatal("Error in fetching live stock price. Aborting")
ml.critical("Error in fetching live stock price : ", e)
# save master data
def __del__(self):
global ACCOUNT, ml
# load previous day's data
prev_data = json.loads(open("..\\user_info.json").read())
username = prev_data['username']
# debug
account_balance_prev = prev_data["account_balance"]
# get new data from trader's database
account_balance_new = account_balance_prev * (1 - FEASIBLE_PERCENT) + ACCOUNT
profit = account_balance_new - account_balance_prev
# set up new data
new_data = dict()
new_data['username'] = username
new_data["account_balance"] = account_balance_new
new_data["stocks_to_sell"] = dict()
new_data["stocks_to_buy_back"] = dict()
# grab data from trader database
for trader in self.traders:
# check owned stocks
if trader.IN_LONG_TRADE:
new_data["stocks_to_sell"][trader.ticker] = {"buffer_price": trader.price_for_buffer}
# check owed stocks
if trader.IN_SHORT_TRADE:
new_data["stocks_to_buy_back"][trader.ticker] = {"buffer_price": trader.price_for_buffer}
# save trader database in respective files
del trader
# save master database
with open("..\\user_info.json", "w") as fp:
fp.write(json.dumps(new_data, indent=4))
# output profit
Notify.info(f"\n\nNet Profit : {profit} INR\n")
ml.info(f"\n\nNet Profit : {profit} INR\n")
Notify.info(f'Stocks owned : {len(new_data["stocks_to_sell"])}')
ml.info(f'Stocks owned : {len(new_data["stocks_to_sell"])}')
Notify.info(f'Stocks owed : {len(new_data["stocks_to_buy_back"])}')
ml.info(f'Stocks owed : {len(new_data["stocks_to_buy_back"])}')
def main():
"""
Main Function
"""
# make sure that market is open
if not DEV_MODE:
if args.t:
Notify.for_input("Check Market? (y/n) : ")
confirm = input().strip().lower()
print("")
else:
confirm = "y"
if is_open() or confirm == "n":
pass
else:
Notify.fatal("Market is closed at the moment, aborting.")
print("")
quit(0)
else:
Notify.warn("You are in developer mode, if not intended, please quit.")
Notify.info("Press ENTER to continue, Ctrl+C to quit")
input()
# allow market to settle to launch Ichimoku strategy
if IDLE_DELAY == 0:
Notify.info("Skipped Idle phase")
else:
Notify.info(f"Entered Idle phase at {datetime.datetime.now(TZ).strftime('%H:%M:%S')}")
ml.info(f"Entered Idle phase")
Notify.info(f"\tExpected release : after {IDLE_DELAY // 60} minutes")
print("")
sleep(IDLE_DELAY)
ml.info("Idle phase complete")
# find relevant stocks to focus on
Notify.info("Finding stocks to focus on .....")
try:
stocks_to_focus = fetch_stocks()
except:
stocks_to_focus = []
Notify.fatal("Could not fetch relevant stocks. Verify Network connection and check logs for details.")
ml.critical("Could not fetch relevant stocks, Most possibly due to network error")
quit(0)
Notify.info("\tStatus : Complete")
ml.info("Successfully found relevant stocks")
print("")
# setup traders and begin trade
master = Master()
master.validate_repo()
master.lineup_traders(stocks_to_focus)
master.init_traders(args.t)
master.start_trading(args.t)
# trading in over by this point
Notify.info("Trading complete")
ml.info("Trading complete")
# initiate packup
del master
quit(0)
if __name__ == "__main__":
try:
main()
except KeyboardInterrupt:
Notify.fatal("Operation cancelled by user.")
ml.critical("Operation cancelled by user")
quit(0)
except Exception as err:
Notify.fatal("Encountered fatal error. Check log for details. Aborting")
ml.critical("Trade abort due to unexpected error : ", err)