왜 미국 주식 자동매매가 좋은가요?

🎯 1. 시장이 24시간 돌아가진 않지만, 야간 거래 가능

미국 주식 매매 장점은 야간 거래가 가능하다는 점입니다.

저처럼 회사에 다니면서 야간에 시스템 트레이딩을 공부하는 사람은 야간에는 테스트가 힘듭니다.

NXT 가 가능한 시간이 있기는 하지만 퇴근 하고 저녁 식사를 하고 나면 대부분 해당 시간 이후 입니다.

정말 하루 휴가를 내지 않으면 테스트가 불가능합니다. TOT

오~~ 그런데 미국 주식은 야간에도 가능합니다. 

구분 국내(KRX) 국내 NXT 미국 주식 (서머타임 적용) 미국 주식 (서머타임 미적용)
프리마켓 08:30~08:40 (종가매매) 08:00~08:50 (프리마켓) 17:00~22:30 18:00~23:30
정규장 09:00~15:30 09:00~15:20 (메인마켓) 22:30~05:00 (다음날) 23:30~06:00 (다음날)
애프터마켓 15:40~16:00 (종가매매) 15:30~20:00 (애프터마켓) 05:00~07:00 (다음날, 1차)
07:00~09:00 (2차)
06:00~07:00 (다음날, 1차)
07:00~09:00 (2차)
단일가매매 16:00~18:00 없음 없음 없음
총 거래 가능 시간 약 6.5시간 12시간 최대 16시간(프리+정규+애프터) 최대 16시간(프리+정규+애프터)
 

✔️ 자동매매 시스템을 퇴근 후에도 작동하게 만들면 "자고 있는 동안 돈이 일한다"는 진짜 자동 수익 모델 가능!


🌊 2. 변동성이 크고 기회가 많다

테슬라, 엔비디아 같은 미국 종목은 하루에 5~10%도 쉽게 움직입니다.
즉, 간단한 자동전략으로도 먹을 게 많아요.

✔️ "코스피야 눈치게임, 나스닥은 롤러코스터!"
변동성이 있어야 자동 전략도 수익을 낼 확률이 높습니다.


💵 3. 환율 상승도 추가 수익!

주가도 오르고 환율도 오르면?
👉 두 배로 이익! 반대로 손해도 두 배니까 리스크 관리 중요.


🤖 Step-by-Step 코드 설명 (미국 주식 자동매매 시스템)


🧱 Step 1: 프로그램 시작 + 기본 설정

self.kiwoom = Kiwoom()
self.kiwoom.CommConnect(block=True)
self.account = self.kiwoom.GetLoginInfo("ACCNO")[0]

키움 API에 연결하고 로그인 계좌를 불러옵니다. 미국 주식은 해외주식 거래 가능한 계좌여야 해요.


📊 Step 2: 엑셀에서 종목, 매수가, 매도가 불러오기

def read_excel(self):
    return pd.read_excel("stocks.xlsx")
excel
종목명 | 매수가 | 매도가  
------|--------|--------  
AAPL  | 180    | 200  
TSLA  | 250    | 300

📂 엑셀 파일로 전략 관리 가능 = 비전문가도 쉽게 전략 수정 가능!


🔍 Step 3: 현재가 조회 (미국 주식용)

def get_current_price(self, code):
    data = self.kiwoom.block_request("opt40001", 종목코드=code, output="해외주식기본정보", next=0)
    price = data.get("현재가", "0")
    return int(str(price).replace(",", "").split('.')[0])

미국 주식은 opt40001이라는 TR을 써야 시세 조회가 됩니다.
일반 국내주식 TR(opt10001)은 사용할 수 없어요.


🤝 Step 4: 조건 체크 → 자동 매수/매도

if current <= buy_price and name not in self.bought_set:
    self.kiwoom.SendOrder("미국주식매수", "1001", self.account, 1, code, qty, 0, "03", "")
elif current >= sell_price and name not in self.sold_set:
    self.kiwoom.SendOrder("미국주식매도", "1002", self.account, 2, code, qty, 0, "03", "")
 

📥 매수가 이하면 매수, 📤 매도가 이상이면 매도
매수/매도는 **시장가("03")**로 실행해서 빠르게 체결시킵니다.
self.bought_set은 중복 주문 방지용입니다.


📦 Step 5: 현재 보유 종목 업데이트

self.kiwoom.block_request("opw00018", 계좌번호=..., 비밀번호=..., ...)

이 부분은 아직 국내 주식 기준입니다.
미국 주식은 opw00005나 opw00004와 같은 별도 해외계좌 평가잔고 조회 TR을 사용해야 할 수도 있어요.
추후 확장 포인트!

 

✅ 전체 코드 (미국 주식용)

import sys
import time
import pandas as pd
from datetime import datetime
from PyQt5.QtWidgets import *
from PyQt5.QtCore import QTimer
from pykiwoom.kiwoom import Kiwoom

EXCEL_PATH = "stocks.xlsx"
LOG_FILE = "trade_log.txt"
INTERVAL_MS = 30000  # 30초 주기

class TradingBot(QMainWindow):
    def __init__(self):
        super().__init__()
        self.setWindowTitle("📈 미국주식 자동 매매 시스템")
        self.setGeometry(300, 100, 900, 700)

        self.kiwoom = Kiwoom()
        self.kiwoom.CommConnect(block=True)
        self.account = self.kiwoom.GetLoginInfo("ACCNO")[0]
        self.account_pw = ""

        self.bought_set = set()
        self.sold_set = set()

        self.init_ui()
        self.get_password()
        self.update_holdings()
        self.log("미국주식 자동매매 시작 ✅")

        self.timer = QTimer()
        self.timer.timeout.connect(self.run_trading)
        self.timer.start(INTERVAL_MS)

    def init_ui(self):
        self.text_log = QTextEdit()
        self.text_log.setReadOnly(True)

        self.btn_manual = QPushButton("🔁 수동 실행")
        self.btn_manual.clicked.connect(self.run_trading)

        self.status_label = QLabel("⏱️ 대기 중...")

        self.table = QTableWidget()
        self.table.setColumnCount(4)
        self.table.setHorizontalHeaderLabels(["종목명", "수량", "매입가", "현재가"])
        self.table.horizontalHeader().setStretchLastSection(True)

        layout = QVBoxLayout()
        layout.addWidget(self.text_log)
        layout.addWidget(self.status_label)
        layout.addWidget(self.btn_manual)
        layout.addWidget(QLabel("\n📦 현재 보유 종목"))
        layout.addWidget(self.table)

        central = QWidget()
        central.setLayout(layout)
        self.setCentralWidget(central)

    def get_password(self):
        pw, ok = QInputDialog.getText(self, "계좌 비밀번호 입력", "계좌 비밀번호를 입력하세요:", QLineEdit.Password)
        if ok and pw:
            self.account_pw = pw
            self.log("🔐 계좌 비밀번호가 등록되었습니다.")
        else:
            QMessageBox.critical(self, "오류", "비밀번호가 입력되지 않았습니다. 프로그램을 종료합니다.")
            sys.exit(1)

    def log(self, message):
        now = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
        log_msg = f"[{now}] {message}"
        self.text_log.append(log_msg)
        with open(LOG_FILE, "a", encoding="utf-8") as f:
            f.write(log_msg + "\n")

    def read_excel(self):
        return pd.read_excel(EXCEL_PATH)

    def name_to_code(self, name):
        # 미국 주식은 'A' 접두사를 붙이는 경우가 많습니다
        return f"A{name.strip().upper()}"

    def get_current_price(self, code):
        try:
            data = self.kiwoom.block_request(
                "opt40001",
                종목코드=code,
                output="해외주식기본정보",
                next=0
            )
            price = data.get("현재가", "0")
            return int(str(price).replace(",", "").split('.')[0])
        except Exception as e:
            self.log(f"[현재가 조회 실패] {code} : {e}")
            return 0

    def run_trading(self):
        self.status_label.setText("🔍 주가 확인 중...")
        df = self.read_excel()

        for _, row in df.iterrows():
            name = row['종목명']
            buy_price = int(row['매수가'])
            sell_price = int(row['매도가'])
            code = self.name_to_code(name)

            current = self.get_current_price(code)
            self.log(f"{name} 현재가: {current} / 매수: {buy_price} / 매도: {sell_price}")

            qty = 1  # 테스트용 수량

            if current <= buy_price and name not in self.bought_set:
                self.log(f"📥 매수 주문: {name} @ {current}")
                self.kiwoom.SendOrder("미국주식매수", "1001", self.account, 1, code, qty, 0, "03", "")
                self.bought_set.add(name)
                self.sold_set.discard(name)

            elif current >= sell_price and name not in self.sold_set:
                self.log(f"📤 매도 주문: {name} @ {current}")
                self.kiwoom.SendOrder("미국주식매도", "1002", self.account, 2, code, qty, 0, "03", "")
                self.sold_set.add(name)
                self.bought_set.discard(name)

        self.update_holdings()
        self.status_label.setText("⏱️ 대기 중...")

    def update_holdings(self):
        try:
            data = self.kiwoom.block_request("opw00018",
                                             계좌번호=self.account,
                                             비밀번호=self.account_pw,
                                             비밀번호입력매체구분="00",
                                             조회구분=2,
                                             output="계좌평가잔고개별합산",
                                             next=0)

            if not isinstance(data, dict) or '종목번호' not in data:
                self.log("[경고] 보유 종목 정보가 없습니다.")
                self.table.setRowCount(0)
                return

            codes = data['종목번호']
            names = data['종목명']
            qtys = data['보유수량']
            prices = data['매입가']

            self.table.setRowCount(len(codes))
            for i in range(len(codes)):
                name = names[i].strip()
                qty = int(qtys[i]) if str(qtys[i]).strip().isdigit() else 0
                buy_price = int(prices[i]) if str(prices[i]).strip().isdigit() else 0
                cur_price = self.get_current_price(codes[i].strip())

                self.table.setItem(i, 0, QTableWidgetItem(name))
                self.table.setItem(i, 1, QTableWidgetItem(str(qty)))
                self.table.setItem(i, 2, QTableWidgetItem(str(buy_price)))
                self.table.setItem(i, 3, QTableWidgetItem(str(cur_price)))
        except Exception as e:
            self.log(f"[보유 종목 조회 오류] {str(e)}")

