Slide 1

Slide 1 text

Building a Cryptocurrency Trading System Was it successful? Will Choi 2021.06.02

Slide 2

Slide 2 text

이 주제를 선택한 이유 1. 데이터 중심 어플리케이션 설계에 관한 주제 ❌ 2. 버즈빌에서 일하며 배운 것 ➡ 빌링서비스 구축기 ❌ 3. 개인적으로 개발했던 경험기👌 a. 축구 기록 관리 웹 어플리케이션 운영 경험 ❌ b. 부동산 데이터 분석 어플리케이션 개발 경험 ❌ c. 암호화폐 자동 매매 알고리즘 개발 경험 👌

Slide 3

Slide 3 text

System Trading을 개발한 계기 1. 2017년 갑자기 전회사 개발팀으로 업무 변경 - 업무 : Java - 관심 : Python 2. 회사에 Python을 전파하고자 하였으나 실패 3. 그냥 혼자 Python 프로젝트를 만들자 4. 암호화폐 Boom 5. 은행이율보다 높은 수익을 내는 뭔가를 만들자 - 연이율10% ?? / 월이율 1% ?? 6. 2017년 추석 연휴

Slide 4

Slide 4 text

알고리즘 v1 개요 1. 가격 하락시 매수 ➡ 조금 오르면 매도 2. 매수 했는데 계속 하락 ➡ 손절 매 수 매 도 매 수 매 도 매 수 매 도 매 수 손 절

Slide 5

Slide 5 text

문제 1. 완벽한 타이밍을 맞추기 쉽지 않다.➡ 거의 불가능 2. 손절이 많이 발생해서 이익을 상쇄 시켜 버린다.

Slide 6

Slide 6 text

워렌버핏의 투자원칙

Slide 7

Slide 7 text

워렌버핏의 투자원칙

Slide 8

Slide 8 text

알고리즘 v2 개요 1. 가격 하락시 매수 ➡ 조금 오르면 매도 2. 매수 했는데 계속 하락 ➡ 또 다시 매수(매도시점을 앞당김) ➡ 이익이 발생하는 가격이 오면 매도 🔄 반복 매 수 매 도 매 수 매 도 매 수 매 도 매 수 매 수 매 도

Slide 9

Slide 9 text

리스크 1. 암호화폐가 없어진다. 2. 거래소 폐쇄 또는 암호화폐 거래 금지 등 3. 매수한 암호화폐가 가격이 떨어지기만 한 후 영원히 혹은 몇년간 상승하지 않거나 사라진다.

Slide 10

Slide 10 text

리스크 1. 암호화폐 자체가 없어진다. ➡ 없어질 확률은 거의 없을 것이다. 2. 거래소 폐쇄 또는 암호화폐 거래 금지 등 ➡ 약간의 리스크는 있지만 현재 세계적인 추세로는 폐쇄나 금지가 발생하지 않을 것이다. 3. 매수한 암호화폐가 가격이 떨어지기만 한 후 영원히 혹은 몇년간 상승하지 않거나 사라진다. ➡ 신뢰할 수 있는 종류만 매매하도록 한다.

Slide 11

Slide 11 text

알고리즘 세부사항 진화 과정 1. Is_buy_timing - 현재 시장의 추세를 분석 - 분석된 추세에 따라 매수 시점을 결정 2. sell_order - 박리 다매 - 조금 오르면 매도(예:0.1%) 🔄 반복

Slide 12

Slide 12 text

문제 1. Make_buy_order 를 했는데 매수가 안되고 가격이 상승 - 하나도 체결이 안되는 경우 - 일부만 체결이 된 경우

Slide 13

Slide 13 text

알고리즘 진화 1. 매수 도중에 가격이 오르면 매수 주문을 취소! 2. 취소후에 보유자산이 있으면 평균단가에 적정한 이율을 적용하여 매도! 3. 없으면 다시 시장 추세 분석!

Slide 14

Slide 14 text

문제 1. Make_sell_order를 했는데 매도가 안되고 가격이 하락

