파이썬 스케줄러로 자동매매 돌리기 — 무한루프 대신 스케줄러를 쓰는 이유
while True로 시작했다가 겪은 문제
자동매매 코드를 처음 짤 때는 while True: 안에 time.sleep()을 넣는 방식으로 충분해 보인다. 실제로 초반에는 이렇게 돌렸다. 문제는 실행할 작업이 늘어나면서 드러났다. 시세 조회는 짧은 주기로, 잔고 점검은 그보다 긴 주기로, 장 마감 후 정리는 하루에 한 번 — 주기가 다른 작업이 한 반복문 안에 섞이자 시간 계산이 점점 꼬였다.
이 글은 특정 스케줄러 라이브러리의 API를 나열하지 않는다. 대신 "왜 반복문 대신 스케줄러 구조로 옮겨야 하는가"와, 최소한의 스케줄러 구조를 의사코드로 정리한다.
반복문 방식의 한계
- 주기가 다른 작업을 섞으면 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시간 무인 운영과 장애 대응에서 다룬 감시 로직과 함께 붙는다.
자주 하는 실수
- 모든 작업을 하나의 스레드에서 순차 실행한다. 한 작업이 지연되면 뒤에 있는 작업 전체가 밀린다.
- 예외 처리를 스케줄러 레벨이 아니라 각 작업 안에서만 산발적으로 한다. 누락된 작업 하나가 전체를 죽일 수 있다.
- 서버 재시작 시 마지막 실행 시각을 기억하지 못한다. 재시작 직후 밀린 작업이 한꺼번에 몰려 실행되는 문제가 생긴다.
오늘의 정리
- 자동매매에서 주기가 다른 여러 작업을 하나의 while 반복문으로 관리하면 시간 계산이 금방 꼬인다.
- 스케줄러 구조는 "언제 실행할지" 판단과 "무엇을 실행할지"를 분리해, 작업이 늘어나도 구조가 단순하게 유지되게 한다.
- 개별 작업의 예외가 스케줄러 전체를 멈추지 않도록, 작업 단위로 예외를 잡고 알림 후 다음 주기를 기다리게 해야 한다.
- 장중·장외 작업을 같은 스케줄러 틀 안에서 관리할 수 있지만, 장애 감시는 별도 체계로 반드시 붙여야 한다.
이 글의 코드는 스케줄러 구조를 설명하기 위한 의사코드이며 특정 라이브러리나 실거래 시스템의 구현이 아닙니다. 이 글은 투자 조언이 아니며 특정 종목이나 매매기법을 추천하지 않습니다. 수익을 보장하지 않으며, 투자 손실에 대한 책임은 투자자 본인에게 있습니다.