if __name__ == "__main__":
    app = QApplication(sys.argv)
    bot = TradingBot()
    bot.show()
    sys.exit(app.exec_())

🔚 마무리 - 미국 주식 자동매매, 지금 시작할 이유

국내 주식미국 주식
낮에만 거래 퇴근 후 거래 가능
하루 수익 적음 하루 5~10% 기회
코스피 지수에 민감 다양한 종목 개별성장
환율효과 없음 환차익도 가능

 

Posted by 제이브레인
,

📖 "말만 하지 말고 코인 트레이딩 해봅시다"

어느 날, 평범한 직장인 '엑셀'은 매일 시세만 들여다보며 이렇게 중얼거렸다.

“왜 내가 사면 떨어지고, 팔면 오르는 거지…?”

그날도 출근길에 코인 차트를 보던 엑셀은 문득 이런 생각을 하게 된다.

“내가 사는 가격, 파는 가격을 엑셀에 적어두면, 파이썬이 대신 사고팔아주면 안 되나?”

엑셀은 곧장 집으로 달려가 노트북을 켰다. 그리고 이렇게 외쳤다.

“그래, 자동 매매를 만드는 거야! 내가 주인공인 시스템 트레이딩의 시작이야!”


🧱 Step 1. 엑셀로 트레이딩 전략 정리

엑셀 파일 이름: coin_trading.xlsx

코인명매수가매도가
BTC 70000000 73000000
ETH 4000000 4200000
 

💡 엑셀에 사고 싶은 가격(매수가)와 팔고 싶은 가격(매도가)을 적기만 하면 된다!


🔌 Step 2. 업비트 API 연동

업비트는 모의투자를 공식적으로 제공하지 않지만, 실제 API 키로 소액으로 테스트하거나 백테스트로 전략을 시뮬레이션할 수 있습니다.

먼저 pyupbit 설치:


pip install pyupbit openpyxl

🤖 Step 3. 파이썬 자동 매매 코드

import pyupbit
import time
import pandas as pd

# 업비트 API 키 입력
access_key = "YOUR_ACCESS_KEY"
secret_key = "YOUR_SECRET_KEY"
upbit = pyupbit.Upbit(access_key, secret_key)

# 엑셀에서 매매 전략 불러오기
df = pd.read_excel("coin_trading.xlsx")

def get_current_price(ticker):
    try:
        return pyupbit.get_current_price(ticker)
    except Exception as e:
        print(f"가격 조회 실패: {e}")
        return None

for index, row in df.iterrows():
    coin_name = row['코인명']
    buy_price = row['매수가']
    sell_price = row['매도가']

    ticker = f"KRW-{coin_name}"
    current_price = get_current_price(ticker)

    if current_price is None:
        continue

    print(f"[{coin_name}] 현재가: {current_price}, 목표 매수가: {buy_price}, 목표 매도가: {sell_price}")

    # 매수 조건
    if current_price <= buy_price:
        print(f"📥 {coin_name} 매수 시도 중...")
        try:
            krw_balance = upbit.get_balance("KRW")
            if krw_balance >= 5000:
                order = upbit.buy_market_order(ticker, 5000)  # 5000원 매수
                print(f"✅ 매수 완료: {order}")
            else:
                print("❌ 잔고 부족")
        except Exception as e:
            print(f"매수 실패: {e}")

    # 매도 조건
    elif current_price >= sell_price:
        print(f"📤 {coin_name} 매도 시도 중...")
        try:
            coin_balance = upbit.get_balance(ticker)
            if coin_balance and coin_balance > 0:
                order = upbit.sell_market_order(ticker, coin_balance)
                print(f"✅ 매도 완료: {order}")
            else:
                print("❌ 보유 수량 없음")
        except Exception as e:
            print(f"매도 실패: {e}")

    time.sleep(1)  # 너무 자주 호출하면 제한 걸림
 
 

🛡️ 안전을 위한 팁

  • ✅ 실 계정 전에 백테스트 필수!
  • ✅ 금액 제한 (5000원, coin_balance) 등 실수 방지 코드 필수!
  • ✅ 반복 실행 시 while True: 루프와 스케줄러로 자동화 가능

🎯 결론: 당신도 엑셀이 될 수 있다

이제 당신은 ‘차트를 바라보며 한숨 쉬던 엑셀’이 아닌,
**“엑셀에 가격만 적어도 자동으로 움직이는 트레이딩 시스템의 주인공”**입니다.


🤩 다음 단계 아이디어

  • 실시간 감시 GUI (PyQt 연동)
  • 전략 다양화 (틱 단위 분할 매수, 이동평균 돌파 등)
  • 백테스트 시스템 결합
  • 로그 기록 자동 저장
Posted by 제이브레인
,

🧱 1단계 – 콘솔에서 혼자 거래를 시작합니다

처음에는 파이썬으로 주식 매매 프로그램을 간단히 만들었습니다.
엑셀 파일에 종목, 매수가, 매도가를 기록하고, 매매 조건에 따라 콘솔에 매수/매도 메시지를 출력합니다.

🔹 사용한 코드:

            current_price = get_current_price(kiwoom, code)
            print(f"[{name}] 현재가: {current_price} | 매수: {buy_price} | 매도: {sell_price}")

            qty = 10  # 10주 매매 예시

            if current_price <= buy_price and name not in bought:
                print(f"[매수] {name} @ {current_price}")
                kiwoom.SendOrder("매수", "1001", account, 1, code, qty, 0, "03", "")
                bought.add(name)
                sold.discard(name)

            elif current_price >= sell_price and name not in sold:
                print(f"[매도] {name} @ {current_price}")
                kiwoom.SendOrder("매도", "1002", account, 2, code, qty, 0, "03", "")
                sold.add(name)
                bought.discard(name)

하지만 문제는 매매 내역만 볼 수 있고, 현재 어떤 종목을 가지고 있는지 확인하기가 어렵다는 점입니다.
보유 종목 현황이 없어 불편함을 느낍니다.


💡 2단계 – GUI로 보유 주식을 눈으로 확인하고자 합니다

콘솔 출력만으로는 부족하다는 것을 깨닫고, GUI를 통해 보유 주식을 시각적으로 보여주자는 결심을 합니다.
PyQt를 이용하여 간단한 창과 표를 만드는 것부터 시작합니다.


🪟 3단계 – PyQt5로 테이블 GUI를 구성합니다

PyQt5를 사용하여 종목명, 수량, 평균 매수가를 보여주는 테이블을 생성합니다.