Slide 15

Slide 15 text

알고리즘 진화 1. 매도 도중 가격이 하락 하는 경우 추가 매수 할 수 있는 예산이 있는지 확인 2. 예산이 있다면 매도 주문을 취소! 3. 추가 매수 주문 생성!

Slide 16

Slide 16 text

알고리즘 시뮬레이션

Slide 17

Slide 17 text

알고리즘 시뮬레이션 JB x3 X4

Slide 18

Slide 18 text

code def process(): api = API() data = None while not is_buy_timing(data): # analysis data from api data = api.analysis() buy_order = api.make_buy_order() while not buy_order.is_completed(): current_price = api.get_current_price() if current_price > buy_order.price * 1.01: api.cancel_order(buy_order) if api.get_balance() > 0: break else: return 0 # profit is zero sell_order = api.make_sell_order() while not sell_order.is_completed(): current_price = api.get_current_price() if current_price < sell_order.price * 0.9: if has_budget(): api.cancel_order(sell_order) buy_order_2 = api.make_buy_order() while not buy_order_2.is_completed(): ... profit = sell_order.funds - buy_order.funds return profit

Slide 19

Slide 19 text

누더기 코드 - 극악의 디버깅 난이도 - 코드 수정 거의 불가능 - 나중에 코드 내용 파악 거의 불가능 - 각종 에러 대응 거의 불가능 - Logic 에러 - Api 에러 - 네트워크 에러 - 알 수 없는 에러 - 등등등...

Slide 20

Slide 20 text

아키텍처를 도입하자 Clean Architecture DDD

Slide 21

Slide 21 text

아키텍처를 도입하자 Domain Layer - 나를 위해 24시간 일하는 Worker - 분석담당 Worker(Waiting) - 매수담당 Worker(Buying) - 매도담당 Worker(Selling) - Worker는 공통적으로 work라는 행동을 하는데 각자의 work방식은 고유하다. - Worker는 일할때 필요한 api(인터페이스)가 반드시 필요하다. - 일할 때 필요한 각종 데이터도 가지고 일한다.(budget, market, orders 등)

Slide 22

Slide 22 text

아키텍처를 도입하자 Repository Pattern - Worker들의 작업의 영속성을 위해 DB에 각 Worker들의 상태를 저장할 필요가 있었다.

Slide 23

Slide 23 text

아키텍처를 도입하자 Service Layer ● 언제까지 추가매수? ○ 예산이 남아있을때 까지! ● 예산 분배 전략이 중요! ● 추가 매수하는 시점이 중요! ex) 예산: 20,000원 1차: 100원/1000원어치: 101원/10원 2차: 96원/1000원어치: 99원/20원 3차: 93원/1000원어치: 97원/30원 4차: 73원/3500원어치: 85원/190원 5차: 58원/3500원어치: 74원/295원 6차: 26원/5000원어치: 45원/2250원 7차: 12원/5000원어치: 30원/3000원

Slide 24

Slide 24 text

