튜토리얼약 6분 읽기

파이썬 스케줄러로 자동매매 돌리기 — 무한루프 대신 스케줄러를 쓰는 이유

while True로 시작했다가 겪은 문제

자동매매 코드를 처음 짤 때는 while True: 안에 time.sleep()을 넣는 방식으로 충분해 보인다. 실제로 초반에는 이렇게 돌렸다. 문제는 실행할 작업이 늘어나면서 드러났다. 시세 조회는 짧은 주기로, 잔고 점검은 그보다 긴 주기로, 장 마감 후 정리는 하루에 한 번 — 주기가 다른 작업이 한 반복문 안에 섞이자 시간 계산이 점점 꼬였다.

이 글은 특정 스케줄러 라이브러리의 API를 나열하지 않는다. 대신 "왜 반복문 대신 스케줄러 구조로 옮겨야 하는가"와, 최소한의 스케줄러 구조를 의사코드로 정리한다.

반복문 방식의 한계

스케줄러 구조로 옮기기 — 작업과 주기를 분리한다

python 코드 보기
class Scheduler:
    def __init__(self):
        self.jobs = []

    def every(self, interval_seconds, task_fn):
        self.jobs.append({"type": "interval", "interval": interval_seconds, "task": task_fn, "last_run": 0})

    def at_time(self, hour, minute, task_fn):
        self.jobs.append({"type": "daily", "at": (hour, minute), "task": task_fn, "last_run_date": None})

    def run_forever(self):
        while True:
            now = current_time()
            for job in self.jobs:
                if self._should_run(job, now):
                    self._run_safely(job, now)
            sleep(1)

    def _should_run(self, job, now):
        if job["type"] == "interval":
            return now - job["last_run"] >= job["interval"]
        hour, minute = job["at"]
        return (now.hour, now.minute) == (hour, minute) and job["last_run_date"] != now.date()

    def _run_safely(self, job, now):
        try:
            job["task"]()
        except Exception as e:
            notify(f"작업 실패: {e}")
        finally:
            if job["type"] == "interval":
                job["last_run"] = now
            else:
                job["last_run_date"] = now.date()

여기서 중요한 건 각 작업이 자기 주기만 신경 쓰고, 스케줄러가 "지금 뭘 실행할 시각인지" 판단을 전담한다는 점이다. _should_run은 작업 종류(주기 반복 vs 특정 시각)에 따라 다른 키(last_run / last_run_date)를 보고 판단하고, _run_safely도 실행 후 같은 키를 갱신한다 — 두 종류의 작업을 같은 리스트에서 다루더라도 상태를 헷갈리지 않게 구분하는 게 핵심이다. 그리고 _run_safely에서 보듯 한 작업의 예외가 다른 작업이나 스케줄러 전체를 멈추게 해서는 안 된다.

장 시간과 무관한 작업까지 같이 관리하기

자동매매에는 장중 작업(시세조회·주문)뿐 아니라 장외 작업(일일 정리, 로그 백업)도 섞인다. 스케줄러 구조에서는 이런 작업도 같은 틀 안에서 "몇 시에 실행"으로 등록하면 되므로, 장중·장외 작업을 별도 프로세스로 억지로 나눌 필요가 없다. 다만 장중 작업이 멈춰도 무인 운영 자체는 계속 감시돼야 하므로, 이 부분은 24시간 무인 운영과 장애 대응에서 다룬 감시 로직과 함께 붙는다.

자주 하는 실수

  1. 모든 작업을 하나의 스레드에서 순차 실행한다. 한 작업이 지연되면 뒤에 있는 작업 전체가 밀린다.
  2. 예외 처리를 스케줄러 레벨이 아니라 각 작업 안에서만 산발적으로 한다. 누락된 작업 하나가 전체를 죽일 수 있다.
  3. 서버 재시작 시 마지막 실행 시각을 기억하지 못한다. 재시작 직후 밀린 작업이 한꺼번에 몰려 실행되는 문제가 생긴다.

오늘의 정리

  1. 자동매매에서 주기가 다른 여러 작업을 하나의 while 반복문으로 관리하면 시간 계산이 금방 꼬인다.
  2. 스케줄러 구조는 "언제 실행할지" 판단과 "무엇을 실행할지"를 분리해, 작업이 늘어나도 구조가 단순하게 유지되게 한다.
  3. 개별 작업의 예외가 스케줄러 전체를 멈추지 않도록, 작업 단위로 예외를 잡고 알림 후 다음 주기를 기다리게 해야 한다.
  4. 장중·장외 작업을 같은 스케줄러 틀 안에서 관리할 수 있지만, 장애 감시는 별도 체계로 반드시 붙여야 한다.

이 글의 코드는 스케줄러 구조를 설명하기 위한 의사코드이며 특정 라이브러리나 실거래 시스템의 구현이 아닙니다. 이 글은 투자 조언이 아니며 특정 종목이나 매매기법을 추천하지 않습니다. 수익을 보장하지 않으며, 투자 손실에 대한 책임은 투자자 본인에게 있습니다.

← 전체 글 목록