1. 국내주식/1-1. 국내주식 연구일지

(파이썬) threading와 time 모듈로 특정 시간마다 반복 실행 (2) 클래스 내에서 실행하고 일정시간 경과 후 종료하기

봄이오네 2022. 10. 24. 08:16
반응형
목 차
1. 들어가며
2. 필요성
3. threading 등 개념 설명
1) threading.Timer 모듈
2) time 모듈 활용
4. 클래스 내에서 threading.Timer 실행하기/종료하기
5. 전체코드 및 결과
6. 마치며

1. 들어가며

지난 글에서는 파이썬 내장모듈인 threading 모듈과 time 모듈을 이용하여
특정시간마다 함수가 반복 실행되는 내용을 설명하였다.

(파이썬) threading와 time 모듈로 특정 시간마다 반복 실행

1. 들어가기 주식자동매매 프로그램으로 거래를 하다보면, 일정 시간마다 반복되는 함수를 구현할 필요가 있다. 내가 원하는 패턴에서 매수(진입)하여, 익절/손절 라인에 도달하면 칼같이 매도(

springcoming.tistory.com


이번 글에서는 클래스(class) 내 함수(메소드)가 일정시간마다 반복 및 종료되는 내용을 설명하겠다.


2. 필요성

일정 시간마다 반복되는 기능을 구현하기 위해서는
파이썬의 GIL(Global Interpreter Lock, 전역 인터프리터 락)을 우회할 필요가 있다.

GIL이 무엇인지는 여기서는 길게 설명할 수는 없겠지만,
파이썬(Python)은 하나의 쓰레드(A)가 실행하면,
다른 쓰레드(B)는 기존 쓰레드(A)가 종료될 때까지 실행이 안된다는 것이다.

이를 우회하기 위해
multi-threading, multi-processing, asyncio 등이 많이 언급된다.

필자가 생각하고 있는 시스템은
1분봉 패턴을 1분마다 파악한 후,
기존 설정한 패턴에 해당하면 매수(진입)하려고 한다.
(즉 1분봉 패턴을 수신받은 작업은 계속 돌아가야 한다는 뜻이다.)
그 이후 매수한 주식은 기존 설정한 각각의 손익 분기점에서 매도(청산)하려고 한다.

  • 1분봉 패턴 받는 것이 실행되는 쓰레드 → 쓰레드 A에서 실행 예정
  • 매수/매도가 실행되는 쓰레드 → 쓰레드 B에서 실행 예정

위에서 설명한대로 "1분봉 데이터" 받는 작업(A)이 끝난 이후,
매수/매도 작업(B)가 이루어질텐데

1분봉 데이터를 받는 작업이
장시작~장마감까지 계속 실행 상태(쓰레드를 계속 활용중)라면,
매수/매도를 위한 쓰레드 할당이 되지 않아, 매수/매도는 이루어지지 않는다.

쓰레드는 이렇게 1개 작업(A)이 계속 이루어지는 상태에서
다른 작업(B)가 이루어지는 기능을 해주기 때문에,
주식 시스템을 구성하고 있다면, 알고 있어야할 내용이다.


3. threading 등 개념 설명

1) threading.Timer 모듈

threading 모듈 중 Timer 기능을 활용한다.
아래 <코드>를 실행하면, 3초마다 메시지가 출력된다.

  • 1줄 : threading 모듈을 임포트한다.
  • 3줄 : 임의의 함수(bbb)를 선언한다.
  • 4줄 : bbb함수가 반복되는 것을 보여주기 위해, print에 메시지 입력
  • 5줄 : 3초마다 bbb함수가 실행되는 threading.Timer을 임의의 변수 tt1에 넣어준다.
  • 6줄 : 3초마다 bbb함수가 실행되는 tt1을 시작한다.
import threading

def bbb():
    print("3초마다 bbb함수 실행")
    tt1 = threading.Timer(3, bbb)
    tt1.start()
bbb()

# (expected result)
# 3초마다 bbb함수 실행
# 3초마다 bbb함수 실행
# 3초마다 bbb함수 실행