🔹 PyQt 기본 구조:

import sys
import time
import pandas as pd
from datetime import datetime
from PyQt5.QtWidgets import *
from PyQt5.QtCore import QTimer
from pykiwoom.kiwoom import Kiwoom

EXCEL_PATH = "stocks.xlsx"
LOG_FILE = "trade_log.txt"
INTERVAL_MS = 30000  # 30초 주기

class TradingBot(QMainWindow):
    def __init__(self):
        super().__init__()
        self.setWindowTitle("\U0001F4C8 Excel 기반 자동 매매 시스템")
        self.setGeometry(300, 100, 900, 700)

        self.kiwoom = Kiwoom()
        self.kiwoom.CommConnect(block=True)
        self.account = self.kiwoom.GetLoginInfo("ACCNO")[0]
        self.account_pw = ""

        self.bought_set = set()
        self.sold_set = set()

        self.init_ui()
        self.get_password()
        self.update_holdings()
        self.log("시스템 트레이딩 시작됨 ✅")

        self.timer = QTimer()
        self.timer.timeout.connect(self.run_trading)
        self.timer.start(INTERVAL_MS)

    def init_ui(self):
        self.text_log = QTextEdit()
        self.text_log.setReadOnly(True)

        self.btn_manual = QPushButton("\U0001F501 수동 실행")
        self.btn_manual.clicked.connect(self.run_trading)

        self.status_label = QLabel("⏱️ 대기 중...")

        self.table = QTableWidget()
        self.table.setColumnCount(4)
        self.table.setHorizontalHeaderLabels(["종목명", "수량", "매입가", "현재가"])
        self.table.horizontalHeader().setStretchLastSection(True)

        layout = QVBoxLayout()
        layout.addWidget(self.text_log)
        layout.addWidget(self.status_label)
        layout.addWidget(self.btn_manual)
        layout.addWidget(QLabel("\n📦 현재 보유 종목"))
        layout.addWidget(self.table)

        central = QWidget()
        central.setLayout(layout)
        self.setCentralWidget(central)

    def get_password(self):
        pw, ok = QInputDialog.getText(self, "계좌 비밀번호 입력", "계좌 비밀번호를 입력하세요:", QLineEdit.Password)
        if ok and pw:
            self.account_pw = pw
            self.log("🔐 계좌 비밀번호가 등록되었습니다.")
        else:
            QMessageBox.critical(self, "오류", "비밀번호가 입력되지 않았습니다. 프로그램을 종료합니다.")
            sys.exit(1)

    def log(self, message):
        now = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
        log_msg = f"[{now}] {message}"
        self.text_log.append(log_msg)
        with open(LOG_FILE, "a", encoding="utf-8") as f:
            f.write(log_msg + "\n")

    def read_excel(self):
        return pd.read_excel(EXCEL_PATH)

    def name_to_code(self, name):
        codes = self.kiwoom.GetCodeListByMarket('0') + self.kiwoom.GetCodeListByMarket('10')
        for code in codes:
            if self.kiwoom.GetMasterCodeName(code) == name:
                return code
        return None

    def get_current_price(self, code):
        price = self.kiwoom.GetMasterLastPrice(code)
        if isinstance(price, str):
            return int(price.replace(",", ""))
        elif isinstance(price, int):
            return price
        else:
            return 0

    def run_trading(self):
        self.status_label.setText("🔍 주가 확인 중...")
        df = self.read_excel()

        for _, row in df.iterrows():
            name = row['종목명']
            buy_price = int(row['매수가'])
            sell_price = int(row['매도가'])
            code = self.name_to_code(name)

            if not code:
                self.log(f"[오류] 종목명 {name} → 코드 변환 실패")
                continue

            current = self.get_current_price(code)
            self.log(f"{name} 현재가: {current} / 매수: {buy_price} / 매도: {sell_price}")

            qty = 10

            if current <= buy_price and name not in self.bought_set:
                self.log(f"📥 매수 주문: {name} @ {current}")
                self.kiwoom.SendOrder("매수", "1001", self.account, 1, code, qty, 0, "03", "")
                self.bought_set.add(name)
                self.sold_set.discard(name)

            elif current >= sell_price and name not in self.sold_set:
                self.log(f"📤 매도 주문: {name} @ {current}")
                self.kiwoom.SendOrder("매도", "1002", self.account, 2, code, qty, 0, "03", "")
                self.sold_set.add(name)
                self.bought_set.discard(name)

        self.update_holdings()
        self.status_label.setText("⏱️ 대기 중...")

    def update_holdings(self):
        try:
            data = self.kiwoom.block_request("opw00018",
                                             계좌번호=self.account,
                                             비밀번호=self.account_pw,
                                             비밀번호입력매체구분="00",
                                             조회구분=2,
                                             output="계좌평가잔고개별합산",
                                             next=0)

            self.log(f"[디버그] 계좌번호: {self.account}")
            self.log(f"[디버그] 비밀번호: {'입력됨' if self.account_pw else '미입력'}")
            self.log(f"[디버그] opw00018 응답 타입: {type(data)}")
            self.log(f"[디버그] opw00018 응답: {data}")

            if not isinstance(data, dict) or '종목번호' not in data:
                self.log("[경고] 보유 종목 정보가 없습니다.")
                self.table.setRowCount(0)
                return

            codes = data['종목번호']
            names = data['종목명']
            qtys = data['보유수량']
            prices = data['매입가']

            self.table.setRowCount(len(codes))
            for i in range(len(codes)):
                name = names[i].strip()
                qty = int(qtys[i]) if str(qtys[i]).strip().isdigit() else 0
                buy_price = int(prices[i]) if str(prices[i]).strip().isdigit() else 0
                cur_price = self.get_current_price(codes[i].strip())

                self.table.setItem(i, 0, QTableWidgetItem(name))
                self.table.setItem(i, 1, QTableWidgetItem(str(qty)))
                self.table.setItem(i, 2, QTableWidgetItem(str(buy_price)))
                self.table.setItem(i, 3, QTableWidgetItem(str(cur_price)))
        except Exception as e:
            self.log(f"[보유 종목 조회 오류] {str(e)}")

if __name__ == "__main__":
    app = QApplication(sys.argv)
    bot = TradingBot()
    bot.show()
    sys.exit(app.exec_())

이제 콘솔이 아닌 GUI 창을 통해 보유 종목을 확인할 수 있게 됩니다.
한눈에 보기 편하고, 시각적으로도 깔끔해서 훨씬 관리가 쉬워집니다.


🏁 4단계 – GUI를 통해 실전 트레이딩을 관리합니다

PyQt로 보유 현황을 시각화한 후, 프로그램의 실용성이 크게 향상되었습니다.
더 이상 콘솔 메시지를 뒤져볼 필요 없이, 어떤 종목을 몇 주나 가지고 있는지, 평단가는 얼마인지 한눈에 확인할 수 있습니다.
트레이딩 과정이 체계적으로 정리되어 개발자이자 트레이더로서의 만족도가 매우 높아졌습니다.


✅ 정리

구분 개선 전 개선 후
데이터 확인 콘솔 로그 GUI 테이블
시각화 없음 실시간 보유 종목 표시
사용성 불편 직관적이고 명확함
 

GUI를 도입한 이 작은 변화는 프로그램의 품질을 한 단계 끌어올렸습니다.
이제 이 주식 트레이더는 콘솔 속에서 혼잣말을 하던 시절을 뒤로하고,
PyQt라는 창을 통해 매매를 직관적이고 명확하게 관리하고 있습니다.

Posted by 제이브레인
,

📘 AI는 너무 멀고, 엑셀은 가깝다 – 파이썬으로 자동 주식 매매하기


🧠 Chapter 1. 인공지능 시스템 트레이딩은… 멋지지만

"딥러닝으로 시장을 예측하고 싶다…"
"RNN으로 주가를 예측해볼까?"
"Transformer 기반 모델에 재무제표를 넣으면?"

그렇다.
AI 기반 시스템 트레이딩은 정말 멋집니다.
하지만 문제는…
📆 시간이 너무 많이 듭니다.
📚 논문 읽고,
🧮 모델 만들고,
⚙️ 튜닝하고,
💀 수익은 안 나고…

그래서 나는 생각했습니다.

"이걸 진짜 간단하게 시작할 방법은 없을까?"


🧾 Chapter 2. 엑셀로 전략을 세우고, 파이썬으로 실행하자!

