[docs]classPositionClassifier:"""주문, 포지션의 주체를 분류하기 위한 클래스입니다. 자동 거래 프로그램에서 사용중인 계좌로 직접 HTS, MTS 등으로 거래를 하는 유저를 위해 만들어진 분류기 입니다. """logger=get_logger(__name__+".PositionClassifier")DEF_DIRECT_POSITION_KEY="direct_position"DEF_DIRECT_ORDER_KEY="direct_order"DEF_AUTO_POSITION_KEY="auto_position"DEF_AUTO_ORDER_KEY="auto_order"DEF_TAG_AUTO_ORDER_KEY="tag_auto_order"
[docs]def__init__(self,simple_data_api:EBestSimpleDomesticStock|KISSimpleDomesticStock,kv_store_collection,default_type="auto",):"""OrderClassifier 초기화 메서드입니다 Args: data_api (KISSimpleDomesticStock): 시세 조회 및 시장 데이터 조회를 위한 API 인터페이스 kv_store_collection (String): kv_store에서 사용할 콜렉션 default_type (String): 태깅되지 않은 종목을 무엇으로 간주할지. 'auto', 'direct' 둘 중 한 값을 가짐 Note: 생성된 인스턴스는 즉시 거래가 가능한 상태가 되며, 모든 거래 관련 작업은 자동으로 로깅됩니다. """# key: asset_code(str), value: quantity(int)self.auto_positions={}self.direct_positions={}# key: order_no(str), value: remain_quantity(int)self.auto_pending_orders={}self.direct_pending_orders={}# list of order_no(str)self.tagged_auto_pending_orders=[]self.kv_store=KVStore(kv_store_collection)self.api=simple_data_apiifdefault_typein['auto','direct']:self.default_type=default_typeelse:self.default_type='auto'self.tracker=TradingTracker(simple_data_api)self.tracker.add_pending_order_update_callback(self._on_pending_order_update)self.set_initial_position()
[docs]defset_initial_position(self)->None:"""코드 시작시 현재 account에 있는 position과 kv_store를 참고해서 기존 포지션을 분류한다. Args: """cur_pos=self.api.get_positions()kv_auto_pos=self.kv_store.get(self.DEF_AUTO_POSITION_KEY)or{}kv_direct_pos=self.kv_store.get(self.DEF_DIRECT_POSITION_KEY)or{}self.logger.info(f"set_initial_position. kv_auto_pos: {kv_auto_pos}, kv_direct_pos: {kv_direct_pos}")forposincur_pos:asset_code=pos.asset_codequantity=pos.quantityifkv_auto_posisnotNoneandasset_codeinkv_auto_pos:self.auto_positions[asset_code]=min([quantity,kv_auto_pos[asset_code]])quantity-=self.auto_positions[asset_code]ifasset_codeinkv_direct_pos:self.direct_positions[asset_code]=min(quantity,kv_direct_pos[asset_code])quantity-=self.direct_positions[asset_code]ifquantity>0:ifself.default_type=="auto":ifasset_codeinself.auto_positions:self.auto_positions[asset_code]+=quantityelse:self.auto_positions[asset_code]=quantityelifself.default_type=="direct":ifasset_codeinself.direct_positions:self.direct_positions[asset_code]+=quantityelse:self.direct_positions[asset_code]=quantityself.update_positions()self.logger.info(f"set_initial_position.\npositions: {cur_pos}\nauto_positions: {self.auto_positions}\ndirect_positions: {self.direct_positions}")
[docs]defclear_orders(self)->None:""" 날 바뀌고 kv store에 남아있는 오더는 모두 유효지 않음 장 시작 전에 호출해주면 좋은 함수 """self.logger.info("clear_orders")self.auto_pending_orders={}self.direct_pending_orders={}self.tagged_auto_pending_orders=[]self.update_orders()
[docs]defget_sellable_quantity_by_auto(self,asset_code,quantity)->int:"""pyqqq로 매수했던 수량을 체크해서, 매도간에 pyqqq로 샀던게 몇 주인지 확인하는 함수 Args: asset_code (String): 종목코드 6자리 quantity (int): 체크 하려는 수량. 만약 프로그램으로 5주, 손으로 5주 매수해서 10주 들고있는 상황에서 quantity가 8이면 5를 return 한다. """ret=0positions=self.api.get_positions()forpositioninpositions:ifasset_code==position.asset_code:remained=min(quantity,position.sell_possible_quantity)ifasset_codeinself.auto_positions:diff=min(self.auto_positions[asset_code],remained)ret+=diffremained-=diffifself.default_type=="auto":ifasset_codeinself.direct_positions:diff=min(self.direct_positions[asset_code],remained)remained-=diffret+=remainedbreakreturnret
# 매수, 매도 할때 명시적으로 호출해줘야함
[docs]deftagging_order_auto(self,order_no:str):self.logger.info(f"tagging_order_auto. order_no: {order_no}")iforder_noinself.auto_pending_orders:self.logger.info(f"tagging_order_auto. {order_no} already exist")returneliforder_noinself.direct_pending_orders:self.logger.info(f"tagging_order_auto. {order_no} has been in direct_pending_orders")self.auto_pending_orders[order_no]=self.direct_pending_orders[order_no]delself.direct_pending_orders[order_no]eliforder_nonotinself.tagged_auto_pending_orders:self.logger.info(f"tagging_order_auto. {order_no} is tagged.")self.tagged_auto_pending_orders.append(order_no)self.update_orders()
def_on_pending_order_update(self,status,order:StockOrder):"""Tracker를 통해 TradingTracker의 add_pending_order_update_callback 함수에 인자로 넣으면 정상 동작함. """asset_code=order.asset_codeorder_no=order.order_nopending_quantity=order.pending_quantityself.logger.info(f"_on_pending_order_update. status: {status} order: {order}")# 주문 처리시iforder.side==OrderSide.BUY:ifstatusin["partial","completed"]:iforder_noinself.auto_pending_orders:prev_filled_quantity=order.quantity-self.auto_pending_orders[order_no]filled_quantity=order.filled_quantity-prev_filled_quantityself._add_dict_with_key(self.auto_pending_orders,order_no,-filled_quantity)self._add_dict_with_key(self.auto_positions,asset_code,filled_quantity)eliforder_noinself.direct_pending_orders:prev_filled_quantity=order.quantity-self.direct_pending_orders[order_no]filled_quantity=order.filled_quantity-prev_filled_quantityself._add_dict_with_key(self.direct_pending_orders,order_no,-filled_quantity)self._add_dict_with_key(self.direct_positions,asset_code,filled_quantity)eliforder_noinself.tagged_auto_pending_ordersorself.default_type=="auto":self.auto_pending_orders[order_no]=pending_quantityself._add_dict_with_key(self.auto_positions,asset_code,order.filled_quantity)elifself.default_type=="direct":self.direct_pending_orders[order_no]=pending_quantityself._add_dict_with_key(self.direct_positions,asset_code,order.filled_quantity)# 주문 접수시elifstatusin["accepted"]:iforder_noinself.tagged_auto_pending_orders:self.auto_pending_orders[order_no]=order.pending_quantityelse:self.direct_pending_orders[order_no]=order.pending_quantityelifstatusin["cancelled"]:iforder_noinself.auto_pending_orders:self.auto_pending_orders[order_no]-=order.quantityeliforder_noinself.direct_pending_orders:self.direct_pending_orders[order_no]-=order.quantityeliforder.side==OrderSide.SELL:ifstatusin["accepted"]:# 매도시엔 보유 잔량을 주문 접수시 미리 차감한다.iforder_noinself.tagged_auto_pending_orders:self.auto_pending_orders[order_no]=pending_quantityremain=self._add_dict_with_key(self.auto_positions,asset_code,-pending_quantity)ifremain<0:self._add_dict_with_key(self.direct_positions,asset_code,remain)else:self.direct_pending_orders[order_no]=pending_quantityremain=self._add_dict_with_key(self.direct_positions,asset_code,-pending_quantity)ifremain<0:self._add_dict_with_key(self.auto_positions,asset_code,remain)elifstatusin["cancelled"]:# 주문 취소시엔 보유잔량을 복구한다.iforder_noinself.auto_pending_orders:# order.quantity가 주문 취소한 수량임self.auto_pending_orders[order_no]-=order.quantityself._add_dict_with_key(self.auto_positions,asset_code,order.quantity)eliforder_noinself.direct_pending_orders:self.direct_pending_orders[order_no]-=order.quantityself._add_dict_with_key(self.direct_positions,asset_code,order.quantity)elifstatusin["partial"]:# 부분 체결시엔 미체결분량으로 pending order 잔량을 업데이트한다.iforder_noinself.auto_pending_orders:self.auto_pending_orders[order_no]=pending_quantityeliforder_noinself.direct_pending_orders:self.direct_pending_orders[order_no]=pending_quantityelifstatusin["completed"]:# 전체 체결시엔 메모리 정리를 위해 해당 주문번호를 비워준다. 진행하지 않아도 무방함iforder_noinself.auto_pending_orders:delself.auto_pending_orders[order_no]eliforder_noinself.direct_pending_orders:delself.direct_pending_orders[order_no]# clearself._clear_below_zero_key_value(self.auto_pending_orders,order_no)self._clear_below_zero_key_value(self.direct_pending_orders,order_no)self._clear_below_zero_key_value(self.auto_positions,asset_code)self._clear_below_zero_key_value(self.direct_positions,asset_code)self.update_orders()self.update_positions()
[docs]defprint_current_status(self):self.logger.info(f"print_current_status. auto positions: {self.auto_positions} direct positions: {self.direct_positions}.\nauto orders: {self.auto_pending_orders} direct_orders: {self.direct_pending_orders}.\n tagged order_nos: {self.tagged_auto_pending_orders}")
def_add_dict_with_key(self,target_dict,key,diff):""" target dict가 비어있든 아니든 적절히 diff를 더해주는 연산. 만약 연산 결과가 음수라면 그 음수값을 return하여 적절히 사용할 수 있도록 함 """ifkeyintarget_dict:target_dict[key]+=diffelse:target_dict[key]=diffiftarget_dict[key]<0:returntarget_dict[key]else:return0def_clear_below_zero_key_value(self,target_dict,key):ifkeyintarget_dictandtarget_dict[key]<=0:deltarget_dict[key]