2) time 모듈 활용

반복되는 함수를 일정 시간 내 종료시키기 위해 time 모듈을 활용한다.
여기서 말하는 time 모듈은 UNIX시간을 활용한다.

  • time.time() : 현재시간을 나타낸다.
  • time.time() + 7 : 현재시간에서 7초를 더한다.

UNIX시간은 1970년1월1일 기준으로 경과한 시간을 숫자로 나타내는데,
어렵게 생각할 필요없다.
1666303805 → 2022-10-21 07:10:05초를 말한다. @.@
인터넷 검색을 하면 Unix타임 스패프 변환기를 통해,
쉽게 시간을 변경하자.
(UNIX시간은 다음에 설명한다.)

아래 <코드>를 설명하면,
max_time 변수를 "현재시간 + 7초"로 선언하였다.
7초가 경과하면 tt1 쓰레드는 종료되도록 설정하였다.

여기서 의문이 있다!
7초 내 함수는 "3초 bbb 실행"이 2번 나타나야 하는데, 왜 4번 출력될까?

아래 <코드>를 실행하면, 선언(def bbb)된 함수를

  • 맨 밑의 bbb()가 바로 실행한다. (print 1회)
  • 3초에 한번 실행 (print 2회) → 쓰레드 타이머 적용 (print 2회)
  • 3초에 한번 실행(print 3회) → 쓰레드 타이머 적용 (print 3회)
  • 7초가 다 될 무렵, 갑자기 print 4회가 출력되고 종료된다. (print 4회)

4회가 출력된 이유는 무엇일까?
아래 <코드>에서 현재시간이 7초가 경과하면, 종료하라고 했고,
threading.Timer를 3초마다 실행되고 있다.

최초 실행(bbb)하면 무조건 한번은 실행(1회)된다.
3초에 2회 실행되고, 6초에 3회를 실행되고,
아직 종료시간인 7초가 안되었기 때문에 한번 더 실행된다.(4회 실행)
그래서 종료까지 9초가 걸린다.

  • 최초 실행(print 1회) → 3초 후 실행(print 2회) → 6초 후 3회 실행(print 3회) → 1초가 남아서 실행(print 4회) → 종료


반복함수를 종료하려면 종료시간 설정을 해야하는데,
정확한 시간에 끝나지 않아서 아쉽긴 하다. ^^;

→ 시간 내 반복되는 횟수와 시간을 생각해보고 나서, 종료시간을 정해야,
사용자가 생각하는 시간 내 종료될 수 있다고 생각된다.

import threading
import time

max_time = time.time() + 7
start_time = time.time()

def bbb():
    print("3초마다 bbb함수 실행")
    tt1 = threading.Timer(3, bbb)
    tt1.start()
    if time.time() > max_time:
        tt1.cancel()
        end_time = time.time()
        print("7초가 경과하였으므로, threading.Timer 종료")
        print(f"종료를 위해 {end_time-start_time}이 소요되었습니다")
bbb()


# (expected result)
# 3초마다 bbb함수 실행
# 3초마다 bbb함수 실행
# 3초마다 bbb함수 실행
# 3초마다 bbb함수 실행
# 7초가 경과하였으므로, threading.Timer 종료
# 종료를 위해 9.024038791656494이 소요되었습니다

4. 클래스 내에서 threading.Timer 실행하기/종료하기

2개의 함수(메소드)를 가지고 있는 클래스이다.
여기서는 1개 함수를 실행(21줄 ccc함수)하면,
다른 1개 함수가 실행(12줄 bbb함수)가 실행되게 진행 예정이다.

그림1. 2개 함수를 가진 클래스가 threading.Timer을 통해 실행된다.


1줄 : sys은 파이썬의 함수 혹은 변수를 다루는 기능을 한다.
2줄 : GIL 우회 및 일정시간 반복을 위해 threading 모듈을 임포트한다.
3줄 : 동시성 처리를 위해 PyQt5.QtWidgets를 임포트한다.
4줄 : 종료시간(max_time) 설정을 위해 time 모듈을 임포트한다.