하루를 마무리하며 커피 한 잔.
나는 엑셀을 열고 이렇게 적었습니다.

종목명 매수가 매도가
삼성전자 70000 73000
카카오 45000 48000
 
혹시라도 오피스가 없는 분들은 엑셀을 구글 쉬트에서 만들 수 있습니다.
 
구글? AI는 너무 멀고, 엑셀은 가깝다 – 파이썬으로 자동 주식 매매하기
🧠 Chapter 1. 인공지능 시스템 트레이딩은… 멋지지만
"딥러닝으로 시장을 예측하고 싶다…"
"RNN으로 주가를 예측해볼까?"
"Transformer 기반 모델에 재무제표를 넣으면?"
 
그렇다.
AI 기반 시스템 트레이딩은 정말 멋집니다.
하지만 문제는…
📆 시간이 너무 많이 듭니다.
📚 논문 읽고,
🧮 모델 만들고,
⚙️ 튜닝하고,
💀 수익은 안 나고…
 
그래서 나는 생각했습니다.
 
"이걸 진짜 간단하게 시작할 방법은 없을까?"
 
🧾 Chapter 2. 엑셀로 전략을 세우고, 파이썬으로 실행하자!
하루를 마무리하며 커피 한 잔.
나는 엑셀을 열고 이렇게 적었습니다.
 
종목명 매수가 매도가
삼성전자 70000 73000
카카오 45000 48000
 
혹시라도 오피스가 없는 분들은 엑셀을 구글 쉬트에서 만들 수 있습니다.
구글 앱 중에 구글 쉬트가 있습니다.
 

구글 쉬트에서 위와 같이 입력 후 파일 > 다운로드 > Microsoft Exel 을 선택하면 됩니다.

 
 

“내가 원하는 가격에 사서, 원하는 가격에 팔아줘”
이 전략은 단순하다.
하지만 꾸준히, 빠르게, 감정 없이 실행만 해준다면?
사람보다 나을 수도 있지 않을까?

그래서 파이썬에게 시켰습니다.


🔧 Chapter 3. 만드는 방법은 이렇게 간단합니다.

1단계: 필요한 도구 설치

pip install pandas openpyxl pykiwoom

2단계: 엑셀 만들기 (stocks.xlsx)

종목명 매수가 매도가
삼성전자 70000 73000
카카오 45000 48000
 

저장 후 닫아두자. 나머지는 파이썬이 알아서 합니다.


🤖 Chapter 4. 파이썬 시스템 트레이딩 코드

import pandas as pd
from pykiwoom.kiwoom import Kiwoom
import time

EXCEL_PATH = "stocks.xlsx"
CHECK_INTERVAL = 30  # 몇 초마다 주가를 확인할지

def read_excel(path):
    return pd.read_excel(path)

def name_to_code(kiwoom, name):
    market_codes = kiwoom.GetCodeListByMarket('0') + kiwoom.GetCodeListByMarket('10')
    for code in market_codes:
        if kiwoom.GetMasterCodeName(code) == name:
            return code
    return None

def get_current_price(kiwoom, code):
    price = kiwoom.GetMasterLastPrice(code)
    if isinstance(price, str):
        return int(price.replace(",", ""))
    elif isinstance(price, int):
        return price
    else:
        return 0

def auto_trade_loop(kiwoom, account):
    bought = set()
    sold = set()

    while True:
        df = read_excel(EXCEL_PATH)
        for _, row in df.iterrows():
            name = row['종목명']
            buy_price = int(row['매수가'])
            sell_price = int(row['매도가'])
            code = name_to_code(kiwoom, name)

            if not code:
                print(f"[{name}] 종목 코드 못 찾음.")
                continue

            current_price = get_current_price(kiwoom, code)
            print(f"[{name}] 현재가: {current_price} | 매수: {buy_price} | 매도: {sell_price}")

            qty = 10  # 10주 매매 예시

            if current_price <= buy_price and name not in bought:
                print(f"[매수] {name} @ {current_price}")
                kiwoom.SendOrder("매수", "1001", account, 1, code, qty, 0, "03", "")
                bought.add(name)
                sold.discard(name)

            elif current_price >= sell_price and name not in sold:
                print(f"[매도] {name} @ {current_price}")
                kiwoom.SendOrder("매도", "1002", account, 2, code, qty, 0, "03", "")
                sold.add(name)
                bought.discard(name)

        print(f"🕐 {CHECK_INTERVAL}초 후 다시 확인...\n")
        time.sleep(CHECK_INTERVAL)

def main():
    kiwoom = Kiwoom()
    kiwoom.CommConnect(block=True)
    account = kiwoom.GetLoginInfo("ACCNO")[0]
    auto_trade_loop(kiwoom, account)

if __name__ == "__main__":
    main()

 

파일 삭제 실패 오류

실행 시 아래와 같은 에러가 발생할 수 있습니다.

이 경우 권한 부족 때문에 발생할 수 있습니다.

아래 폴더로 이동한 후 opversionup.exe 프로그램을 관리자 권한으로 실행하세요.

C:\OpenAPI

 

비밀번호 오류

계좌 비밀번호 입력창을 통해 계좌 비밀번호를 입력 및 등록하십시오

라는 에러가 발생하는 경우가 있을 수 있습니다.

 

윈도우 우측 하단을 보면 KOAStudio 실행 아이콘이 보입니다.

여기에서 계좌 비밀번호 저장을 선택 후 비밀번호를 등록하고 AUTO 체크 박스를 체크하면 됩니다.

혹시라도 아이콘이 보이지 않으면 C:\OpenAPI 에서 KOAStudioSA.exe 를 관리자 권한으로 실행 시킵니다.

이후 파일 > Open API 접속을 선택하면 됩니다.

✅ Chapter 5. 결과는?

  • 종목명을 엑셀에 적습니다.
  • 매수/매도 가격을 쓰면,
  • 파이썬이 지정가가 되면 매수하고, 매도가 되면 팔아줍니다.

이걸 모의투자부터 돌려보자.
진짜로 "자동 매매"가 눈앞에서 벌어집니다.


🏁 Chapter 6. 결론

AI는 아직 멀어보일 수 있습니다.
하지만 엑셀은 오늘 당장 시작할 수 있습니다.

엑셀은 나의 전략 노트,
파이썬은 나의 트레이딩 비서.

이제 복잡한 모델이 없어도
단순한 전략으로,
꾸준하게 수익을 쌓아갈 수 있습니다.

Posted by 제이브레인
,

Exchange Traded Fund의 약자로, 상장지수펀드라고도 합니다. ETF는 일반 주식처럼 증권거래소에 상장되어 거래되는 펀드입니다.

🔍 쉽게 말하면?

  • ETF는 여러 종목을 한 번에 담은 바구니처럼 생긴 투자 상품입니다.
  • 이 바구니를 주식처럼 사고팔 수 있어서, 펀드의 분산투자 효과주식의 편리한 거래성을 동시에 누릴 수 있습니다.

📌 ETF의 주요 특징

항목 설명
구성 주식, 채권, 원자재, 외환 등 다양한 자산으로 구성 가능
거래 주식시장(예: 코스피, 코스닥)에서 실시간으로 사고팔 수 있음
분산 투자 하나의 ETF로 여러 종목에 분산 투자하는 효과
낮은 수수료 보통 액티브 펀드보다 운용보수가 저렴함
투명성 대부분의 ETF는 구성 종목을 매일 공개

저는 ETF 는 모두 같은 줄 알았습니다.

그런데 ETF 도 과세와 비과세가 있더군요.

아무래도 과세의 경우 양도소득세 대상이므로 주의를 해야 합니다.

ETF의 과세 여부를 정리한 표입니다.


✅ 국내 주식 ETF 과세 구분표

구분 ETF 예시 투자대상  과세 여부 비고
비과세 ETF KODEX 200, TIGER 200, KODEX 코스닥150 등 국내 주식 100% ❌ 비과세 매매차익·분배금 모두 비과세 (기본 세금 無)
과세 ETF KODEX 골드선물, TIGER 미국S&P500, KODEX 레버리지, 인버스 등 해외 주식, 원자재, 파생상품, 채권 포함 ✅ 과세 매매차익은 양도소득세 대상, 분배금은 배당소득세(15.4%)
 

📌 과세 기준 상세 설명

