핫트레이딩

키움API ID와 비밀번호를 입력 후 "Running on http://0.0.0.0:5000" 메시지가 출력되지 않는 문제 본문

IT 정보/트레이딩뷰 자동매매

키움API ID와 비밀번호를 입력 후 "Running on http://0.0.0.0:5000" 메시지가 출력되지 않는 문제

HOTT 2025. 3. 13. 00:54

 

AWS EC2에서 TradingView와 키움증권 Open API를 연동한 자동매매 프로그램을 제작하는 과정에서, "서버 실행" 단계에서 키움증권 로그인 창이 나타난 후 ID와 비밀번호를 입력했음에도 "Running on http://0.0.0.0:5000" 메시지가 출력되지 않는 문제에 대해 상세히 분석하고 해결 방법을 설명하겠습니다. 이 문제는 주로 Flask와 PyQt5의 이벤트 루프 충돌, 코드 실행 흐름, 또는 키움 Open API의 로그인 처리 지연과 관련이 있습니다. 단계별로 원인을 파악하고 해결책을 제시하겠습니다.

문제 상황 재현

  • 상황: python webhook_server.py를 실행하면:
    1. 키움증권 로그인 창이 나타남.
    2. ID와 비밀번호를 입력해 로그인 성공.
    3. 하지만 CMD에 "Running on http://0.0.0.0:5000" 메시지가 출력되지 않고, 프로그램이 멈춘 듯 보임.
  • 코드: 이전에 제공한 Flask 웹훅 서버 코드 사용 가정:
    기존 파이션코드 "webhook_server.py"
from flask import Flask, request, jsonify
from PyQt5.QAxContainer import QAxWidget
from PyQt5.QtWidgets import QApplication
import sys
import logging

logging.basicConfig(filename='trade.log', level=logging.INFO, 
                    format='%(asctime)s - %(message)s')

app = Flask(__name__)

sys.argv.append("--remote-debugging-port=9222")
kiwoom_app = QApplication(sys.argv)
kiwoom = QAxWidget("KHOPENAPI.KHOpenAPICtrl.1")
kiwoom.dynamicCall("CommConnect()")  # 로그인 대기

def send_order(symbol, action, quantity, price):
    account = "YOUR_ACCOUNT_NUMBER"
    order_type = 1 if action == "buy" else 2
    kiwoom.dynamicCall("SendOrder(QString, QString, QString, int, QString, int, int, QString, QString)",
                       ["주문", "0101", account, order_type, symbol, quantity, price, "03", ""])
    logging.info(f"Order sent: {symbol}, {action}, {quantity}, {price}")

@app.route('/webhook', methods=['POST'])
def webhook():
    try:
        data = request.json
        if not data:
            return jsonify({"error": "No data received"}), 400
        symbol = data.get('symbol')
        action = data.get('action')
        price = int(float(data.get('price')))
        quantity = 1
        send_order(symbol, action, quantity, price)
        return jsonify({"message": f"Order sent: {symbol}, {action}, {price}"}), 200
    except Exception as e:
        logging.error(f"Error: {str(e)}")
        return jsonify({"error": str(e)}), 500

if __name__ == "__main__":
    app.run(host="0.0.0.0", port=5000, threaded=True)

 

 


가능한 원인

  1. PyQt5 이벤트 루프 차단:
    • kiwoom.dynamicCall("CommConnect()")는 키움 Open API의 로그인 창을 띄우고, 로그인 완료까지 메인 스레드를 차단합니다.
    • Flask의 app.run()은 그 이후에 실행되지만, PyQt5의 이벤트 루프(QApplication)와 Flask의 서버 루프가 충돌하거나 순서가 꼬여 진행이 멈춤.
  2. 로그인 상태 확인 누락:
    • 키움 API는 로그인 완료 여부를 확인하는 이벤트(OnEventConnect)를 제공하는데, 이를 처리하지 않아 코드가 대기 상태로 남음.
  3. Flask 서버 실행 지연:
    • threaded=True를 사용했지만, PyQt5와의 상호작용으로 인해 Flask 서버가 제대로 시작되지 않음.
  4. 예외 발생:
    • 코드 내 예외(예: 네트워크 문제, 키움 API 오류)가 발생했으나, CMD에 출력되지 않고 로그 파일에만 기록됨.

