class HistoricalPriceRecord(collections.namedtuple('HistoricalPriceRecord', ['time', 'open', 'high', 'low', 'close', 'volume'])): __slots__ = () _krx_timezone = get_krx_timezone() @classmethod def from_tuple(cls, tup): if '일자' in tup._fields: dt = datetime.datetime.strptime(tup.일자, '%Y%m%d') dt = cls._krx_timezone.localize(dt) time = dt.timestamp() * (10 ** 6) # pylint: disable=redefined-outer-name elif '체결시간' in tup._fields: dt = datetime.datetime.strptime(tup.체결시간, '%Y%m%d%H%M%S') dt = cls._krx_timezone.localize(dt) time = dt.timestamp() * (10 ** 6) else: raise KiwoomOpenApiPlusError('Cannot specify time') open = abs(float(tup.시가)) # pylint: disable=redefined-builtin high = abs(float(tup.고가)) low = abs(float(tup.저가)) close = abs(float(tup.현재가)) volume = abs(float(tup.거래량)) return cls(time, open, high, low, close, volume) @classmethod def records_from_dataframe(cls, df): return [cls.from_tuple(tup) for tup in df[::-1].itertuples()] @classmethod def dict_records_from_dataframe(cls, df): return [msg._asdict() for msg in cls.records_from_dataframe(df)]
class API: # 우선은 최대한 기존 Oanda 구현을 유지한채로 맞춰서 동작할 수 있도록 구현해놓고 # 추후 동작이 되는게 확인되면 천천히 하단 API 에 맞게 최적화를 하는 방향으로 작업하는 것으로... _krx_timezone = get_krx_timezone() def __init__(self, context): self._context = context self._codes = self._context.GetCodeListByMarketAsList() def __getattr__(self, name): return getattr(self._context, name) def get_instruments_original(self, account, instruments): # TODO: 계좌에 따라 시장이 다를 수 있음 instruments = self.GetStockInfoAsDataFrame(instruments) instruments = [tup._asdict() for tup in instruments.itertuples(index=False)] response = {'instruments': instruments} return response def get_instruments(self, account, instruments): if isinstance(instruments, str): instruments = [instruments] instruments = [inst for inst in instruments if inst in self._codes] response = {'instruments': instruments} return response def get_history(self, trcode, inputs, dtbegin=None, dtend=None): if trcode == 'opt10079': code = inputs['종목코드'] interval = inputs['틱범위'] adjusted_price = inputs.get('수정주가구분') == '1' df = self.GetTickStockDataAsDataFrame(code, interval, dtend, dtbegin, adjusted_price=adjusted_price) elif trcode == 'opt10080': code = inputs['종목코드'] interval = inputs['틱범위'] adjusted_price = inputs.get('수정주가구분') == '1' df = self.GetMinuteStockDataAsDataFrame(code, interval, dtend, dtbegin, adjusted_price=adjusted_price) elif trcode == 'opt10081': code = inputs['종목코드'] adjusted_price = inputs.get('수정주가구분') == '1' df = self.GetDailyStockDataAsDataFrame(code, dtend, dtbegin, adjusted_price=adjusted_price) elif trcode == 'opt10082': code = inputs['종목코드'] adjusted_price = inputs.get('수정주가구분') == '1' df = self.GetWeeklyStockDataAsDataFrame(code, dtend, dtbegin, adjusted_price=adjusted_price) elif trcode == 'opt10083': code = inputs['종목코드'] adjusted_price = inputs.get('수정주가구분') == '1' df = self.GetMonthlyStockDataAsDataFrame(code, dtend, dtbegin, adjusted_price=adjusted_price) elif trcode == 'opt10094': code = inputs['종목코드'] adjusted_price = inputs.get('수정주가구분') == '1' df = self.GetYearlyStockDataAsDataFrame(code, dtend, dtbegin, adjusted_price=adjusted_price) else: raise KiwoomOpenApiPlusError('Unexpected trcode %s' % trcode) candles = HistoricalPriceRecord.dict_records_from_dataframe(df) response = {'candles': candles} return response def get_positions(self, account): _summary, foreach = self.GetAccountEvaluationStatusAsSeriesAndDataFrame(account) positions = [{ 'instrument': tup.종목코드, 'side': 'buy', 'units': float(tup.보유수량), 'avgPrice': float(tup.매입금액) / float(tup.보유수량), } for tup in foreach.itertuples()] response = {'positions': positions} return response def get_account(self, account): summary, _foreach = self.GetAccountEvaluationStatusAsSeriesAndDataFrame(account) response = { 'marginAvail': float(summary['D+2추정예수금']), 'balance': float(summary['추정예탁자산']), } return response def create_order(self, account, **kwargs): request_name = 'create_order(%s, %s)' % (account, kwargs) screen_no = '' account_no = account order_type = { 'buy': 1, 'sell': 2, }[kwargs['side']] code = kwargs['instrument'] quantity = kwargs['units'] price = kwargs.get('price', 0) quote_type = { 'limit': '00', 'market': '03', }[kwargs['type']] original_order_no = '' responses = self.OrderCall( request_name, screen_no, account_no, order_type, code, quantity, price, quote_type, original_order_no, ) _msg = next(responses) _tr = next(responses) accept = next(responses) accept_data = dict(zip(accept.single_data.names, accept.single_data.values)) result = { 'orderOpened': {'id': accept_data['주문번호']} } return result def close_order(self, account, oid, size, dataname): request_name = 'close_order(%s, %s, %s, %s)' % (account, oid, size, dataname) screen_no = '' account_no = account order_type = 3 if size >= 0 else 4 code = dataname quantity = 0 price = 0 quote_type = '' original_order_no = oid responses = self.OrderCall( request_name, screen_no, account_no, order_type, code, quantity, price, quote_type, original_order_no, ) _msg = next(responses) _tr = next(responses) _accept = next(responses) confirm = next(responses) confirm_data = dict(zip(confirm.single_data.names, confirm.single_data.values)) result = { 'orderOpened': {'id': confirm_data['주문번호']} } return result def get_today_quotes_by_code(self, codes=None): if codes is None: codes = self._codes df = self.GetStockQuoteInfoAsDataFrame(codes) dt = pd.to_datetime(df['일자'].str.cat(df['체결시간']), format='%Y%m%d%H%M%S').dt.tz_localize(self._krx_timezone) dt = dt.astype(np.int64) // 10 ** 3 df = pd.DataFrame({ 'dataname': df['종목코드'], 'time': dt, 'open': df['시가'].astype(float).abs(), 'high': df['고가'].astype(float).abs(), 'low': df['저가'].astype(float).abs(), 'close': df['종가'].astype(float).abs(), 'volume': df['거래량'].astype(float).abs(), }) msgs = {tup.dataname: tup._asdict() for tup in df.itertuples(index=False)} return msgs
class KiwoomOpenApiPlusPriceEventChannel: _krx_timezone = get_krx_timezone() def __init__(self, stub): self._stub = stub self._fid_list = KiwoomOpenApiPlusRealType.get_fids_by_realtype('주식시세') self._request_observer = QueueBasedIterableObserver() self._request_iterator = iter(self._request_observer) self._response_iterator = self._stub.BidirectionalRealCall(self._request_iterator) self._response_subject = Subject() self._response_scheduler_max_workers = 8 self._response_scheduler = ThreadPoolScheduler(self._response_scheduler_max_workers) self._buffered_response_iterator = QueueBasedBufferedIterator(self._response_iterator) self._response_observable = rx.from_iterable(self._buffered_response_iterator, self._response_scheduler) self._response_subscription = self._response_observable.subscribe(self._response_subject) self._subjects_by_code = {} self.initialize() def close(self): for _code, (_subject, subscription) in self._subjects_by_code.items(): subscription.dispose() self._response_subscription.dispose() self._buffered_response_iterator.stop() self._response_iterator.cancel() def __del__(self): self.close() def initialize(self): request = KiwoomOpenApiPlusService_pb2.BidirectionalRealRequest() request.initialize_request.fid_list.extend(self._fid_list) # pylint: disable=no-member self._request_observer.on_next(request) def register_code(self, code): request = KiwoomOpenApiPlusService_pb2.BidirectionalRealRequest() code_list = [code] fid_list = KiwoomOpenApiPlusRealType.get_fids_by_realtype('주식시세') request.register_request.code_list.extend(code_list) # pylint: disable=no-member request.register_request.fid_list.extend(fid_list) # pylint: disable=no-member self._request_observer.on_next(request) logging.debug('Registering code %s for real events', code) def is_for_code(self, response, code): return response.arguments[0].string_value == code def filter_for_code(self, code): return ops.filter(lambda response: self.is_for_code(response, code)) def is_valid_price_event(self, response): return all(name in response.single_data.names for name in ['20', '27', '28']) def filter_price_event(self): return ops.filter(self.is_valid_price_event) def time_to_timestamp(self, fid20): dt = datetime.datetime.now(self._krx_timezone).date() tm = datetime.datetime.strptime(fid20, '%H%M%S').time() dt = datetime.datetime.combine(dt, tm) dt = self._krx_timezone.localize(dt) return dt.timestamp() * (10 ** 6) def event_to_dict(self, response): single_data = dict(zip(response.single_data.names, response.single_data.values)) result = { 'time': self.time_to_timestamp(single_data['20']), 'bid': abs(float(single_data['28'])), 'ask': abs(float(single_data['27'])), } return result def convert_to_dict(self): return ops.map(self.event_to_dict) def get_observable_for_code(self, code): self.register_code(code) if code not in self._subjects_by_code: subject = Subject() subscription = self._response_subject.pipe( self.filter_for_code(code), self.filter_price_event(), self.convert_to_dict(), ).subscribe(subject) self._subjects_by_code[code] = (subject, subscription) subject, subscription = self._subjects_by_code[code] return subject