항목설명
비과세 ETF 조건 ETF 자산의 100%가 국내 상장 주식으로 구성되어야 함
과세 ETF 조건 ETF 자산 중 해외 주식, 채권, 원자재, 선물 등이 포함된 경우
양도소득세 연 250만 원 공제 후 22% 과세 (지방세 포함), 과세 ETF 매매차익에 해당됨
배당소득세 15.4% (소득세 14% + 지방소득세 1.4%) – 분배금에 적용
 

💡 참고사항

  • 인버스 / 레버리지 ETF는 국내지수 기반이더라도 파생상품 포함으로 과세 대상입니다.
  • 개인투자자가 비과세 혜택을 받으려면 국내 주식형 ETF인지 꼭 확인해야 합니다.
  • 국내 ETF라도 해외 자산이 일부라도 포함되면 과세 대상입니다.

 

미국 주식에 투자할 때 한국 거주자가 부담해야 할 세금은 크게 두 가지로 나뉩니다:


✅ 1. 배당소득세 (미국에서 원천징수)

  • 미국 기업이 배당금을 지급할 경우, 미국 정부가 세금을 먼저 떼고 줍니다.
  • 세율: 15% (한미 조세조약 적용)
    • 예: 애플 주식에서 $100의 배당이 발생하면, $15는 세금으로 미국에 납부되고, $85만 국내 계좌로 들어옵니다.
  • 종합소득세 신고 대상 여부
    • 국내에서 금융소득이 연 2,000만 원을 초과하면, 종합소득세 신고 의무가 생깁니다.

✅ 2. 양도소득세 (한국에서 과세)

  • 미국 주식을 팔아서 차익이 생기면, 한국 국세청에 양도소득세를 내야 합니다.
  • 과세 대상: 연간 미국 주식 투자로 발생한 순이익이 250만 원을 초과할 경우
  • 세율:
    • 기본 22% (지방세 포함)
    • 순이익 = (매도금액 - 매수금액 - 필요경비)

예시

  • 테슬라 주식을 사서 연말까지 600만 원 벌었다면:
    • 과세 대상 = 600만 원 - 250만 원 = 350만 원
    • 세금 = 350만 원 × 22% = 약 77만 원

📌 참고사항

  • 손실이 난 경우 세금은 없습니다.
  • 양도소득세는 다음 해 5월 종합소득세 신고 기간에 별도로 신고 및 납부해야 합니다.
  • 해외주식 손익은 5년간 이월공제가 가능하므로, 손실이 발생한 해는 꼭 신고해 두는 게 유리합니다.

 

미국 주식의 경우 배당 시 배당소득세 15%를 미국에서 원천 징수하고 주식을 매매 시 차익이 생기기면 한국에서 순이익 250만원을 초과할 경우 양도소득세 22%(지방세 포함)가 붙는다고 하니 정말 많이 붙네요.

매매 차익의 경우 한국 주식 과 미국 주식은 따로 계산합니다.

한국 주식 매매차익 -1,000만원 + 미국 주식 매매 차익 500만원이면 전체는 -500만원이지만 양도 소득세를 냅니다.

한국 주식과 미국 주식의 양도소득세는 과세 체계가 완전히 다르기 때문에, 서로 상계되지 않습니다.


📌 요약

구분 매매차익 과세 여부 비고
한국 주식 -1,000만 원 (손실) 비과세 국내 상장주식은 개인 투자자에게 양도세 비과세 (일반적으로)
미국 주식 +500만 원 (이익) 과세 대상 해외 주식은 연 250만 원 초과 시 양도소득세 22% 부과
 

✅ 계산 예시 (미국 주식 양도세만 해당)

  1. 과세 대상 금액
    = 500만 원 (미국 주식 이익) – 250만 원 (기본 공제)
    = 250만 원
  2. 양도소득세 (지방세 포함 22%)
    = 250만 원 × 22%
    = 55만 원 납부

❌ 국내 손실과 해외 수익은 상계되지 않음

  • 한국 주식에서 -1,000만 원 손실을 봐도, 미국 주식에서 +500만 원 수익이 나면 해당 수익에 대해서는 따로 과세됩니다.
  • 한국 주식해외 주식별개의 과세 체계로 취급되기 때문입니다.

✅ 팁: 해외 주식 손실은 이월공제 가능 (최대 5년)

  • 만약 미국 주식에서 손실이 났다면, 다음 해 이후 5년간 이익에서 차감할 수 있습니다.
  • 하지만 한국 주식 손실은 이월공제 대상이 아니고, 세금도 비과세라 따로 혜택이 없습니다.

 

한국 주식 + 미국 주식이 서로 상계되지 않아서 잘못하면 전체 매매 차익이 마이너스라도 세금을 낼수 있겠네요.

하하~~ 한국 주식 또는 미국 주식으로 한 쪽으로 집중해야겠어요.

잘못하면 비좋은 개살구라고 세금은 세금데로 내고 소득은 없는 최악의 경우도 발생할 수 있겠어요.

Posted by 제이브레인
,

Qt Designer 로 xxx.ui 의 경우 Designer 툴로 편집하는 장점은 있으나 동적 UI 생성이 어려운 점이 있었습니다.

예를 들면 windows 를 확대하거나 축소 시를 고려하는 것이 어렵더군요.

선택 기준 Qt Designer 사용 코드 작성
빠른 UI 개발 ✅ 유리 ❌ 불리
UI 변경이 잦음 ❌ 불리 ✅ 유리
동적 UI 생성 ❌ 불리 ✅ 유리
초보자 접근성 ✅ 유리 ❌ 불리
버전 관리 편의성 ❌ 불리 ✅ 유리
복잡한 애니메이션 ❌ 불리 ✅ 유리

그래서 xxx.ui 를 코드 작성하는 것으로 변경하였습니다.

1) 코드 생성

xxx.ui 파일을 전체를 복사 후에 아래와 같은 ChatGPT 프롬프트를 사용하였습니다.

다음은 python Qt designer 로 만들어졌습니다.
이를 .ui 파일이 아닌 .py 파일로 수정해주세요.
accountBalance, accountStocks 는 우측  layout 으로 표시해주세요.

코드 생성 후 다음과 같이 추가적인 프롬프트를 사용하였습니다.

브라우저를 종료 후에 입력하지 말고 기존 프롬프트 창에 계속 입력하면 됩니다.

accountBalance 의 경우 table 이 2라인입니다. window 확대 시에 table 이 2 라인인 것을 고려하여 확대해주세요.
accountStocks 에 2행 3열의 table 은 보입니다. 그런데 나머지 부분이 횐색으로 표시됩니다. 2행 3열의 table 크기에 맞추어 횐색 부분의 크기를 줄여주세요.

 

전체 코드는 아래 git 에서 확인 할 수 있습니다.

git clone https://github.com/jbpark/JbTraderExample.git
cd JbTraderExample/jbtrader/ch5.12

 

2) 코드 실행

실행 결과 확대 축소 시에 정렬이 잘되는 것을 확인 할 수 있습니다.

Posted by 제이브레인
,

종목 코드만을 표시했더니 종목 코드가 정상적으로 입력했는지 구분이 어려웠습니다.

예를 들면 삼성전자 종목 코드를 입력해도 내가 정말 삼성전자 종목코드를 입력했는지는 명확하지 않았습니다.

그래서 종목 코드를 입력하면 종목 명칭을 업데이트 하는 기능을 추가하였습니다.

1) 코드 생성

ChatGPT 에 사용된 프롬프트는 다음과 같습니다.

python pyqt5 designer 로 guruma_one.ui 를 만들었습니다.

guruma_one.ui 에는 다음과 같은 콤포넌트들이 존재합니다.
name : accountComboBox, type : QComboBox
name : logTextEdit, type : QTextEdit
name : stockName , type : QLineEdit
name : stockCode , type : QLineEdit

다음과 같은 기능을 제공합니다.
1. 키움증권 API 를 연결하고 연결 후 API를 초기화하고 실 서버 연결 시 "실 서버 연결 성공" 이라고 메시지를 logTextEdit 에 출력하고 
모의 투자 서버 연결 시 "모의 투자 서버 연결 성공"이라고 메시지를 logTextEdit 에 출력
2. Kiwoom 계좌 정보를 accountComboBox 에 출력
3. stockCode 에 종목 코드를 입력하면 stockName 에 종목 이름을 입력


파이썬 코드로 작성해주세요. 주석도 자세히 달아주세요. 코드는 오류 테스트 후에 오류가 있으면 수정해주세요.

 