해결 방법 (단계별)

1. 로그인 완료 확인 추가

키움 API의 로그인 상태를 비동기적으로 확인하도록 수정합니다. OnEventConnect 이벤트를 처리해 로그인 완료 후 Flask 서버를 시작하도록 합니다.

  • 수정된 코드
    아래 python 코드를 EC2에서 메모장에 붙여넣기 후 기존 파일과 구분하여 저장가능 예) C:\webhook_server_1.py

    from flask import Flask, request, jsonify
    from PyQt5.QAxContainer import QAxWidget
    from PyQt5.QtWidgets import QApplication
    from PyQt5.QtCore import QEventLoop
    import sys
    import logging

    logging.basicConfig(filename='trade.log', level=logging.INFO, 
                        format='%(asctime)s - %(message)s')

    app = Flask(__name__)

    class Kiwoom:
        def __init__(self):
            self.app = QApplication(sys.argv)
            self.kiwoom = QAxWidget("KHOPENAPI.KHOpenAPICtrl.1")
            self.logged_in = False
            self.kiwoom.OnEventConnect.connect(self.on_login)

        def login(self):
            self.kiwoom.dynamicCall("CommConnect()")
            loop = QEventLoop()
            while not self.logged_in:
                loop.exec_()  # 로그인 완료까지 대기

        def on_login(self, err_code):
            if err_code == 0:
                logging.info("Login successful")
                self.logged_in = True
            else:
                logging.error(f"Login failed with error code: {err_code}")
                sys.exit(1)

        def send_order(self, symbol, action, quantity, price):
            account = "YOUR_ACCOUNT_NUMBER"  # 본인 계좌번호 변경입력
            order_type = 1 if action == "buy" else 2
            self.kiwoom.dynamicCall("SendOrder(QString, QString, QString, int, QString, int, int, QString, QString)",
                                    ["주문", "0101", account, order_type, symbol, quantity, price, "03", ""])
            logging.info(f"Order sent: {symbol}, {action}, {quantity}, {price}")

    kiwoom = Kiwoom()

    @app.route('/webhook', methods=['POST'])
    def webhook():
        try:
            data = request.json
            if not data:
                return jsonify({"error": "No data received"}), 400
            symbol = data.get('symbol')
            action = data.get('action')
            price = int(float(data.get('price')))
            quantity = 1
            kiwoom.send_order(symbol, action, quantity, price)
            return jsonify({"message": f"Order sent: {symbol}, {action}, {price}"}), 200
        except Exception as e:
            logging.error(f"Error: {str(e)}")
            return jsonify({"error": str(e)}), 500

    if __name__ == "__main__":
        kiwoom.login()  # 로그인 완료까지 대기
        logging.info("Starting Flask server")
        app.run(host="0.0.0.0", port=5000, threaded=True)


     
  • 변경 내용:
    • Kiwoom 클래스로 로그인 로직 분리.
    • OnEventConnect 이벤트 핸들러 추가.
    • QEventLoop로 로그인 완료 대기.
    • 로그인 성공 후 Flask 서버 시작.
  • 실행:
    • CMD에서 python webhook_server.py 실행.
    • 로그인 창에서 ID/비밀번호 입력 후, "Starting Flask server" 로그와 "Running on http://0.0.0.0:5000" 메시지 확인.

2. Flask와 PyQt5 분리 (스레드 사용)