code def work(): while True: if worker := WorkerRepository.get_oldest_worker(): try: worker.work() WorkerRepository.update(worker) if worker.status == 'end': new_worker = ... WorkerRepository.add(new_worker) send_alarm(...) except domain_model.ApiError as e: send_alarm(f'API Error: ...") except Exception as e: send_alarm(f'Exception: ...") Service Layer

Slide 25

Slide 25 text

code class Worker(abc.ABC): def __init__(self, api: AbstractApi ...): self.api = api ... @abc.abstractmethod def work(self): raise NotImplementedError def buy_order(self, price: float) -> Order: ... order = self.api.buy_order(price, self.budget) ... def sell_order(self, price: float) -> Order: ... def cancel_order(self, order: Order): ... def update_from_api(self): ... Domain Layer

Slide 26

Slide 26 text

code Domain Layer class WaitingWorker(Worker): def work(self): if self._is_buy_timing(): order = self.buy_order(self.trade_price) self.status = WorkerStatus.BUYING self.orders.append(order) def _is_buy_timing(self): trend = self._decide_trend() if self.trade_price < self.waiting_price: if trend == MarketTrend.OVERHEATING: return self.trade_price < self.short_average * SHORT_TIMING ... elif trend == MarketTrend.NORMAL: return ... elif trend == MarketTrend.FROZEN: return ... return False def _decide_trend(self): if self.trade_price > self.long_average * (...): return MarketTrend.OVERHEATING elif ...: return MarketTrend.NORMAL elif ...: return MarketTrend.FROZEN else return MarketTrend.DANGER

Slide 27

Slide 27 text

code Domain Layer class BuyingWorker(Worker): def work(self): latest_order = self.orders.get_latest_order() sell_rate = self.get_sell_rate() if latest_order.status == OrderStatus.WAIT: if self.trade_price >= self.orders.avg_buy_price * sell_rate: self.cancel_order(latest_order) else: # cancel or done if self.balance > 0: order = self.sell_order(self.orders.avg_buy_price * sell_rate) self.status = WorkerStatus.SELLING self.orders.append(order) else: self.status = WorkerStatus.WAITING

Slide 28

Slide 28 text

code Domain Layer class SellingWorker(Worker): def work(self): latest_order = self.orders.get_latest_order() if latest_order.status == OrderStatus.WAIT: if self.budget != EMPTY and self._is_additional_buy_timing(): self.cancel_order(latest_order) else: # cancel or done if wait_orders := self.orders.get_wait_orders(): for o in wait_orders: self.cancel_order(o) else: if self.balance > 0: order = self.buy_order(self.next_price) self.status = WorkerStatus.BUYING self.orders.append(order) else: self.revenue = self.orders.get_sell_funds - self.orders.get_buy_funds self.status = WorkerStatus.END

Slide 29

Slide 29 text

code Domain Layer class AbstractApi(abc.ABC): def __init__(self, market: Market): self.market = market @abc.abstractmethod def buy_order(self, price: float, budget: float) -> Order: raise NotImplementedError @abc.abstractmethod def sell_order(self, price: float, volume: float) -> Order: raise NotImplementedError @abc.abstractmethod def cancel_order(self, order_id: str) -> str: raise NotImplementedError @abc.abstractmethod def get_order(self, order_id: str) -> Order: raise NotImplementedError @abc.abstractmethod def get_orders(self, order_ids: List[str]=None) -> List[Order]: raise NotImplementedError @abc.abstractmethod def get_prices(self, unit: str='minute', counts: int=1, to: datetime=None) -> List[Price]: raise NotImplementedError @abc.abstractmethod def get_balance(self) -> Balance: raise NotImplementedError

Slide 30

Slide 30 text

code Repository class Worker(models.Model): ... def to_domain(self, api: AbstractApi) -> domain_model.Worker: work_type = domain_model.Work if self.status == WorkerStatus.WAITING: work_type = domain_model.WaitingWorker elif self.status == WorkerStatus.BUYING: work_type = domain_model.BuyingWorker elif self.status == WorkerStatus.SELLING: work_type = domain_model.SellingWorker w = work_type( api=api(self.market), status=self.status, ... ) return w

Slide 31

Slide 31 text

아키텍처 도입후 최종 구성도

Slide 32

Slide 32 text

결과 Was it successful?

Slide 33

Slide 33 text

결과 Was it successful? - 은행이율보다 높은 수익률을 달성하였을까? 20-11 20-12 21-01 21-02 21-03 21-04 21-05

Slide 34

Slide 34 text

결과 Was it successful? - 6.42% - Not bad 20-11 20-12 21-01 21-02 21-03 21-04 21-05 0.8% 1.5% 0.3% 0.5% 0.45% 0.67% 2.2%

Slide 35

Slide 35 text

결론 - 투자는 없어도 되는 돈으로 - 주식에도 적용 - 클린아키텍처 & DDD - 끈질기게 계속 하면 된다.