생성된 코드는 다음과 같으며 생성된 코드 중 일부 버그는 수정하였습니다.

아래 코드는 다음 위치에서 다운 받는 것이 가능합니다.

git clone https://github.com/jbpark/JbTraderExample.git
cd JbTraderExample/jbtrader/ch5.11

 

stock_name.py

import sys
from PyQt5.QtWidgets import QApplication, QMainWindow, QMessageBox
from PyQt5.uic import loadUi
from PyQt5.QtCore import QEventLoop
from pykiwoom.kiwoom import Kiwoom


class KiwoomApp(QMainWindow):
    def __init__(self):
        super().__init__()
        loadUi("guruma_one.ui", self)  # UI 파일 로드

        self.kiwoom = Kiwoom()
        self.kiwoom.CommConnect()  # 키움 API 연결

        # 이벤트 처리 연결
        self.kiwoom.OnEventConnect = self.on_event_connect  # 연결 이벤트 핸들러

        self.stockCode.editingFinished.connect(self.update_stock_name)

    def on_event_connect(self, err_code):
        """
        키움 API 연결 이벤트 처리
        실서버와 모의서버에 따른 로그 메시지를 출력함
        """
        if err_code == 0:
            server_type = self.kiwoom.GetLoginInfo("GetServerGubun")
            if server_type == "1":
                self.logTextEdit.append("모의 투자 서버 연결 성공")
            else:
                self.logTextEdit.append("실 서버 연결 성공")

            # 계좌 정보 가져오기
            self.update_account_info()
        else:
            QMessageBox.critical(self, "오류", "키움 API 연결 실패")

    def update_account_info(self):
        """
        계좌 정보를 가져와서 accountComboBox에 출력
        """
        accounts = self.kiwoom.GetLoginInfo("ACCNO").split(';')
        self.accountComboBox.clear()
        self.accountComboBox.addItems([acc.strip() for acc in accounts if acc.strip()])

    def update_stock_name(self):
        """
        stockCode에 입력된 종목 코드에 따라 stockName을 자동으로 설정
        """
        code = self.stockCode.text().strip()
        if code:
            stock_name = self.kiwoom.GetMasterCodeName(code)
            self.stockName.setText(stock_name)


if __name__ == "__main__":
    app = QApplication(sys.argv)
    window = KiwoomApp()
    window.show()
    sys.exit(app.exec_())

 

위 코드의 핵심은 다음과 같이 종목 코드로 종목명을 구하는 부분입니다.

stock_name = self.kiwoom.GetMasterCodeName(code)

 

Qt Designer 로 종목명도 추가해줍니다.

2) 코드 실행

위 코드를 실행 후에 종목 코드에 주식 종목 코드를 입력하면 아래와 같이 종목명이 출력됩니다.

종목명이 표시되므로 내가 어떤 종목 코드를 입력했는지 좀더 확실해져서 좋네요.

3) JbTrader 코드 통합

JbTrader 에 종목명 부분 통합 시에 변경된 부분입니다.

Class init 함수에 stockCode 가 edit 가 종료되면 stockName 을 업데이트하도록 이벤트 추가되어 있습니다.

stockName 은 readOnly 로 설정하고 background color  를 회색으로 변경하였습니다.

class StockTrader(QtWidgets.QMainWindow):
    def __init__(self):
        super().__init__()

...

        self.stockName.setReadOnly(True)
        self.stockName.setStyleSheet("background: lightgray; color: black;")

        self.stockCode.editingFinished.connect(self.update_stock_name)

    def update_stock_name(self):
        """
        stockCode에 입력된 종목 코드에 따라 stockName을 자동으로 설정
        """
        code = self.stockCode.text().strip()
        if code:
            stock_name = self.kiwoom.GetMasterCodeName(code)
            self.stockName.setText(stock_name)

 

Posted by 제이브레인
,

시장가로 매수하거나 지정가로 매수하려고 할 때 현재 가격이 표시되어 있지 않고 입력만 하도록 되어 있어서 잘못 입력할 수도 있고 가격을 다른 곳에서 확인 후에 입력해야 해서 많이 불편하네요.

그래서 매수/매도 가격에 현재가와 상하호가를 입력하여 직관적으로 선택하도록 기능추가하려고 합니다.

1) 코드 생성

ChatGPT 에 사용한 프롬프트입니다.

매수/매도 가격에 현재가를 기준으로 상하호가를 계산하여 입력하도록 했습니다.

그리고 5초에 한번 자동 업데이트하게 했고 만약 사용자 매수/매도 가격을 선택하면 자동 업데이트를 멈추게했습니다.

python pyqt5 designer 로 guruma_one.ui 를 만들었습니다.

guruma_one.ui 에는 다음과 같은 콤포넌트들이 존재합니다.
name : accountComboBox, type : QComboBox
name : logTextEdit, type : QTextEdit
name : stockCode , type : QLineEdit
name : orderType, type : QComboBox
name : buyPrice, type : QComboBox
name : buyAmount, type : QLineEdit
name : sellPrice, type : QComboBox
name : sellAmount, type : QLineEdit
name : buyButton, type : QPushButton

다음과 같은 기능을 제공합니다.
1. 키움증권 API 를 연결하고 연결 후 API를 초기화하고 실 서버 연결 시 "실 서버 연결 성공" 이라고 메시지를 logTextEdit 에 출력하고 
모의 투자 서버 연결 시 "모의 투자 서버 연결 성공"이라고 메시지를 logTextEdit 에 출력
2. Kiwoom 계좌 정보를 accountComboBox 에 출력
3. stockCode 에 종목 코드를 입력하면 호가 단위를 현재 종목코드의 현재가를 기준으로 계산
4. buyPrice 에 -20호가 부터 +20호가까지 추가합니다. 
QSpinBox 의 각 item 은 -+호가 : 가격을 표시하고 현재가격은 현재가 : 가격을 표시합니다.
기본값은 현재가를 선택합니다.
5. sellPrice 에도 buyPrice와 동일한 값을 선택합니다.
기본값은 +1호가를 선택합니다.
6. 5초에 한번씩 가격을 업데이트하고 만약 buyPrice 또는 sellPrice 의 가격을 선택하면 업데이트를 멈춥니다.

파이썬 코드로 작성해주세요. 주석도 자세히 달아주세요. 코드는 오류 테스트 후에 오류가 있으면 수정해주세요.

 

Qt Designer 에서 buyPrice, sellPrice 를 QLineEdit -> QComboBox 로 변경합니다.

2) 생성된 코드

생성된 코드이며 일부 버그는 수정하였습니다.

아래 코드는 다음 위치에서 다운 받는 것이 가능합니다.

git clone https://github.com/jbpark/JbTraderExample.git
cd JbTraderExample/jbtrader/ch5.10

price_update.py

import sys

from PyQt5.QtCore import QTimer
from PyQt5.QtWidgets import QApplication, QMainWindow
from PyQt5.uic import loadUi
from pykiwoom.kiwoom import Kiwoom


def get_cell_value_or_error(df, row_idx, col_name):
    try:
        return df.at[row_idx, col_name]  # 특정 위치의 값 반환
    except KeyError:
        raise ValueError(f"Invalid index '{row_idx}' or column '{col_name}'")


