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
Exemple #2
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