PyQt5와 Flask의 이벤트 루프 충돌을 피하기 위해, Flask 서버를 별도 스레드에서 실행합니다.

  • 수정된 코드:
    아래 python 코드를 EC2에서 메모장에 붙여넣기 후 기존 파일과 구분하여 저장가능 예) C:\webhook_server_2.py
     
    from flask import Flask, request, jsonify
    from PyQt5.QAxContainer import QAxWidget
    from PyQt5.QtWidgets import QApplication
    from PyQt5.QtCore import QEventLoop
    import sys
    import logging
    import threading

    logging.basicConfig(filename='trade.log', level=logging.INFO, 
                        format='%(asctime)s - %(message)s')

    app = Flask(__name__)

    class Kiwoom:
        def __init__(self):
            self.app = QApplication(sys.argv)
            self.kiwoom = QAxWidget("KHOPENAPI.KHOpenAPICtrl.1")
            self.logged_in = False
            self.kiwoom.OnEventConnect.connect(self.on_login)

        def login(self):
            self.kiwoom.dynamicCall("CommConnect()")
            loop = QEventLoop()
            while not self.logged_in:
                loop.exec_()

        def on_login(self, err_code):
            if err_code == 0:
                logging.info("Login successful")
                self.logged_in = True
            else:
                logging.error(f"Login failed with error code: {err_code}")
                sys.exit(1)

        def send_order(self, symbol, action, quantity, price):
            account = "YOUR_ACCOUNT_NUMBER"  # 본인 계좌번호 입력

            order_type = 1 if action == "buy" else 2
            self.kiwoom.dynamicCall("SendOrder(QString, QString, QString, int, QString, int, int, QString, QString)",
                                    ["주문", "0101", account, order_type, symbol, quantity, price, "03", ""])
            logging.info(f"Order sent: {symbol}, {action}, {quantity}, {price}")

    kiwoom = Kiwoom()

    @app.route('/webhook', methods=['POST'])
    def webhook():
        try:
            data = request.json
            if not data:
                return jsonify({"error": "No data received"}), 400
            symbol = data.get('symbol')
            action = data.get('action')
            price = int(float(data.get('price')))
            quantity = 1
            kiwoom.send_order(symbol, action, quantity, price)
            return jsonify({"message": f"Order sent: {symbol}, {action}, {price}"}), 200
        except Exception as e:
            logging.error(f"Error: {str(e)}")
            return jsonify({"error": str(e)}), 500

    def run_flask():
        app.run(host="0.0.0.0", port=5000, threaded=True)

    if __name__ == "__main__":
        kiwoom.login()  # 로그인 완료 대기
        flask_thread = threading.Thread(target=run_flask)
        flask_thread.start()
        logging.info("Flask server started in separate thread")
        kiwoom.app.exec_()  # PyQt5 이벤트 루프 실행
     
     
  • 변경 내용:
    • Flask 서버를 threading.Thread로 별도 스레드에서 실행.
    • PyQt5 이벤트 루프(app.exec_())를 메인 스레드에서 유지.
  • 실행:
    • 로그인 후 "Flask server started in separate thread" 로그와 "Running on http://0.0.0.0:5000" 메시지 확인.

3. 디버깅 및 로그 확인

  • 로그 확인:
    • C:\trade.log 파일 열기:
      • 로그인 성공: "Login successful".
      • 오류: "Login failed with error code: X".
    • 오류 코드 예:
      • -100: ID/비밀번호 오류.
      • -101: 서버 연결 실패.
  • CMD 출력 활성화:
    • 콘솔에도 로그 출력 추가:
      python 코드
       
      logging.getLogger().addHandler(logging.StreamHandler())

4. 추가 문제 해결

  1. 네트워크 문제:
    • EC2 보안 그룹에서 포트 5000이 열려 있는지 확인.
    • CMD에서 netstat -an | find "5000"으로 포트 사용 여부 점검.
  2. 키움 API 설치 문제:
    • 키움 Open API가 제대로 설치되지 않았으면 재설치:
      • 키움증권 홈페이지 > OpenAPISetup.exe 다운로드 및 실행.
  3. Python 버전:
    • 32비트 Python 사용 확인:
      text 명령어
       
      python -c "import platform; print(platform.architecture())"

최종 확인

  • 수정된 코드로 python webhook_server.py 실행.
  • 로그인 성공 후 CMD에 "Running on http://0.0.0.0:5000" 출력 확인.
  • 브라우저에서 http://<EC2-퍼블릭-IP>:5000/webhook 접속 시 405 에러(Method Not Allowed) 뜨면 정상 (POST만 허용).

요약

문제의 핵심은 PyQt5와 Flask의 이벤트 루프 충돌입니다. 해결책은:

  1. 로그인 완료를 이벤트로 확인 (OnEventConnect).
  2. Flask를 별도 스레드로 분리. 로그를 통해 디버깅하며 진행하세요.
반응형