class TopTrader(QMainWindow, ui): def __init__(self): super().__init__() self.logger = TTlog().logger self.mongo = MongoClient() self.tt_db = self.mongo.TopTrader self.slack = Slack(config_manager.get_slack_token()) self.kw = Kiwoom() self.init_trading() # self.just_sell_all_stocks() self.auto_trading() def init_trading(self): self.login() # self.update_stock_info() self.load_stock_info() self.set_account() self.my_stock_pocket = set() # 보유중인 종목의 code를 저장 t = datetime.today() self.s_time = datetime(t.year, t.month, t.day, 9, 0, 0) # 장 시작시간, 오전9시 def login(self): # Login err_code = self.kw.login() if err_code != 0: self.logger.error("Login Fail") exit(-1) self.logger.info("Login success") def load_stock_info(self): self.stock_dict = {} doc = self.tt_db.stock_information.find({}) for d in doc: code = d["code"] self.stock_dict[code] = d self.logger.info("loading stock_information completed.") def update_stock_info(self): # kospi kospi = [] code_list = self.kw.get_code_list_by_market("0") for code in code_list: stock_name = self.kw.get_master_stock_name(code) kospi.append({ "code": code, "stock_name": stock_name, "market": "kospi" }) self.tt_db.stock_information.insert(kospi) # kosdaq kosdaq = [] code_list = self.kw.get_code_list_by_market("10") for code in code_list: stock_name = self.kw.get_master_stock_name(code) kosdaq.append({ "code": code, "stock_name": stock_name, "market": "kosdaq" }) self.tt_db.stock_information.insert(kosdaq) def set_account(self): self.acc_no = self.kw.get_login_info("ACCNO") self.acc_no = self.acc_no.strip(";") # 계좌 1개를 가정함. self.stock_account = self.get_account_info(self.acc_no) # kiwoom default account setting self.kw.set_account(self.acc_no) def start_timer(self): if self.timer: self.timer.stop() self.timer.deleteLater() self.timer = QTimer() self.timer.timeout.connect(self.check_stocks_to_sell) # self.timer.setSingleShot(True) self.timer.start(30000) # 30 sec interval def just_sell_all_stocks(self): curr_time = datetime.today() self.stock_account = self.get_account_info(self.acc_no) for data in self.stock_account["종목정보"]: self.logger.info("Trading시간 종료되어 보유한 종목 모두 매도처리합니다.") code, stock_name, quantity = data["종목코드"][1:], data["종목명"], int( data["보유수량"]) self.kw.시장가_신규매도(code, quantity) 손익율 = data["손익율"] self.tt_db.trading_history.insert({ 'date': curr_time, 'code': code, 'stock_name': self.stock_dict[code]["stock_name"], 'market': self.stock_dict[code]["market"], 'event': '', 'condi_name': '', 'trade': 'sell', 'profit': 손익율, 'quantity': quantity, 'hoga_gubun': '시장가', 'account_no': self.acc_no }) time.sleep(0.5) def check_stocks_to_sell(self): self.logger.info("[Timer Interrupt] 30 second") self.stock_account = self.get_account_info(self.acc_no) curr_time = datetime.today() self.logger.info("=" * 50) self.logger.info("현재 계좌 현황입니다...") if not bool(self.stock_account): self.logger.error("계좌정보를 제대로 받아오지 못했습니다.") return if datetime.today() < self.s_time: self.logger.info("시작시간이 되지 않아 매도하지 않습니다.") return self.my_stock_pocket = set() for data in self.stock_account["종목정보"]: code, stock_name, quantity = data["종목코드"][1:], data["종목명"], int( data["보유수량"]) self.logger.info("* 종목: {}, 손익율: {}%, 보유수량: {}, 평가금액: {}원".format( data["종목명"], ("%.2f" % data["손익율"]), int(data["보유수량"]), format(int(data["평가금액"]), ','))) self.my_stock_pocket.add(code) 손익율 = data["손익율"] if 손익율 >= 3.0 or 손익율 <= -2.0: if 손익율 > 0: self.logger.info( "시장가로 물량 전부 익절합니다. ^^ [{}:{}, {}주]".format( stock_name, code, quantity)) else: self.logger.info( "시장가로 물량 전부 손절합니다. ㅜㅜ. [{}:{}, {}주]".format( stock_name, code, quantity)) self.my_stock_pocket.remove(code) self.kw.시장가_신규매도(code, quantity) self.tt_db.trading_history.insert({ 'date': curr_time, 'code': code, 'stock_name': self.stock_dict[code]["stock_name"], 'market': self.stock_dict[code]["market"], 'event': '', 'condi_name': '', 'trade': 'sell', 'profit': 손익율, 'quantity': quantity, 'hoga_gubun': '시장가', 'account_no': self.acc_no }) def get_account_info(self, acc_no): self.logger.info("계좌평가현황요청") ret = self.kw.계좌평가현황요청("계좌평가현황요청", acc_no, "", "1", "6001") return ret def search_condi(self, event_data): """키움모듈의 OnReceiveRealCondition 이벤트 수신되면 호출되는 callback함수 이벤트 정보는 event_data 변수로 전달된다. ex) event_data = { "code": code, # "066570" "event_type": event_type, # "I"(종목편입), "D"(종목이탈) "condi_name": condi_name, # "스켈핑" "condi_index": condi_index # "004" } :param dict event_data: :return: """ curr_time = datetime.today() if curr_time < self.s_time: self.logger.info("시작시간이 되지 않아 매수하지 않습니다.") return # 실시간 조건검색 이력정보 self.tt_db.real_condi_search.insert({ 'date': curr_time, 'code': event_data["code"], 'stock_name': self.stock_dict[event_data["code"]]["stock_name"], 'market': self.stock_dict[event_data["code"]]["market"], 'event': event_data["event_type"], 'condi_name': event_data["condi_name"] }) if event_data["event_type"] == "I": 예수금 = self.stock_account["계좌정보"]["예수금"] D2_예수금 = self.stock_account["계좌정보"]["예수금"] 총예수금 = 예수금 + D2_예수금 if 총예수금 < 100000: # 잔고가 10만원 미만이면 매수 안함 self.logger.info("총예수금({}) 부족으로 추가 매수하지 않습니다.".format(총예수금)) return if event_data["code"] in self.my_stock_pocket: self.logger.info("해당 종목({}) 이미 보유중이라 추가매수하지 않습니다.".format( self.stock_dict[event_data["code"]]["stock_name"])) return # curr_price = self.kw.get_curr_price(event_data["code"]) # quantity = int(100000/curr_price) quantity = 20 # self.kw.reg_callback("OnReceiveChejanData", ("조건식매수", "5000"), self.account_stat) stock_name = self.stock_dict[event_data["code"]]["stock_name"] market = self.stock_dict[event_data["code"]]["market"] self.tt_db.trading_history.insert({ 'date': curr_time, 'code': event_data["code"], 'stock_name': stock_name, 'market': market, 'event': event_data["event_type"], 'condi_name': event_data["condi_name"], 'trade': 'buy', 'quantity': quantity, 'hoga_gubun': '시장가', 'account_no': self.acc_no }) self.logger.info("{}:{}를 {}주 시장가_신규매수합니다.".format( stock_name, event_data["code"], quantity)) self.my_stock_pocket.add(event_data["code"]) self.kw.시장가_신규매수(event_data["code"], quantity) # self.kw.send_order("조건식매수", "5000", self.acc_no, 1, event_data["code"], quantity, 0, "03", "") def auto_trading(self): """키움증권 HTS에 등록한 조건검색식에서 검출한 종목을 매수하고 -2%, +3%에 손절/익절 매도하는 기본적인 자동매매 함수 :return: """ self.logger.info("TopTrader 자동매매 시작합니다...") self.timer = None self.start_timer() # callback fn 등록 self.kw.reg_callback("OnReceiveRealCondition", "", self.search_condi) screen_no, cnt = 4000, 0 condi_info = self.kw.get_condition_load() self.logger.info("실시간 조건 검색 시작합니다.") for condi_name, condi_id in condi_info.items(): # 화면번호, 조건식이름, 조건식ID, 실시간조건검색(1) self.logger.info("화면번호: {}, 조건식명: {}, 조건식ID: {}".format( screen_no, condi_name, condi_id)) self.kw.send_condition(str(screen_no), condi_name, int(condi_id), 1) time.sleep(0.5) cnt += 1 if cnt == 10: screen_no += 1 cnt = 0
class TopTrader(QMainWindow, ui): def __init__(self): super().__init__() # self.setupUi(self) # load app screen duration = sys.argv[1] self.logger = TTlog(logger_name="TT"+duration).logger self.mongo = MongoClient() self.tt_db = self.mongo.TopTrader self.slack = Slack(config_manager.get_slack_token()) today = datetime.today() self.end_date = datetime(today.year, today.month, today.day, 16, 0, 0) self.kw = Kiwoom() self.login() self.get_screen_no = { "min1": "3000", "min3": "3001", "min5": "3002", "min10": "3003", "min60": "3004", "day": "3005", "week": "3006", "month": "3007", "year": "3008" } if duration.startswith("min"): self.collect_n_save_data_min(duration) else: self.collect_n_save_data(duration) def login(self): err_code = self.kw.login() if err_code != 0: self.logger.error("Login Fail") exit(-1) self.logger.info("Login success") def upsert_db(self, col, datas): self.logger.info("Upsert Data to DB") s_time = time.time() for doc in datas: # col.update(condition, new_data, upsert=True) col.update({'date': doc['date'], 'code': doc['code']}, doc, upsert=True) e_time = time.time() print("Time: ", int(e_time-s_time)) def get_stock_list(self, market): kospi_code_list = self.kw.get_code_list_by_market(market) stock_list = [[c, self.kw.get_master_stock_name(c)] for c in kospi_code_list] stock_list = [(c, name) for c, name in stock_list if not any(map(lambda x: x in name, constant.FILTER_KEYWORD))] stock_list.sort() return stock_list def get_last_data(self, cur, duration): current_flag = True if duration.startswith("min"): first_date = datetime(2018, 7, 23, 0, 0, 0) else: first_date = datetime(2018, 6, 1, 0, 0, 0) # 최초 저장하는 경우 if cur.count() == 0: s_date, e_date, s_index = first_date, self.end_date, 0 else: data = cur.next() if (data['last'] + 1) != data['total']: # 지난번에 저장하다가 중간에 멈춘 경우 if data['end_date'] != self.end_date: s_date, e_date, s_index = data['start_date'], data['end_date'], data['last'] + 1 current_flag = False # 이번에 저장하다가 중간에 멈춘 경우 else: s_date, e_date, s_index = data['start_date'], self.end_date, data['last'] + 1 else: # 지난번에 저장을 완료, 이번에 저장해야 하는 경우 if data['end_date'] != self.end_date: s_date, e_date, s_index = data['end_date'], self.end_date, 0 # 이번에 저장을 완료 else: s_date, e_date, s_index = data['end_date'], self.end_date, data['last'] + 1 return s_date, e_date, s_index, current_flag def collect_n_save_data_min(self, duration): """ 코스피 종목의 분단위 데이터를 수집한다. 1. 한번도 db에 저장한적이 없다면 6/1일부터 오늘까지의 데이터를 저장한다. 2. 지난번에 저장하다가 중간에 멈췄다면, 나머지 작업을 모두 완료 후, 해당시점의 e_date 부터 오늘까지의 데이터를 전 종목에 대해 저장한다. 3. 이번에 저장하다가 중간에 멈췄다면, 이전 e_date 부터 오늘까지의 데이터를 전 종목에 대해 저장한다. 4. 지난번에 저장을 완료했다면, 해당시점의 e_date 부터 오늘까지의 데이터를 전 종목에 대해 저장한다. :param duration: min1, min3, min5, min10, min60 중 하나의 값 :return: """ # stock_list = self.get_stock_list(constant.KOSPI) # stock_list += self.get_stock_list(constant.KOSDAQ) stock_list = self.get_stock_list(constant.KOSDAQ) cur = self.tt_db.time_series_temp2.find({'type': duration}) s_date, e_date, s_index, current_flag = self.get_last_data(cur, duration) if not current_flag: msg = "[{}] {} ~ {}. start to collect stock data. this is previous process.".format( duration, s_date, e_date, e_date, self.end_date ) else: msg = "[{}] {} ~ {}. start to collect stock data. this is current process.".format( duration, s_date, e_date ) self.slack.log(msg) col = { "min1": self.tt_db.time_series_min1, "min3": self.tt_db.time_series_min3, "min5": self.tt_db.time_series_min5, "min10": self.tt_db.time_series_min10, "min60": self.tt_db.time_series_min60 }[duration] fn = self.kw.stock_price_by_min total = len(stock_list) msg = "[{}] {} / {} -> start to collect stock data".format(duration, s_index, total) self.slack.log(msg) for i, stock in enumerate(stock_list[s_index:], s_index): code, stock_name = stock self.logger.info("%s/%s - %s/%s" % (i, total, code, stock_name)) self.logger.info("period : {} ~ {}".format(s_date, e_date)) self.logger.info("time_series_{}".format(duration)) try: doc = fn(code, tick=duration.strip("min"), screen_no=self.get_screen_no[duration], start_date=s_date, end_date=e_date) except KiwoomServerCheckTimeError as e: self.logger.error("[KiwoomServerCheckTimeError] {}".format(duration)) self.tt_db.urgent2.update({'type': 'error'}, {'type': 'error', 'error_code': e.error_code}, upsert=True) exit(0) try: self.upsert_db(col, doc) except pymongo.errors.InvalidOperation as e: # cannot do an empty bulk write ? self.logger.error(e) self.tt_db.time_series_temp2.update({'type': duration}, {'type': duration, 'code': code, 'stock_name': stock_name, 'last': i, 'start_date': s_date, 'end_date': e_date, 'total': total}, upsert=True) self.tt_db.urgent2.update({'type': 'error'}, {'type': 'error', 'error_code': 0}, upsert=True) exit(0) # Program exit def collect_n_save_data(self, duration): """ 코스피 종목의 분단위 데이터를 제외한 나머지 데이터를 수집한다. 1. 한번도 db에 저장한적이 없다면 6/1일부터 오늘까지의 데이터를 저장한다. 2. 지난번에 저장하다가 중간에 멈췄다면, 나머지 작업을 모두 완료 후, 해당시점의 e_date 부터 오늘까지의 데이터를 전 종목에 대해 저장한다. 3. 이번에 저장하다가 중간에 멈췄다면, 이전 e_date 부터 오늘까지의 데이터를 전 종목에 대해 저장한다. 4. 지난번에 저장을 완료했다면, 해당시점의 e_date 부터 오늘까지의 데이터를 전 종목에 대해 저장한다. :param duration: str - day, week, month, year 중 하나의 값 :return: """ stock_list = self.get_stock_list(constant.KOSPI) stock_list += self.get_stock_list(constant.KOSDAQ) cur = self.tt_db.time_series_temp2.find({'type': duration}) s_date, e_date, s_index, current_flag = self.get_last_data(cur, duration) if not current_flag: msg = "[{}] {} ~ {}. start to collect stock data. this is previous process.".format( duration, s_date, e_date, e_date, self.end_date ) else: msg = "[{}] {} ~ {}. start to collect stock data. this is current process.".format( duration, s_date, e_date ) self.slack.log(msg) col = { "day": self.tt_db.time_series_day, "week": self.tt_db.time_series_week, "month": self.tt_db.time_series_month, "year": self.tt_db.time_series_year }[duration] fn = { "day": self.kw.stock_price_by_day, "week": self.kw.stock_price_by_week, "month": self.kw.stock_price_by_month # "year": self.kw.stock_price_by_year }[duration] total = len(stock_list) msg = "[{}] {} / {} -> start to collect stock data".format(duration, s_index, total) self.slack.log(msg) for i, stock in enumerate(stock_list[s_index:], s_index): code, stock_name = stock self.logger.info("%s/%s - %s/%s" % (i, total, code, stock_name)) self.logger.info("time_series_{}".format(duration)) try: doc = fn(code, screen_no=self.get_screen_no[duration], start_date=s_date, end_date=e_date) except KiwoomServerCheckTimeError as e: self.logger.error("[KiwoomServerCheckTimeError] {}".format(duration)) self.tt_db.urgent2.update({'type': 'error'}, {'type': 'error', 'error_code': e.error_code}, upsert=True) exit(0) self.upsert_db(col, doc) self.tt_db.time_series_temp2.update({'type': duration}, {'type': duration, 'code': code, 'stock_name': stock_name, 'last': i, 'start_date': s_date, 'end_date': e_date, 'total': total}, upsert=True) self.tt_db.urgent2.update({'type': 'error'}, {'type': 'error', 'error_code': 0}, upsert=True) exit(0) # Program exit