class GurumaApp(QMainWindow):
    def __init__(self):
        super().__init__()
        loadUi("guruma_one.ui", self)  # UI 파일 로드

        self.kiwoom = Kiwoom()  # Kiwoom 인스턴스 생성
        self.kiwoom.CommConnect()  # API 연결

        self.kiwoom.OnEventConnect = self.event_connect  # 연결 이벤트 핸들러

        # 버튼 클릭 이벤트 연결
        self.buyButton.clicked.connect(self.update_stock_price)

        # 주기적으로 가격 업데이트를 위한 타이머 설정
        self.price_update_timer = QTimer(self)
        self.price_update_timer.timeout.connect(self.update_stock_price)
        self.price_update_timer.start(5000)  # 5초마다 실행

        self.buyPrice.installEventFilter(self)
        self.sellPrice.installEventFilter(self)

    def eventFilter(self, obj, event):
        if (obj == self.buyPrice or obj == self.sellPrice) and event.type() == event.MouseButtonPress:
            self.on_combobox_clicked()
        return super().eventFilter(obj, event)

    def on_combobox_clicked(self):
        print("QComboBox가 클릭되었습니다!")  # 특정 함수 실행
        self.stop_price_update()

    def event_connect(self, err_code):
        """
        키움증권 API 연결 이벤트 처리
        """
        if err_code == 0:
            server_type = self.kiwoom.GetLoginInfo("GetServerGubun")
            if server_type == "1":
                self.logTextEdit.append("모의 투자 서버 연결 성공")
            else:
                self.logTextEdit.append("실 서버 연결 성공")
            self.load_accounts()
        else:
            self.logTextEdit.append("API 연결 실패")

    def load_accounts(self):
        """
        계좌 정보를 가져와서 accountComboBox에 출력
        """
        accounts = self.kiwoom.GetLoginInfo("ACCNO").split(';')
        self.accountComboBox.addItems([acc for acc in accounts if acc])

    def update_stock_price(self):
        """
        stockCode 입력 시 현재 가격 및 상하 몇 호가 가격을 buyPrice, sellPrice에 반영
        """
        stock_code = self.stockCode.text().strip()
        if not stock_code:
            self.logTextEdit.append("종목 코드를 입력하세요.")
            return

        self.kiwoom.SetInputValue("종목코드", stock_code)
        self.kiwoom.CommRqData("주식기본정보", "opt10001", 0, "0101")
        QTimer.singleShot(5000, self.process_stock_price)

    def get_price_tick(self, price):
        """
        현재가를 기반으로 호가 단위를 계산하는 함수
        """
        if price < 1000:
            return 1
        elif price < 5000:
            return 5
        elif price < 10000:
            return 10
        elif price < 50000:
            return 50
        elif price < 100000:
            return 100
        elif price < 500000:
            return 500
        else:
            return 1000

    def process_stock_price(self):
        """
        조회한 주식 정보를 buyPrice와 sellPrice에 반영
        """
        """
                조회한 주식 정보를 buyPrice와 sellPrice에 반영
                """

        stock_code = self.stockCode.text().strip()
        if not stock_code:
            self.logTextEdit.append("종목 코드를 입력하세요.")
            return

        df = self.kiwoom.block_request("opt10001",
                                       종목코드=stock_code,
                                       output="주식기본정보",
                                       next=0)

        current_price_str = get_cell_value_or_error(df, 0, '현재가')
        if not current_price_str:
            self.logTextEdit.append("현재가 정보를 가져올 수 없습니다.")
            return

        current_price_str = str(int(current_price_str))

        try:
            current_price = abs(int(current_price_str))
        except ValueError:
            self.logTextEdit.append("현재가 변환 오류: {current_price_str}")
            return

        price_tick = self.get_price_tick(current_price)

        self.buyPrice.clear()
        self.sellPrice.clear()

        for i in range(-20, 21):  # -20호가부터 +20호가까지 추가
            price = current_price + i * price_tick
            label = f"{i}호가: {price}" if i != 0 else f"현재가: {price}"
            self.buyPrice.addItem(label, price)
            self.sellPrice.addItem(label, price)

        # 기본값 설정: buyPrice는 현재가, sellPrice는 +1호가
        self.buyPrice.setCurrentIndex(20)
        self.sellPrice.setCurrentIndex(21)

        self.logTextEdit.append(f"현재가 {current_price}원부터 ±20호가까지 설정 완료")

    def stop_price_update(self):
        """
        사용자가 가격을 선택하면 타이머 정지
        """
        print("stop_price_update")
        self.price_update_timer.stop()
        self.logTextEdit.append("가격 선택 완료. 자동 업데이트 중지")


if __name__ == "__main__":
    app = QApplication(sys.argv)
    window = GurumaApp()
    window.show()
    sys.exit(app.exec_())

3) 코드 실행

종목 코드에 삼성전자를 입력하고 실행 시 아래와 같이 매수 금액이 표시되는 것을 확인 할 수 있습니다.

매도 금액은 현재가 보다 1호가 위를 기본 선택하도록 했습니다.

 

4) JbTrader 에 가격 정보 업데이트 통합

위 가격 정보를 표시하는 기능을 JbTrader 에 통합시켰습니다.

통합된 코드는 다음 위치에 있습니다.

git clone https://github.com/jbpark/JbTraderExample.git
cat JbTraderExample/jbtrader/ch5.10/guruma_one.py

통합 후에 실행한 모습니다.

뭔가~ 그럴 듯한 모습을 갖춰가는 것 같지 않나요?

모의 계좌이긴한데 삼성전자를 사두었었는데 수익률이 1.32%네요.

실계좌도 빨리 수익이 났으면 좋겠어요.

Posted by 제이브레인
,

1) 코드 생성

로그인한 계좌의 잔고를 표시하는 기능을 만들어보겠습니다.

해당 기능을 테스트 후에 기존 코드와 통합하면 됩니다.

자동차를 만들 때 자동차 바퀴를 테스트한 뒤에 문제가 없으면 장착하는 것과 같습니다.

그리고 기능 하나 하나 만들 때 요즘은 인공지능 툴에 물어보면 되므로 정말 만들기가 편해졌습니다.

기존 API 기능을 만들 때는 두꺼운 메뉴얼을 보고 해당 메뉴얼에서 예제를 찾아서 만들었습니다.

기능하나 하나 만들는 시간도 많이 들었고 해당 예제를 찾는 것이 시간이 걸렸습니다.

인터넷에서 찾을 때도 요령이 없으면 한참을 검색하여 찾았습니다.

그런데 제가 찾는 코드가 파이썬인데 C++, Java 등의 다른 언어 였다면 이를 응용하기도 힘들었습니다.

이제는 예제도 만들기 쉬워서 기존 코드와 통합하는 요령만 알면 됩니다.

저는 기능 하나를 기존 코드와 통합 할 때 함수 형태로 분리가능하도록 만든 후에 함수를 추가하고 버튼이나 초기화에 추가하는 방법으로 통합하고 있습니다.

 

다음과 같은 ChatGPT 프롬프트를 사용하여 생성하였고 에러 수정하였습니다.

python, 키움 pykiwoom 모듈을 사용하여 QTableWidget  에 잔고를 표시하는 코드를 만들어주세요.

아래 코드는 다음 위치에서 다운 받는 것이 가능합니다.

git clone https://github.com/jbpark/JbTraderExample.git
cd JbTraderExample/jbtrader/ch5.9

 

kiwoom_balance.py

import sys

from PyQt5.QtWidgets import QApplication, QWidget, QVBoxLayout, QTableWidget, QTableWidgetItem, QPushButton, QLabel
from pykiwoom.kiwoom import Kiwoom


class BalanceViewer(QWidget):
    def __init__(self):
        super().__init__()
        self.initUI()
        self.kiwoom = Kiwoom()
        self.kiwoom.CommConnect()

    def initUI(self):
        self.setWindowTitle("잔고 조회")
        self.setGeometry(100, 100, 600, 500)

        layout = QVBoxLayout()
        self.setLayout(layout)

        # D+2추정예수금, 추정예탁자산, 누적투자손익 라벨 추가
        self.deposit_label = QLabel("D+2추정예수금: ")
        self.total_eval_label = QLabel("추정예탁자산: ")
        self.estimated_asset_label = QLabel("누적투자손익: ")
        layout.addWidget(self.deposit_label)
        layout.addWidget(self.total_eval_label)
        layout.addWidget(self.estimated_asset_label)

        # 테이블 위젯 생성
        self.tableWidget = QTableWidget()
        layout.addWidget(self.tableWidget)

        # 조회 버튼 생성
        self.btn_refresh = QPushButton("잔고 조회")
        self.btn_refresh.clicked.connect(self.load_balance)
        layout.addWidget(self.btn_refresh)

    def load_balance(self):
        account_list = self.kiwoom.GetLoginInfo("ACCNO")  # 계좌 목록 가져오기
        if isinstance(account_list, list) and account_list:  # 리스트이고 값이 있으면
            account_num = account_list[0]  # 첫 번째 계좌 선택
        else:
            print("계좌 정보를 가져오지 못했습니다.")
            return

        df = self.kiwoom.block_request("opw00018",
                                       계좌번호=account_num,
                                       비밀번호="",
                                       비밀번호입력매체구분="00",
                                       조회구분=2,
                                       output="계좌평가잔고개별합산",
                                       next=0)

        columns = ["종목명", "평가손익", "수익률", "보유수량", "매입가"]
        self.tableWidget.setColumnCount(len(columns))
        self.tableWidget.setHorizontalHeaderLabels(columns)

        self.tableWidget.setRowCount(len(df))

        rowNum = 0
        for index, row in df.iterrows():
            self.tableWidget.setItem(rowNum, 0, QTableWidgetItem(row['종목명']))
            self.tableWidget.setItem(rowNum, 1, QTableWidgetItem(str(int(row['평가손익']))))
            self.tableWidget.setItem(rowNum, 2, QTableWidgetItem(str(float(row['수익률(%)']))))
            self.tableWidget.setItem(rowNum, 3, QTableWidgetItem(str(int(row['보유수량']))))
            self.tableWidget.setItem(rowNum, 4, QTableWidgetItem(str(int(row['매입가']))))
            rowNum += 1

        df = self.kiwoom.block_request("opw00004",
                                       계좌번호=account_num,
                                       비밀번호="",
                                       상장폐지조회구분=0,
                                       비밀번호입력매체구분="00",
                                       output="계좌평가현황",
                                       next=0)

        self.deposit_label.setText(f"D+2추정예수금: {str(int(df['D+2추정예수금'][0]))}")
        self.total_eval_label.setText(f"추정예탁자산: {str(int(df['추정예탁자산'][0]))}")
        self.estimated_asset_label.setText(f"누적투자손익: {str(int(df['누적투자손익'][0]))}")