6줄 : aaa클래스를 선언한다.
7줄 : __init__(self)는 클래스가 실행되면, 무조건 실행된다.
8줄 : 종료시간을 11초로 설정한다.
9줄 : 종료시간이 UNIX 형태로 출력된다.

12줄 : bbb함수를 선언한다.
14~15줄 : threading.Timer를 3초로 설정한다.

20줄 : ccc함수를 선언한다.
22줄 : 이 글의 핵심이다.
ccc함수가 실행되면, 12줄의 bbb함수가 실행되게 하라는 내용이다.
23줄~24줄 : threading.Timer를 5초로 설정한다.
25줄 : 8초(11-3초)가 경과하면,
26줄 : threading.Timer를 종료하라.

29줄 : if __name__ == "__main__"는 아래 내용을 제일먼저 실행하라는 내용이다.
30줄 : 동시성 처리를 위해 QApplication을 app에 인스턴스화 해준다.
31줄 : 클래스aaa를 zzz에 인스턴스화 해준다.
(클래스도 변수 취급하여 사용하려고 함)
33줄 : 31줄에서 인스턴스된 aaa()클래스는 zzz로 변수화되었다.
20줄의 zzz.ccc() 함수를 실행하라.
35줄 : 앱(프로그램)을 실행하라(exec)


5. 전체코드 및 결과

import sys
import threading
from PyQt5.QtWidgets import *
import time

class aaa():
    def __init__(self):
        self.max_time = time.time() + (11)
        print(self.max_time)
        print("max_time은 11초로 설정")

    def bbb(self):
        print("3초마다 bbb함수 실행")
        tt1 = threading.Timer(3, self.bbb)
        tt1.start()
        if time.time() > self.max_time:
            tt1.cancel()
            print("11초가 경과하여 종료합니다.")

    def ccc(self):
        print("5초마다 ccc함수 실행")
        zzz.bbb()
        tt2= threading.Timer(5, self.ccc)
        tt2.start()
        if time.time() > self.max_time - 3:
            tt2.cancel()
            print("8초가 경과하여 종료합니다.")

if __name__ == "__main__":
    app = QApplication(sys.argv)
    zzz = aaa()

    zzz.ccc()

    app.exec_()


# (expected result)

# 1666308435.844187
# max_time은 11초로 설정
# 5초마다 ccc함수 실행
# 3초마다 bbb함수 실행
# 3초마다 bbb함수 실행
# 5초마다 ccc함수 실행
# 3초마다 bbb함수 실행
# 3초마다 bbb함수 실행
# 3초마다 bbb함수 실행
# 3초마다 bbb함수 실행
# 5초마다 ccc함수 실행
# 3초마다 bbb함수 실행
# 8초가 경과하여 종료합니다.
# 3초마다 bbb함수 실행
# 11초가 경과하여 종료합니다.
# 3초마다 bbb함수 실행
# 11초가 경과하여 종료합니다.
# 3초마다 bbb함수 실행
# 11초가 경과하여 종료합니다.

6. 마치며

지금까지 threading.Timer를 활용하여 일정시간 내 함수의 반복 및 종료를 알아보았다.
threading.Timer를 작동시키기 위해서는 한번은 무조건 실행해 주어야 한다.

반복되는 횟수/시간을 계산할 때, 최초 한번 실행되는 점은 분명 고려해야 한다.
또한 사용자가 당초 생각했던 시간보다, 조금 늦게 종료된다.
* 이유는 위에서 설명했듯이, 종료시간이 안되었으면 한번 더 실행된다)

위와 같은 한계가 있더라도,
클래스 내에서 threading.Timer 실행시키는 방법을 알게되니, 기분이 상당히 좋다.

동 내용은 향후 1분봉 패턴을 1분마다 받아서
패턴에 해당하면 매수하고, 일정 시간이 흐르면 매도하는 내용 후
threading.Timer가 종료되도록 하는 코드에 활용할 예정이다.

반응형