class Trading(object): """Trading 된 시점의 모든 데이터를 snapshot 으로 보관하는 객체 특정시점의 모든 data를 snapshot뜨는 기능에 집중 """ def __init__(self, trading_type): self.trading_type = trading_type # SELL, BUY (defined at constant) self.logger = TTlog().logger self.trading_core_index = [ 'timestamp', 'code', 'stock_name', 'trading_type', '현재가', '매매수량', '보유수량', '매매금액', '매입금액', '최대매입금액', '매입평균가', '평가손익', '실현손익', '누적손익', '평가금액', '수익률', '누적수익률', '평가금액변동', '시작잔고', '예수금', '총평가금액', '총매입금액', '총평가손익', '총손익', '추정자산', '총수익률', '총누적손익', '총누적수익률' ] def print_attr(self, attr_list=None): self.logger.debug("\n" + ("-" * 100)) self.logger.debug("[Stock] Information : {}/{}".format(self.stock_name, self.code)) if bool(attr_list): attr_val = [] for attr in attr_list: val = self.__getattribute__(attr) log_str = "{}: {}".format(attr, val) if isinstance(val, float): log_str = "{}: {:.2f}".format(attr, val) attr_val.append(log_str) self.logger.debug("\t".join(attr_val)) else: self.logger.debug(self.__repr__()) # pass def __repr__(self): core_index = self.trading_core_index log = [] for attr in core_index: val = self.__getattribute__(attr) log_str = "{}: {}".format(attr, val) if isinstance(val, float): log_str = "{}: {:.2f}".format(attr, val) log.append(log_str) return "\t".join(log) def check_core_index(self): """Trading의 핵심 지표들이 모두 설정되었는지 check. 누락된 지표가 있다면 Exception 발생하고 프로그램 종료 :return: """ for index in self.trading_core_index: self.__getattribute__(index)
class DBM(): def __init__(self, dbname, host=None, port=None): """DBM 생성자, dbname을 인자로 받는다. :param str db: database name """ if bool(host) and bool(port): self.mongo = MongoClient(host=host, port=port) else: self.mongo = MongoClient() self.db = self.mongo.get_database(dbname) self.col_table = { "tick1": self.db.time_series_tick1, "tick3": self.db.time_series_tick3, "tick5": self.db.time_series_tick5, "tick10": self.db.time_series_tick10, "tick30": self.db.time_series_tick30, "min1": self.db.time_series_min1, "min3": self.db.time_series_min3, "min5": self.db.time_series_min5, "min10": self.db.time_series_min10, "min30": self.db.time_series_min30, "real_condi_search": self.db.real_condi_search } self.logger = TTlog(logger_name="DB").logger def get_unique_data(self, col, query=None): """collection의 특정 필드값을 unique 하게 얻고 싶을때 사용 ex) >>> cur = self.db.trading_history.distinct("stock_name", {'date': {'$gt': datetime(2018, 7, 20, 13, 0, 0)}}) :param col: 필드명 :param query: 특정 조건을 만족하는 doc 를 명시하고 싶은경우 query 를 사용 :return: """ # if bool(query): cur = self.db.trading_history.distinct(col, query) else: cur = self.db.trading_history.distinct(col) return cur def get_time_series_collection(self, time_unit): return self.col_table[time_unit] def check_tick_cache(self, code, date, tick="1"): """특정 종목의 하루단위 tick data가 DB에 있는지 확인하고, DB에 있으면 cur를 반환한다. :param code: :param date: :param tick: :return: """ col = self.get_time_series_collection("tick" + tick) date = datetime(date.year, date.month, date.day, 0, 0, 0) cur = col.find({'code': code, 'date': date}) if cur.count() != 0: return True, cur return False, None def save_force(self, col, data, search_condition): try: col.insert(data) except Exception as e: col.update(search_condition, data, upsert=True) def get_tick_data(self, code, date, tick="1"): col = self.get_time_series_collection("tick" + tick) base_date = datetime(date.year, date.month, date.day) query = {'code': code, 'date': base_date} cur = col.find(query) if cur.count() == 0: return [] return cur.next()["time_series_1tick"] def save_tick_data(self, data, tick="1"): col = self.get_time_series_collection("tick" + tick) try: col.insert(data) except Exception as e: col.update({ 'date': data['date'], 'code': data['code'] }, data, upsert=True) def get_code_list_of_rcs(self, s_date, e_date): cur = self.db.real_condi_search.find( {'date': { '$gte': s_date, '$lte': e_date }}, { 'code': 1, '_id': 0 }) code_list = list({data['code'] for data in cur}) code_list.sort() return code_list def get_code_list_condi_search_result(self, date): # cache cur = self.db.real_condi_search_cache.find({'date': date}) if cur.count() != 0: self.logger.debug("hit cache (real_condi_search_cache)") code_list = cur.next()['code_list'] else: s_date = datetime(date.year, date.month, date.day, 9, 0, 0) e_date = datetime(date.year, date.month, date.day, 16, 0, 0) code_list = self.get_code_list_of_rcs(s_date, e_date) self.db.real_condi_search_cache.insert({ 'date': date, 'code_list': code_list }) return code_list def get_condi_result(self, s_date, e_date): query = {"date": {"$gte": s_date, "$lte": e_date}, "event": "I"} cur = self.db.real_condi_search.find(query) # date, code, stock_name, condi_name condi_result = [data for data in cur] condi_result.sort(key=lambda x: x['date']) return condi_result def code_list_by_condi_id(self, condi_index, date): query = {'condi_index': condi_index, 'date': date} cur = self.db.real_condi_search.find(query) code_list = [doc['code'] for doc in cur] return code_list def already_collect_tick_data(self, code, date, tick="1"): cur = self.db.collect_tick_data_history.find({ 'code': code, 'date': date, 'tick': tick }) return cur.count() != 0 def save_collect_tick_data_history(self, code, date, tick="1"): self.db.collect_tick_data_history.update( { 'code': code, 'date': date, 'tick': tick }, { 'code': code, 'date': date, 'tick': tick }, upsert=True) def record_collect_tick_data_status(self, status, date, tick="1"): self.db.collect_tick_data_status.update({ "date": date, "tick": tick }, { "status": status, "date": date, "tick": tick }, upsert=True) def get_collect_tick_data_status(self, date, tick="1"): cur = self.db.collect_tick_data_status.find({ 'date': date, 'tick': tick }) if cur.count() == 0: return "NEVER_STARTED" data = cur.next() return data['status'] def get_real_condi_search_data(self, date, condi_name): y, m, d = date.year, date.month, date.day s_date = datetime(y, m, d, 9, 0, 0) e_date = datetime(y, m, d, 15, 30, 0) # need to fix (condi_name -> condi_index) # query = {'date': {'$gte': s_date, '$lte': e_date}, 'event': 'I', 'condi_index': condi_index} query = { 'date': { '$gte': s_date, '$lte': e_date }, 'event': 'I', 'condi_name': condi_name } cur = self.db.real_condi_search.find(query)\ .sort('date', pymongo.ASCENDING) if cur.count() == 0: return [] return list(cur)
class TopTrader(QMainWindow, ui): def __init__(self): super().__init__() # self.setupUi(self) # load app screen self.logger = TTlog().logger self.dbm = DBM('TopTrader') self.mongo = MongoClient() self.db = self.mongo.TopTrader self.slack = Slack(config_manager.get_slack_token()) self.kw = Kiwoom() self.login() self.main() def load_tick_data(self, code, stock_name, date, tick="1", screen_no="1234"): """특정 종목의 하루단위 tick data를 가져오는 함수 tick_data가 DB에 있으면 DB에서 가져오고, 없으면 kw module로부터 가져온다. :param code: :param stock_name: :param date: :param tick: :param screen_no: :return: """ ret, cur = self.dbm.check_tick_cache(code, date, tick="1") if ret: tick_data = cur.next() else: base_date = datetime(date.year, date.month, date.day, 0, 0, 0) raw_data = self.kw.stock_price_by_tick(code, tick=tick, screen_no=screen_no, date=base_date) tick_data = { 'code': code, 'stock_name': stock_name, 'date': base_date, 'time_series_1tick': [{ 'timestamp': _d['date'], '현재가': _d['현재가'], '거래량': _d['거래량'] } for _d in raw_data] } return tick_data def collect_tick_data(self, code, stock_name, s_date, e_date, tick="1", screen_no="1234"): """특정 종목의 일정 기간의 tick data를 DB에 저장하는 함수 DB에 이미 값이 저장되어 있으면 저장하지 않고 skip 한다. s_date, e_date는 초단위까지 지정이 가능하지만, 저장은 일단위로 저장한다. tick은 1, 3, 5, 10, 30 선택 가능하다. :param code: :param stock_name: :param tick: :param s_date: :param e_date: :return: """ s_base_date = datetime(s_date.year, s_date.month, s_date.day, 0, 0, 0) e_base_date = datetime(e_date.year, e_date.month, e_date.day, 0, 0, 0) days = (e_base_date - s_base_date).days + 1 date_list = [s_base_date + timedelta(days=x) for x in range(0, days)] for base_date in date_list: ret, cur = self.dbm.check_tick_cache(code, base_date, tick="1") if ret: self.logger.debug( "No need to save data. already has data in DB") continue raw_data = self.kw.stock_price_by_tick(code, tick=tick, screen_no=screen_no, date=base_date) tick_data = { 'code': code, 'stock_name': stock_name, 'date': base_date, 'time_series_1tick': [{ 'timestamp': _d['date'], '현재가': _d['현재가'], '거래량': _d['거래량'] } for _d in raw_data] # 'time_series': [{'timestamp': _d['date'], '현재가': _d['현재가'], '거래량': _d['거래량']} for _d in raw_data] } # DB에 없으면 수행되는 부분이라서 insert 함수를 사용. self.dbm.save_tick_data(tick_data, tick="1") def main(self): # 사용자 정의 부분_S # 8/2일자 조건검색식으로 검출된 모든 종목의 1tick data를 수집한다. with open("base_date_when_collect_tick_data.txt") as f: year, month, day = [int(i) for i in f.read().strip().split(" ")] self.base_date = datetime(year, month, day) # 사용자 정의 부분_E # kospi, kosdaq 모든 종목의 코드와 종목명 정보를 불러온다. self.stock_info = self.kw.get_stock_basic_info() code_list = self.dbm.get_code_list_condi_search_result(self.base_date) code_list.sort() self.dbm.record_collect_tick_data_status("START", self.base_date, tick="1") for num, code in enumerate(code_list, 1): stock_name = self.stock_info[code]["stock_name"] if self.dbm.already_collect_tick_data(code, self.base_date, tick="1"): self.logger.debug( "{}/{} - {}:{} Skip. Already saved data!".format( num, len(code_list), code, stock_name)) continue self.logger.debug("{}/{} - {}:{} Start !".format( num, len(code_list), code, stock_name)) self.collect_tick_data(code, stock_name, self.base_date, self.base_date, tick="1", screen_no="1234") self.logger.debug("{}/{} - {}:{} Completed !".format( num, len(code_list), code, stock_name)) self.dbm.save_collect_tick_data_history(code, self.base_date, tick="1") self.dbm.record_collect_tick_data_status("WORKING", self.base_date, tick="1") self.dbm.record_collect_tick_data_status("END", self.base_date, tick="1") self.logger.debug("save all 1tick information..") exit(0) # program exit def login(self): err_code = self.kw.login() if err_code != 0: self.logger.error("Login Fail") exit(-1) self.logger.info("Login success")
class Account(object): """ [Todo] 구현되어야 할 api list - """ def __init__(self, init_balance, th): self.시작잔고 = init_balance self.예수금 = init_balance self.총평가금액 = 0.0 self.총매입금액 = 0.0 self.총최대매입금액 = 0.0 self.총평가손익 = 0.0 self.총손익 = 0.0 # 총평가손익 + 총누적손익 self.추정자산 = init_balance self.총수익률 = 0.0 self.총누적손익 = 0.0 self.총누적수익률 = 0.0 self.보유주식 = {} self.core_index = ['시작잔고', '예수금', '총평가금액', '총최대매입금액', '총매입금액', '총평가손익', '총손익', '추정자산', '총수익률', '총누적손익', '총누적수익률'] self.core_index = ['총누적손익', '총누적수익률', '총손익', '총수익률', '총평가손익', '총평가금액', '총매입금액', '추정자산', '총최대매입금액', '예수금', '시작잔고'] self.check_core_index() self.dbm = DBM('TopTrader') self.timestamp = None self.trading_history = th self.logger = TTlog().logger def print_attr(self, trading_type, stock_name, code, reason, attr_list=None): if not cfg_mgr.ACCOUNT_MONITOR: return self.logger.debug("\n" + ("-" * 100)) self.logger.debug("[Account] Information : {}".format(self.timestamp)) self.logger.debug("[Trading Type] -> {}-{} ( {}/{} )".format(trading_type, reason, stock_name, code)) if bool(attr_list): attr_val = [] for attr in attr_list: val = self.__getattribute__(attr) log_str = "{}: {}".format(attr, val) if isinstance(val, float): log_str = "{}: {:.2f}".format(attr, val) attr_val.append(log_str) self.logger.debug("\n".join(attr_val)) else: self.logger.debug(self.__repr__()) def __repr__(self): core_index = ['timestamp'] + self.core_index # log = ["{}: {}".format(attr, str(self.__getattribute__(attr))) for attr in core_index] log = [] for attr in core_index: val = self.__getattribute__(attr) log_str = "{}: {}".format(attr, val) if isinstance(val, float): log_str = "{}: {:.2f}".format(attr, val) log.append(log_str) return "\t".join(log) def sync(self): """실제 계좌 정보를 불러와서 멤버 변수를 초기화 한다. :return: """ @common.type_check def update_sell(self, stock: Stock, price_per_stock: int, amount: int, reason: str): """특정 주식을 매도했을때, 계좌정보를 업데이트 :param code: :param amount: :return: """ # stock 객체 업데이트 stock.trading_reason = reason stock.update_sell(price_per_stock, amount) if cfg_mgr.debug_mode(): pass # self.print_attr('SELL', stock.stock_name, stock.code, reason) # pdb.set_trace() # 계좌 정보 업데이트 self.예수금 += stock.매매금액 self.총평가금액 += stock.평가금액변동 self.총매입금액 += stock.매입금액변동 self.총최대매입금액 = max(self.총매입금액, self.총최대매입금액) self.추정자산 = self.예수금 + self.총평가금액 # 현금(예수금) + 주식(총평가금액) self.총누적손익 += stock.실현손익 self.총평가손익 = float(self.총평가금액 - self.총매입금액) # 보유한 주식의가치로 얻은 손익 self.총손익 = self.총평가손익 + self.총누적손익 # 개념상 손익 (현금의 손익 + 가치의 손익) try: self.총수익률 = self.총평가손익 / self.총매입금액 * 100 # 보유한 주식에 대한 총 수익률 except ZeroDivisionError as e: self.총수익률 = 0.0 self.총누적수익률 = float(self.총누적손익) / self.총최대매입금액 * 100 # 실현한 수익에 대한 총 수익률 self.sell_transaction(stock) if stock.보유수량 == 0: del self.보유주식[stock.code] else: self.보유주식[stock.code] = stock try: if cfg_mgr.debug_mode(): self.print_attr('SELL', stock.stock_name, stock.code, reason) # pdb.set_trace() except Exception: pdb.set_trace() print("Exception") return "" def gen_trading_info(self, stock, trading_type): """ self.time = "" # 또는 체결시간? self.trading_type = "" # [B]uy, [S]ell self.code = "" # 종목의 코드 self.stock_name = "" # 종목 이름 self.stock_price = "" # buy, sell 시 얼마에 사고팔았는지? self.loss_flag = None # 손해를 봤는지, 안봤는지 (buy 일땐 None, sell 일땐 True/False self.profit_amount = 0 # 손해 또는 이익을 본 금액 (sell에서만 사용) self.profit_rate = 0.0 # 손해 또는 이익을 본 비율 (sell에서만 사용) :return: """ tr = Trading(trading_type) # 속성값이 겹치지 않게 잘 관리되어야 함. # 겹치는 속성이 있다면, Account속성에는 '총' 붙이도록 함. stock_index = stock.get_core_index() account_index = self.get_core_index() # temp code - allow to duplicate 'timestamp' ret = (set(stock_index) & set(account_index)) - set('timestamp') if bool(ret): self.logger.error("{} is duplicate index. program exit") exit(-1) tr = common.copy_attr(stock, tr, stock_index) tr = common.copy_attr(self, tr, account_index) return tr def sell_transaction(self, stock): """ :return: """ tr = self.gen_trading_info(stock, constant.SELL_TRADING_TYPE) self.trading_history.record_sell_transaction(tr) @common.type_check def update_buy(self, stock: Stock, price_per_stock: int, amount: int, reason: str): """특정 주식을 매수했을때, 계좌정보를 업데이트 :param code: :param price_per_stock: :param amount: :return: """ # stock 객체 업데이트 stock.trading_reason = reason stock.update_buy(price_per_stock, amount) if cfg_mgr.debug_mode(): pass # self.print_attr('BUY', stock.stock_name, stock.code, reason) # need to move to util/common pkg # pdb.set_trace() # 계좌 정보 업데이트 self.예수금 -= stock.매매금액 self.총평가금액 += stock.평가금액변동 self.총매입금액 += stock.매입금액변동 self.총최대매입금액 = max(self.총매입금액, self.총최대매입금액) self.추정자산 = self.예수금 + self.총평가금액 # 현금(예수금) + 주식(총평가금액) # self.총누적손익 += self.총누적손익 --> 변동없음 self.총평가손익 = float(self.총평가금액 - self.총매입금액) # 보유한 주식의가치로 얻은 손익 self.총손익 = self.총평가손익 + self.총누적손익 # 개념상 손익 (현금의 손익 + 가치의 손익) self.총수익률 = self.총평가손익 / self.총매입금액 * 100 # 보유한 주식에 대한 총 수익률 self.총누적수익률 = float(self.총누적손익) / self.총최대매입금액 * 100 # 실현한 수익에 대한 총 수익률 # 보유주식에 포함 self.보유주식[stock.code] = stock self.buy_transaction(stock) try: if cfg_mgr.debug_mode(): self.print_attr('BUY', stock.stock_name, stock.code, reason) # need to move to util/common pkg # pdb.set_trace() except Exception: # pdb.set_trace() print("Exception") def buy_transaction(self, stock): """ :return: """ tr = self.gen_trading_info(stock, constant.BUY_TRADING_TYPE) self.trading_history.record_buy_transaction(tr) def revaluate(self): """주식가치가 변경(현재가 변경)되면서 계좌에 업데이트 해야 하는 정보를 모두 업데이트 총평가금액 추정자산 = 예수금 + 총평가금액 총수익률 = 총평가금액 - 총매입금액 / 총매입금액 * 100 총손익 = (총평가금액 - 총매입금액) + 실현손익 :return: """ # self.총평가금액 = 0 for stock in self.보유주식.values(): # if cfg_mgr.debug_mode(): # self.print_attr('KEEP', stock.stock_name, stock.code, 'CHANGE_PRICE') # need to move to util/common pkg # pdb.set_trace() # 계좌 정보 업데이트 # self.예수금 -= stock.매매금액 self.총평가금액 += stock.평가금액변동 # self.총매입금액 += stock.매입금액변동 # self.총최대매입금액 = max(self.총매입금액, self.총최대매입금액) self.추정자산 = self.예수금 + self.총평가금액 # 현금(예수금) + 주식(총평가금액) # self.총누적손익 += self.총누적손익 --> 변동없음 self.총평가손익 = float(self.총평가금액 - self.총매입금액) # 보유한 주식의가치로 얻은 손익 self.총손익 = self.총평가손익 + self.총누적손익 # 개념상 손익 (현금의 손익 + 가치의 손익) self.총수익률 = self.총평가손익 / self.총매입금액 * 100 # 보유한 주식에 대한 총 수익률 # self.총누적수익률 = float(self.총누적손익) / self.총최대매입금액 * 100 # 실현한 수익에 대한 총 수익률 # if cfg_mgr.debug_mode(): # self.print_attr('KEEP', stock.stock_name, stock.code, 'CHANGE_PRICE') # need to move to util/common pkg # pdb.set_trace() def update_account_value(self, timestamp): """보유주식을 현 timestamp에 맞게 내부 정보를 업데이트 한다. 변경된 현재가를 기준으로 아래 필드가 함께 업데이트 된다. self.수익률 = (self.현재가 - self.매입평균가) / self.매입평균가 * 100 self.평가손익 = self.총매입금액 * self.수익률 self.총평가금액 = self.현재가 * self.보유수량 :param timestamp: :return: """ self.timestamp = timestamp if not bool(self.보유주식): return for stock in self.보유주식.values(): stock.update_stock_value(timestamp) self.revaluate() def add_stocks(self, code_list): """보유주식에 포함시킨다. :param code: :return: """ for code in code_list: self.보유주식[code] = Stock.get_instance(code) def set_tick1_data(self, target_date): """보유한 주식(Stock)에 대해 tick1 data를 생성한다. :param target_date: :return: """ for stock in self.보유주식.values(): stock.gen_time_series_sec1(target_date) def get_tick_data(self, code): """ :param code: :return: """ return self.보유주식[code].tick_data def get_balance(self): """현재 잔고를 반환 :return: """ return self.balance def has_code(self, code): """해당 종목을 현재 보유중인지 :param code: :return: """ return code in self.보유주식 def get_stock_count(self): """현재 보유중인 종목의 갯수를 반환 :return: """ return self.get_code_count() def get_code_count(self): """현재 보유중인 종목의 갯수를 반환 :return: """ return len(self.get_code_list_in_account()) def get_code_list_in_account(self): """현재 보유중인 종목코드를 반환 :return: """ return self.보유주식.keys() def get_stock_list_in_account(self): """현재 보유중인 종목(Stock)을 반환 :return: """ return [stock for stock in self.보유주식.values()] def clear_stock(self, stock, price, reason): """보유한 주식중 특정 종목 일괄 청산 :param code: :return: """ stock.trading_reason = reason stock.update_sell(int(price), int(stock.보유수량)) def all_clear_stocks(self, timestamp): """보유한 주식을 모두 일괄 청산 :return: """ for stock in self.보유주식.values(): self.clear_stock(stock, stock.get_curr_price(timestamp), constant.TRADING_TIME_EXPIRED) def get_core_index(self): """Account 객체의 핵심 지표 리스트. 반드시 Account 객체의 속성값과 동기화가 되어야 함. :return: """ return self.core_index def check_core_index(self): """계좌의 핵심 지표들이 모두 설정되었는지 check. 누락된 지표가 있다면 Exception 발생하고 프로그램 종료 :return: """ for index in self.get_core_index(): self.__getattribute__(index) def is_empty(self): return bool(self.보유주식)
class Stock(object): """code당 1개의 객체(singleton/code)를 생성한다. """ _inst = {} def __init__(self, code): self.empty = True self.first_trading = True # 첫 거래인지 self.timestamp = None self.first_buy_time = None # if add/delete index, must update self.core_index self.timestamp = None self.code = code self.stock_name = cfg_mgr.STOCK_INFO[code]['stock_name'] self.현재가 = 0 self.매매수량 = 0 self.보유수량 = 0 self.매매금액 = 0 self.매입금액 = 0 self.최대매입금액 = 0 self.매입평균가 = 0.0 self.평가손익 = 0.0 self.실현손익 = 0 self.누적손익 = 0 self.평가금액 = 0.0 self.수익률 = 0.0 self.누적수익률 = 0.0 self.평가금액변동 = 0.0 self.매입금액변동 = 0.0 self.실현손익변동 = 0.0 self.core_index = [ 'timestamp', 'code', 'stock_name', '현재가', '매매수량', '보유수량', '매매금액', '매입금액', '최대매입금액', '매입평균가', '평가손익', '실현손익', '누적손익', '평가금액', '수익률', '누적수익률', '평가금액변동', '매입금액변동', '실현손익변동' ] self.check_core_index() self.time_series_sec1 = None self.logger = TTlog().logger self.dbm = DBM('TopTrader') def print_attr(self, trading_type, attr_list=None): if not cfg_mgr.STOCK_MONITOR: return self.logger.debug("\n" + ("-" * 100)) self.logger.debug("[Stock] Information : {}/{}".format( self.stock_name, self.code)) self.logger.debug("[Trading Type] -> {}-{}".format( trading_type, self.trading_reason)) if bool(attr_list): attr_val = [] for attr in attr_list: val = self.__getattribute__(attr) log_str = "{}: {}".format(attr, val) if isinstance(val, float): log_str = "{}: {:.2f}".format(attr, val) attr_val.append(log_str) self.logger.debug("\t".join(attr_val)) else: self.logger.debug(self.__repr__()) # pass def __repr__(self): core_index = self.core_index # log = ["{}: {}".format(attr, str(self.__getattribute__(attr))) for attr in core_index] log = [] for attr in core_index: val = self.__getattribute__(attr) log_str = "{}: {}".format(attr, val) if isinstance(val, float): log_str = "{}: {:.2f}".format(attr, val) log.append(log_str) return "\t".join(log) @classmethod def get_instance(cls, code): if code not in cls._inst: cls._inst[code] = Stock(code) return cls._inst[code] @classmethod def get_new_instance(cls, code, recycle_time_series=False): if code in cls._inst and recycle_time_series: time_series_sec1 = cls._inst[code].time_series_sec1 cls._inst[code] = Stock(code) cls._inst[code].time_series_sec1 = time_series_sec1 else: cls._inst[code] = Stock(code) return cls._inst[code] def get_holding_period(self, timestamp=None): """최초 해당 주식을 보유한 시간으로부터 경과된 시간을 반환한다. :return: """ if not bool(timestamp): timestamp = self.timestamp if not bool(self.first_buy_time) or timestamp <= self.first_buy_time: return timedelta(seconds=0) return timestamp - self.first_buy_time def bep(self, event, price, amount=None): """Backup/Evaluate/PostAction prev : 기존 데이터 :return: """ # Backup self.backup() # dispatch Evaluate function eval_fn = self.__getattribute__("evaluate_{}".format(event)) eval_fn(**{'price': price, 'amount': amount}) # dispatch Post function post_fn = self.__getattribute__("post_{}".format(event)) post_fn(**{'price': price, 'amount': amount}) # Update def backup(self): """중요 지표들을 모두 백업한다. 백업 지표는 앞에 '기존' 키워드가 붙는다. :return: """ core_index = [ '현재가', '매매수량', '보유수량', '매매금액', '매입금액', '최대매입금액', '매입평균가', '평가손익', '실현손익', '누적손익', '평가금액', '수익률', '누적수익률' ] for index in core_index: val = self.__getattribute__(index) self.__setattr__('기존' + index, val) def evaluate_buy(self, **kwargs): price, amount = kwargs['price'], kwargs['amount'] self.현재가 = price self.매매수량 = amount self.보유수량 = self.기존보유수량 + self.매매수량 self.매매금액 = self.현재가 * self.매매수량 self.매입금액 = self.기존매입금액 + self.매매금액 self.매입금액변동 = self.매입금액 - self.기존매입금액 self.최대매입금액 = max(self.최대매입금액, self.매입금액) self.매입평균가 = self.매입금액 / self.보유수량 self.평가손익 = (self.현재가 - self.매입평균가) * self.보유수량 # self.실현손익 = self.실현손익 self.실현손익변동 = 0 # self.누적손익 = self.누적손익 self.평가금액 = self.현재가 * self.보유수량 if self.보유수량 == 0: self.수익률 = 0.0 else: self.수익률 = round((self.평가금액 - self.매입금액) / self.매입금액 * 100, 2) self.누적수익률 = round(float(self.누적손익) / self.최대매입금액 * 100, 2) self.평가금액변동 = self.평가금액 - self.기존평가금액 if self.first_trading: self.first_buy_time = self.timestamp self.first_trading = False def post_buy(self, **kwargs): price, amount = kwargs['price'], kwargs['amount'] core_index = [ 'timestamp', '현재가', '매입평균가', '수익률', '누적수익률', '누적손익', '매매수량', '보유수량', '매입금액', '최대매입금액', '평가손익', '실현손익', '평가금액', '매매금액' ] self.print_attr('BUY', core_index) # pdb.set_trace() def evaluate_sell(self, **kwargs): price, amount = kwargs['price'], kwargs['amount'] self.현재가 = price self.매매수량 = amount self.보유수량 = self.기존보유수량 - self.매매수량 self.매매금액 = self.현재가 * self.매매수량 self.매입금액 = self.기존매입평균가 * self.보유수량 self.매입금액변동 = self.매입금액 - self.기존매입금액 self.최대매입금액 = max(self.최대매입금액, self.매입금액) # self.매입평균가 = self.매입평균가 self.평가손익 = (self.현재가 - self.매입평균가) * self.보유수량 self.실현손익 = (self.현재가 - self.매입평균가) * self.매매수량 self.실현손익변동 = self.실현손익 - self.기존실현손익 self.누적손익 += self.실현손익 self.평가금액 = self.현재가 * self.보유수량 # self.기존수익률 = round((self.기존평가금액 - self.기존매입금액) / self.기존매입금액 * 100, 2) if self.보유수량 == 0: self.수익률 = 0.0 else: self.수익률 = round((self.평가금액 - self.매입금액) / self.매입금액 * 100, 2) self.누적수익률 = round(float(self.누적손익) / self.최대매입금액 * 100, 2) self.평가금액변동 = self.평가금액 - self.기존평가금액 if self.보유수량 == 0: self.first_trading = True self.first_buy_time = None def post_sell(self, **kwargs): price, amount = kwargs['price'], kwargs['amount'] core_index = [ 'timestamp', '현재가', '매입평균가', '실현손익', '기존수익률', '기존누적수익률', '누적손익', '매매수량', '보유수량', '매입금액', '최대매입금액', '평가손익', '평가금액', '매매금액' ] self.print_attr('SELL', core_index) # pdb.set_trace() def evaluate_change_price(self, **kwargs): price, amount = kwargs['price'], 0 self.현재가 = price self.매매수량 = amount # self.보유수량 = self.기존보유수량 - self.매매수량 self.매매금액 = 0 # self.매입금액 = self.기존매입평균가 * self.보유수량 # self.최대매입금액 = max(self.최대매입금액, self.매입금액) # self.매입평균가 = self.매입평균가 self.평가손익 = (self.현재가 - self.매입평균가) * self.보유수량 # self.실현손익 = (self.현재가 - self.매입평균가) * self.매매수량 # self.누적손익 += self.실현손익 self.평가금액 = self.현재가 * self.보유수량 if self.보유수량 == 0: self.수익률 = 0.0 else: self.수익률 = round((self.평가금액 - self.매입금액) / self.매입금액 * 100, 2) # self.누적수익률 = round(float(self.누적손익) / self.최대매입금액 * 100, 2) self.평가금액변동 = self.평가금액 - self.기존평가금액 def post_change_price(self, **kwargs): price, amount = kwargs['price'], 0 core_index = [ '현재가', '매매수량', '보유수량', '매매금액', '매입금액', '최대매입금액', '매입평균가', '평가손익', '실현손익', '누적손익', '평가금액', '수익률', '누적수익률' ] # self.print_attr('CHANGE PRICE') @common.type_check def update_sell(self, price_per_stock: int, amount: int): """현재가에 amount 만큼 매도를 하여, 내부 정보를 업데이트 한다. :param amount: :return: """ self.bep('sell', price_per_stock, amount) @common.type_check def update_buy(self, price_per_stock: int, amount: int): """해당 주식을 매수하였을 경우, 관련 지표를 업데이트 :param price_per_stock: :param amount: :return: """ self.bep('buy', price_per_stock, amount) def update_stock_value(self, timestamp): """해당 주식의 현재가가 변경되었을 경우, 관련 지표를 업데이트 :param timestamp: :return: """ self.timestamp = timestamp 현재가 = self.time_series_sec1.ix[timestamp]['현재가'] self.bep('change_price', 현재가) def set_strategy(self, strg): """ :param strg: :return: """ self.strg = strg def gen_time_series_sec1(self, target_date): """특정종목 특정일의 초단위 tick_data(DataFrame 객체)를 생성한다. timestamp 현재가 volumn 2018-08-02 09:00:00 0.0 0.0 2018-08-02 09:00:01 15000.0 130.0 2018-08-02 09:00:02 15000.0 91.0 2018-08-02 09:00:03 15500.0 0.0 ...(생략) :param target_date: :return: """ if self.time_series_sec1 is not None: self.logger.info( "{}/{} use existing gen_time_series_sec1..".format( self.stock_name, self.code)) return self.time_series_sec1 self.logger.info("{}/{} gen_time_series_sec1..".format( self.stock_name, self.code)) y, m, d = target_date.year, target_date.month, target_date.day s_time = datetime(y, m, d, 9, 0, 0) e_time = datetime(y, m, d, 15, 30, 0) df = pd.DataFrame( self.dbm.get_tick_data(self.code, target_date, tick="1")) ts_group = df.groupby('timestamp') price = pd.DataFrame(ts_group.max()['현재가']) index = pd.date_range(s_time, e_time, freq='S') price = price.reindex(index, method='ffill', fill_value=0) # volumn = pd.DataFrame(ts_group.sum()['거래량']) # volumn = volumn.reindex(index, method='ffill', fill_value=0) # price['volumn'] = volumn # index = timestamp (freq=second) # 현재가 = max, ffill, fill_value=0 # 거래량 = sum, ffill, fill_value=0 self.time_series_sec1 = price return self.time_series_sec1 def get_curr_price(self, timestamp): """현 시점(timestamp)의 주가를 반환한다. :param timestamp: :return: """ if self.time_series_sec1 is None: y, m, d = timestamp.year, timestamp.month, timestamp.day self.time_series_sec1 = self.gen_time_series_sec1(datetime( y, m, d)) return self.time_series_sec1.ix[timestamp]['현재가'] def get_core_index(self): """Stock객체의 핵심 지표 리스트. 반드시 Stock객체의 속성값과 동기화가 되어야 함. :return: """ return self.core_index def check_core_index(self): """ :return: """ for index in self.get_core_index(): self.__getattribute__(index)