if __name__ == '__main__':
    app = QApplication(sys.argv)
    viewer = BalanceViewer()
    viewer.show()
    sys.exit(app.exec_())

 

2) 코드 실행

짜잔~~ 실행이 되었네요.

 

3) JbTader 에 잔고 조회 추가

Qt Designer 로 잔고 조회를 위한 Table Widget 을 추가합니다.

아래와 같이 행, 열을 추가합니다.

행의 경우 잔고에 표시를 원하는 항목을 추가합니다.

예수금 D+2, 추정예탁자산, 누적투자손익을 추가하였습니다.

열은 항목, 데이터를 추가하였습니다.

 

잔고 표시 통합합니다.

load_balance 라는 함수로 우선 분리 후에 이를 QTableWidget 에 추가하도록 해당 부분을 수정하면됩니다.

코드를 통합 시에는 개별항목으로 분리하고 본 코드에 Copy and Paste 후에 해당 함수에서 UI 항목등의 항목을 변경하는 순으로 진행하였습니다.

gruma_one.py (일부 코드이며 전체 코드는 위의  git 에서 확인이 가능합니다.)

    def load_balance(self):
        if self.kiwoom.GetConnectState() == 0:
            print("키움증권 로그인 실패!")
        else:
            print("키움증권 로그인 성공!")

        accounts = self.kiwoom.GetLoginInfo("ACCNO")
        print("계좌 목록:", accounts)

        if isinstance(accounts, list):  # 리스트이면 첫 번째 계좌 선택
            account_num = accounts[0]
        else:  # 문자열이면 split() 사용
            account_num = accounts.strip().split(';')[0]

        print("사용할 계좌:", account_num)

        df = self.kiwoom.block_request("opw00004",
                                       계좌번호=account_num,
                                       비밀번호="",
                                       상장폐지조회구분=0,
                                       비밀번호입력매체구분="00",
                                       output="계좌평가현황",
                                       next=0)

        # 예수금, 추정예탁자산, 누적투자손익 업데이트
        item = QTableWidgetItem(str(int(df['D+2추정예수금'][0])))
        item.setTextAlignment(Qt.AlignVCenter | Qt.AlignRight)
        self.accountBalance.setItem(1, 0, item)

        item = QTableWidgetItem(str(int(df['추정예탁자산'][0])))
        item.setTextAlignment(Qt.AlignVCenter | Qt.AlignRight)
        self.accountBalance.setItem(1, 1, item)

        item = QTableWidgetItem(str(int(df['누적투자손익'][0])))
        item.setTextAlignment(Qt.AlignVCenter | Qt.AlignRight)
        self.accountBalance.setItem(1, 2, item)

4) 보유 종목 추가

보유 종목을 위해서 Table Widget 을 추가합니다.

항목은 이미지와 같이 종목명, 평가손익, 수익률(%), 보유수량, 매입가를 추가하였습니다.

gruma_one.py (일부 코드이며 전체 코드는 위의  git 에서 확인이 가능합니다.)

    def load_stocks(self):
        if self.kiwoom.GetConnectState() == 0:
            print("키움증권 로그인 실패!")
        else:
            print("키움증권 로그인 성공!")

        accounts = self.kiwoom.GetLoginInfo("ACCNO")
        print("계좌 목록:", accounts)

        if isinstance(accounts, list):  # 리스트이면 첫 번째 계좌 선택
            account_num = accounts[0]
        else:  # 문자열이면 split() 사용
            account_num = accounts.strip().split(';')[0]

        print("사용할 계좌:", account_num)

        df = self.kiwoom.block_request("opw00018",
                                       계좌번호=account_num,
                                       비밀번호="",
                                       비밀번호입력매체구분="00",
                                       조회구분=2,
                                       output="계좌평가잔고개별합산",
                                       next=0)

        columns = ["종목명", "평가손익", "수익률(%)", "보유수량", "매입가"]
        self.accountStocks.setColumnCount(len(columns))
        self.accountStocks.setHorizontalHeaderLabels(columns)

        self.accountStocks.setRowCount(len(df))

        rowNum = 0
        for index, row in df.iterrows():
            self.accountStocks.setItem(rowNum, 0, QTableWidgetItem(row['종목명']))
            self.accountStocks.setItem(rowNum, 1, QTableWidgetItem(str(int(row['평가손익']))))
            self.accountStocks.setItem(rowNum, 2, QTableWidgetItem(str(float(row['수익률(%)']))))
            self.accountStocks.setItem(rowNum, 3, QTableWidgetItem(str(int(row['보유수량']))))
            self.accountStocks.setItem(rowNum, 4, QTableWidgetItem(str(int(row['매입가']))))
            rowNum += 1

5) JbTader 실행

잔고항목 및 보유 종목이 추가되었습니다.

뭔가~~ 조금씩 기능 추가가 되어가는 모습을 보니 뿌듯합니다.

Posted by 제이브레인
,

실행된 JbTrader Main Window 를 보면 폰트에 따라 글씨가 잘리기도 합니다.

뭔가~~ 완성도가 떨어지는 것으로 보이네요.

폰트는 시스템마다 약간씩 다르므로 제대로 설정하지 않으면 글씨가 잘리거나 너무 작게 표시될 수 있습니다.

1) 폰트 다운로드

폰트 중에 무료 글꼴인 나눔 글꼴을 다운로드 받아 보겠습니다.

나눔글꼴 저작권을 검색하니 다음과 같다고 합니다.

참고하세요.

네이버 나눔글꼴의 지적 재산권은 네이버와 네이버문화재단에 있습니다. 네이버 나눔글꼴은 개인 및 기업 사용자를 포함한 모든 사용자에게 무료로 제공되며 자유롭게 수정하고 재배포하실 수 있습니다. 단, 글꼴 자체를 유료로 판매하는 것은 금지하며 네이버 나눔글꼴은 본 저작권 안내와 라이선스 전문을 포함해서 다른 소프트웨어와 번들하거나 재배포 또는 판매가 가능합니다.

naver.com 에서 나눔 고딕 검색합니다.

한글한글 아름답게를 선택합니다.

글꼴모음을 클릭합니다.

 

나눔글꼴 전체 내려받기를 클릭합니다.

2) 폰트 설치

나눔고딕은 압축 파일을 풀고
nanum-all\나눔 글꼴\나눔고딕\NanumFontSetup_TTF_GOTHIC\
폴더의 ttf 파일을 아래 경로로 복사하면 됩니다.

C:\Windows\Fonts\

3) Qt Designer 에 폰트 적용

Qt Designer 실행 후 폰트를 모두 적용합니다.

아래 코드는 다음 위치에서 다운 받는 것이 가능합니다.

git clone https://github.com/jbpark/JbTraderExample.git
cd JbTraderExample/jbtrader/ch5.8

QLabel 글씨가 잘리지 않도록 글씨 선택 후 마우스로 잘린 영역 없도록 드래그하여 늘려주는 것이 중요합니다.

 

4) 폰트 적용 확인

폰트 적용 후 프로그램을 다시 실행하면 아래와 같이 짤리는 글씨 없이 정상 출력됨을 확인 할 수 있습니다.

Posted by 제이브레인
,