IT개발

Kiwoom OpenAPI를 활용한 자동 주식 매매 프로그램 개발

HyochulLab 2025. 3. 10. 15:24

1. 소개

주식 시장에서 자동 매매 시스템은 투자 전략을 빠르고 효과적으로 실행할 수 있는 중요한 도구입니다. 이번 글에서는 Kiwoom OpenAPI를 활용하여 주식 매매 프로그램을 구현하는 방법을 소개합니다. 이 프로그램은 **싱글턴 패턴(Singleton)**을 통해 AxKHOpenAPI 객체를 어디서든 간편하게 접근할 수 있도록 구성되어 있으며, 다음과 같은 기능을 제공합니다.

  • 로그인 및 로그인 상태 확인
  • 계좌 조회
  • 현재가 조회
  • 매수 및 매도 주문
  • 체결 및 잔고 조회

2. 설계 개요

주요 클래스 및 파일 구성

파일/클래스명 설명

SingletonBase<T> 싱글턴 패턴을 구현하기 위한 공통 클래스
APIContext AxKHOpenAPI를 싱글턴으로 관리하며, 이벤트를 등록해주는 클래스
TradeManager 매매 로직(로그인, 계좌 조회, 주문, 등)을 수행
MainForm UI 및 이벤트 처리 (TR 결과 반영, 로그인 상태 표시 등)
MainForm.Designer.cs MainForm의 컨트롤 배치 및 디자인 요소를 담는 코드

3. 싱글턴 패턴 구현 - SingletonBase<T>

가장 먼저, 싱글턴 패턴을 간단하게 구현할 수 있는 SingletonBase<T> 클래스를 정의합니다.
해당 클래스를 통해 어떤 클래스든 하나의 인스턴스만 생성하고, 전역에서 접근 가능하도록 설정할 수 있습니다.

/// <summary>
/// 싱글턴 패턴을 제네릭으로 구현한 클래스
/// </summary>
public sealed class SingletonBase<T> where T : new()
{
    private static readonly Lazy<T> instance = new Lazy<T>(() => new T());

    /// <summary>
    /// 유일한 인스턴스를 반환합니다.
    /// </summary>
    public static T Instance
    {
        get { return instance.Value; }
    }

    // 외부에서 생성자를 호출할 수 없도록 private 선언
    private SingletonBase() { }
}

4. Kiwoom OpenAPI 관리 - APIContext

다음은 싱글턴 패턴을 활용하여 **Kiwoom OpenAPI(AxKHOpenAPI)**를 전역에서 공유할 수 있도록 하는 클래스입니다.
AxKHOpenAPI는 WinForms 컨트롤이기 때문에 Control을 상속하는 내부 Context 클래스를 통해 생성합니다.

using System;
using System.Windows.Forms;
using AxKHOpenAPILib;

public abstract class APIContext
{
    /// <summary>
    /// 어디서든지 AxKHOpenAPI 인스턴스에 접근하기 위한 정적 프로퍼티
    /// </summary>
    public static AxKHOpenAPI Get
    {
        get
        {
            var context = SingletonBase<Context>.Instance;
            if (!context.Created)
            {
                // Control이 생성되지 않았다면 강제로 CreateControl()을 호출하여 핸들을 만들어줌
                context.CreateControl();
            }
            return context.axApi;
        }
    }

    /// <summary>
    /// 실제로 AxKHOpenAPI를 생성하고 관리하는 내부 클래스
    /// </summary>
    private sealed class Context : Control
    {
        internal AxKHOpenAPI axApi;

        // TR 수신 이벤트를 외부(TradeManager 등)에서 구독할 수 있도록
        public event Action<string, string> OnReceiveTrDataEvent;

        // 로그인 상태 변화를 외부에 알리기 위한 이벤트
        public event Action<bool> OnLoginStatusChanged;

        internal Context()
        {
            InitializeComponent();
            RegisterEvents();
        }

        private void InitializeComponent()
        {
            this.axApi = new AxKHOpenAPI();
            ((System.ComponentModel.ISupportInitialize)(this.axApi)).BeginInit();
            // axApi를 이 컨트롤에 추가
            this.Controls.Add(this.axApi);
            ((System.ComponentModel.ISupportInitialize)(this.axApi)).EndInit();
        }

        /// <summary>
        /// Kiwoom OpenAPI에서 발생하는 주요 이벤트를 등록
        /// </summary>
        private void RegisterEvents()
        {
            this.axApi.OnEventConnect += AxApi_OnEventConnect;
            this.axApi.OnReceiveTrData += AxApi_OnReceiveTrData;
        }

        /// <summary>
        /// 로그인 이벤트 (성공/실패)를 처리하는 핸들러
        /// </summary>
        private void AxApi_OnEventConnect(object sender, _DKHOpenAPIEvents_OnEventConnectEvent e)
        {
            bool isLoggedIn = (e.nErrCode == 0);
            // 로그인 상태를 알리는 이벤트
            OnLoginStatusChanged?.Invoke(isLoggedIn);
        }

        /// <summary>
        /// TR 이벤트를 받아서 외부로 전달하는 핸들러
        /// </summary>
        private void AxApi_OnReceiveTrData(object sender, _DKHOpenAPIEvents_OnReceiveTrDataEvent e)
        {
            // 예: 예수금 데이터를 가져오기 (실제 개발 시에는 e.sTrCode, e.sRQName에 따라 다르게 처리)
            string result = axApi.GetCommData(e.sTrCode, e.sRQName, 0, "예수금");
            OnReceiveTrDataEvent?.Invoke(e.sRQName, result);
        }
    }
}

핵심 포인트

  1. **AxKHOpenAPI**가 WinForms 컨트롤이므로, 일반 클래스가 아닌 Control을 상속한 내부 클래스로 생성.
  2. 싱글턴을 통해 전역 어디서나 동일한 AxKHOpenAPI를 사용할 수 있음.
  3. 이벤트(OnReceiveTrData, OnEventConnect) 등을 등록해두고, 필요한 데이터를 외부로 전달할 수 있도록 이벤트를 발행.

5. 매매 로직 - TradeManager

이제 실제 매매 로직을 처리하는 TradeManager를 구현합니다.

  • 로그인/로그아웃 관리
  • 계좌 조회
  • 현재가 조회
  • 주문 등의 로직을 담당하며,
  • APIContext에서 발행되는 이벤트(OnReceiveTrDataEvent, OnLoginStatusChanged)를 연결하여 UI에 전달합니다.
using System;
using AxKHOpenAPILib;

public class TradeManager
{
    private readonly AxKHOpenAPI _api;
    private string _accountNumber;

    // TR 데이터 수신 시 외부(MainForm 등)로 전달해줄 이벤트
    public event Action<string, string> OnDataReceived;

    // 로그인 상태가 바뀔 때 알릴 이벤트
    public event Action<bool> OnLoginStatusChanged;

    public TradeManager()
    {
        // 싱글턴으로 관리되는 OpenAPI 인스턴스를 가져옴
        _api = APIContext.Get;

        // APIContext에서 발행하는 이벤트를 이쪽에서 수신하고, 다시 외부로 전달
        APIContext.Get.OnReceiveTrDataEvent += HandleTrData;
        APIContext.Get.OnLoginStatusChanged += status => OnLoginStatusChanged?.Invoke(status);
    }

    /// <summary>
    /// 로그인 실행
    /// </summary>
    public void Login() => _api.CommConnect();

    /// <summary>
    /// 현재 로그인 상태 확인 (1 = 로그인됨, 0 = 로그아웃됨)
    /// </summary>
    public bool IsLoggedIn() => (_api.GetConnectState() == 1);

    /// <summary>
    /// 계좌 정보를 조회 (예: 예수금)
    /// </summary>
    public void GetAccountInfo()
    {
        // 로그인한 뒤에 얻은 계좌번호 가져오기
        _accountNumber = _api.GetLoginInfo("ACCNO").Split(';')[0];

        // TR 조회를 위한 InputValue 설정 (예: OPW00001, 예수금 조회)
        _api.SetInputValue("계좌번호", _accountNumber);
        _api.CommRqData("계좌조회", "OPW00001", 0, "1001");
    }

    /// <summary>
    /// 현재가 조회 (OPT10001 예: 현재가, 시가총액 등 기본정보)
    /// </summary>
    public void RequestCurrentPrice(string stockCode)
    {
        _api.SetInputValue("종목코드", stockCode);
        _api.CommRqData("현재가조회", "OPT10001", 0, "1002");
    }

    /// <summary>
    /// 매수 혹은 매도 주문
    /// </summary>
    /// <param name=\"stockCode\">종목코드</param>
    /// <param name=\"quantity\">수량</param>
    /// <param name=\"price\">가격</param>
    /// <param name=\"orderType\">1=매수, 2=매도</param>
    public void PlaceOrder(string stockCode, int quantity, int price, int orderType)
    {
        int tradeType = (orderType == 1) ? 1 : 2;
        _api.SendOrder(\"주문\", \"0101\", _accountNumber, tradeType, stockCode, quantity, price, \"00\", \"\");
    }

    /// <summary>
    /// APIContext.OnReceiveTrDataEvent 이벤트를 처리하여
    /// 필요한 정보를 외부로 전달
    /// </summary>
    private void HandleTrData(string rqName, string result)
    {
        // MainForm 등에 있는 OnDataReceived를 호출하여 결과 전달
        OnDataReceived?.Invoke(rqName, result);
    }
}

매매 로직 요약

  • Login() : OpenAPI의 CommConnect()를 호출하여 로그인 창 출력
  • IsLoggedIn() : 로그인 상태(0/1) 반환
  • GetAccountInfo() : 계좌번호 및 예수금 조회
  • RequestCurrentPrice() : 종목코드에 대한 현재가 조회
  • PlaceOrder() : 매수/매도 주문 발행

이처럼 TradeManager는 실제 TR 요청이나 주문 처리 로직을 담당합니다.


6. 메인 폼 (UI) - MainForm

WinForms 기반의 메인 폼에서는 TradeManager를 활용하여 로그인/계좌 조회/주문 등을 수행하고, 이벤트를 통해 전달되는 정보를 UI에 반영합니다.

using System;
using System.Windows.Forms;

public partial class MainForm : Form
{
    private readonly TradeManager _tradeManager;

    public MainForm()
    {
        InitializeComponent();

        // TradeManager 생성
        _tradeManager = new TradeManager();

        // 이벤트 구독
        _tradeManager.OnDataReceived += UpdateUI;
        _tradeManager.OnLoginStatusChanged += UpdateLoginStatus;
    }

    // 폼 로드 시 로그인 시도 & 로그인 상태 표시
    private void MainForm_Load(object sender, EventArgs e)
    {
        _tradeManager.Login();
        UpdateLoginStatus(_tradeManager.IsLoggedIn());
    }

    // 로그인 상태 갱신
    private void UpdateLoginStatus(bool isLoggedIn)
    {
        lblLoginStatus.Text = isLoggedIn ? "로그인 상태: 연결됨" : "로그인 상태: 연결되지 않음";
    }

    // TR 결과를 받아와서 UI에 반영
    private void UpdateUI(string rqName, string result)
    {
        if (rqName == "계좌조회")
        {
            lblAccountInfo.Text = $"예수금: {result}";
        }
        else if (rqName == "현재가조회")
        {
            lblCurrentPrice.Text = $"현재가: {result}";
        }
        // 필요한 경우 추가 분기 처리
    }

    // 버튼 예시 (계좌조회 버튼 클릭)
    private void btnGetAccount_Click(object sender, EventArgs e)
    {
        _tradeManager.GetAccountInfo();
    }

    // 종목코드 입력 후 현재가 조회 버튼
    private void btnRequestPrice_Click(object sender, EventArgs e)
    {
        _tradeManager.RequestCurrentPrice(txtStockCode.Text);
    }

    // 매수 버튼
    private void btnBuy_Click(object sender, EventArgs e)
    {
        int.TryParse(txtQuantity.Text, out int qty);
        int.TryParse(txtPrice.Text, out int price);
        _tradeManager.PlaceOrder(txtStockCode.Text, qty, price, 1);  // 1 = 매수
    }

    // 매도 버튼
    private void btnSell_Click(object sender, EventArgs e)
    {
        int.TryParse(txtQuantity.Text, out int qty);
        int.TryParse(txtPrice.Text, out int price);
        _tradeManager.PlaceOrder(txtStockCode.Text, qty, price, 2);  // 2 = 매도
    }
}

여기서는 MainForm의 partial class를 표시했습니다. 버튼 클릭 핸들러(btnGetAccount_Click 등) 안에서 TradeManager의 메서드를 호출하여 TR 요청을 수행하고, 결과는 UpdateUI()를 통해 라벨 등에 표시합니다.


7. 메인 폼 디자인 - MainForm.Designer.cs

아래는 WinForms 디자이너가 자동 생성하는 부분이지만, 예시로 직접 작성할 수 있습니다.
컨트롤 선언위치 설정을 통해 로그인 상태, 종목코드, 수량, 가격, 현재가, 예수금 등을 편리하게 확인할 수 있도록 만듭니다.

partial class MainForm
{
    private System.ComponentModel.IContainer components = null;

    // UI 컨트롤 선언
    private System.Windows.Forms.Button btnGetAccount;
    private System.Windows.Forms.Button btnRequestPrice;
    private System.Windows.Forms.Button btnBuy;
    private System.Windows.Forms.Button btnSell;
    private System.Windows.Forms.Label lblLoginStatus;
    private System.Windows.Forms.Label lblAccountInfo;
    private System.Windows.Forms.Label lblCurrentPrice;
    private System.Windows.Forms.TextBox txtStockCode;
    private System.Windows.Forms.TextBox txtQuantity;
    private System.Windows.Forms.TextBox txtPrice;

    // Form Dispose
    protected override void Dispose(bool disposing)
    {
        if (disposing && (components != null))
        {
            components.Dispose();
        }
        base.Dispose(disposing);
    }

    private void InitializeComponent()
    {
        this.btnGetAccount = new System.Windows.Forms.Button();
        this.btnRequestPrice = new System.Windows.Forms.Button();
        this.btnBuy = new System.Windows.Forms.Button();
        this.btnSell = new System.Windows.Forms.Button();
        this.lblLoginStatus = new System.Windows.Forms.Label();
        this.lblAccountInfo = new System.Windows.Forms.Label();
        this.lblCurrentPrice = new System.Windows.Forms.Label();
        this.txtStockCode = new System.Windows.Forms.TextBox();
        this.txtQuantity = new System.Windows.Forms.TextBox();
        this.txtPrice = new System.Windows.Forms.TextBox();
        this.SuspendLayout();

        // btnGetAccount
        this.btnGetAccount.Location = new System.Drawing.Point(20, 60);
        this.btnGetAccount.Name = "btnGetAccount";
        this.btnGetAccount.Size = new System.Drawing.Size(100, 30);
        this.btnGetAccount.Text = "계좌 조회";
        this.btnGetAccount.Click += new System.EventHandler(this.btnGetAccount_Click);

        // btnRequestPrice
        this.btnRequestPrice.Location = new System.Drawing.Point(20, 100);
        this.btnRequestPrice.Name = "btnRequestPrice";
        this.btnRequestPrice.Size = new System.Drawing.Size(100, 30);
        this.btnRequestPrice.Text = "현재가 조회";
        this.btnRequestPrice.Click += new System.EventHandler(this.btnRequestPrice_Click);

        // btnBuy
        this.btnBuy.Location = new System.Drawing.Point(20, 180);
        this.btnBuy.Name = "btnBuy";
        this.btnBuy.Size = new System.Drawing.Size(100, 30);
        this.btnBuy.Text = "매수";
        this.btnBuy.Click += new System.EventHandler(this.btnBuy_Click);

        // btnSell
        this.btnSell.Location = new System.Drawing.Point(130, 180);
        this.btnSell.Name = "btnSell";
        this.btnSell.Size = new System.Drawing.Size(100, 30);
        this.btnSell.Text = "매도";
        this.btnSell.Click += new System.EventHandler(this.btnSell_Click);

        // lblLoginStatus
        this.lblLoginStatus.Location = new System.Drawing.Point(20, 20);
        this.lblLoginStatus.Name = "lblLoginStatus";
        this.lblLoginStatus.Size = new System.Drawing.Size(200, 25);
        this.lblLoginStatus.Text = "로그인 상태: -";

        // lblAccountInfo
        this.lblAccountInfo.Location = new System.Drawing.Point(20, 140);
        this.lblAccountInfo.Name = "lblAccountInfo";
        this.lblAccountInfo.Size = new System.Drawing.Size(300, 25);
        this.lblAccountInfo.Text = "예수금: -";

        // lblCurrentPrice
        this.lblCurrentPrice.Location = new System.Drawing.Point(140, 100);
        this.lblCurrentPrice.Name = "lblCurrentPrice";
        this.lblCurrentPrice.Size = new System.Drawing.Size(300, 25);
        this.lblCurrentPrice.Text = "현재가: -";

        // txtStockCode
        this.txtStockCode.Location = new System.Drawing.Point(140, 60);
        this.txtStockCode.Name = "txtStockCode";
        this.txtStockCode.Size = new System.Drawing.Size(100, 25);
        this.txtStockCode.Text = "종목코드";

        // txtQuantity
        this.txtQuantity.Location = new System.Drawing.Point(250, 60);
        this.txtQuantity.Name = "txtQuantity";
        this.txtQuantity.Size = new System.Drawing.Size(70, 25);
        this.txtQuantity.Text = "수량";

        // txtPrice
        this.txtPrice.Location = new System.Drawing.Point(330, 60);
        this.txtPrice.Name = "txtPrice";
        this.txtPrice.Size = new System.Drawing.Size(70, 25);
        this.txtPrice.Text = "가격";

        // MainForm
        this.ClientSize = new System.Drawing.Size(420, 240);
        this.Controls.Add(this.btnGetAccount);
        this.Controls.Add(this.btnRequestPrice);
        this.Controls.Add(this.btnBuy);
        this.Controls.Add(this.btnSell);
        this.Controls.Add(this.lblLoginStatus);
        this.Controls.Add(this.lblAccountInfo);
        this.Controls.Add(this.lblCurrentPrice);
        this.Controls.Add(this.txtStockCode);
        this.Controls.Add(this.txtQuantity);
        this.Controls.Add(this.txtPrice);
        this.Name = "MainForm";
        this.Text = "Kiwoom Trader";
        this.Load += new System.EventHandler(this.MainForm_Load);
        this.ResumeLayout(false);
        this.PerformLayout();
    }
}
  • lblLoginStatus: 현재 로그인 상태 표시
  • lblAccountInfo: 예수금 등 계좌 정보 표시
  • lblCurrentPrice: 종목의 현재가 표시
  • txtStockCode, txtQuantity, txtPrice: 주문 시 필요한 종목코드, 수량, 가격 입력
  • btnGetAccount, btnRequestPrice, btnBuy, btnSell: 각각의 기능을 수행하는 버튼

8. 마무리 및 확장 아이디어

이로써 Kiwoom OpenAPI를 기반으로 하는 자동 주식 매매 프로그램전체 코드가 완성되었습니다.

✅ 작동 방식 요약

  1. SingletonBase: 어디서든지 하나의 인스턴스만을 공유하는 싱글턴 패턴 구현
  2. APIContext: 싱글턴을 통해 관리되는 AxKHOpenAPI 컨트롤 생성 및 이벤트 등록
  3. TradeManager: 로그인/계좌조회/주문 등의 기능을 담당, 이벤트를 통해 MainForm으로 결과 전달
  4. MainForm: UI 이벤트를 TradeManager에 연결, TR 결과를 라벨 등에 표시

✅ 추가 개발 가능성

  1. 자동 매매 로직: 특정 지표(RSI, MACD 등)를 이용한 진입/청산 로직 구현
  2. 실시간 체결 정보: OnReceiveRealData, OnReceiveChejanData 이벤트를 활용하여 실시간 체결/잔고 갱신
  3. DB 연동: 매매 이력, 체결 정보 등을 데이터베이스에 저장
  4. 타 UI 프레임워크: 이 코드는 WinForms 기반이지만, WPF나 다른 UI에서 WinForms Host를 사용해 구현 가능

9. 결론

위 코드를 통해 로그인 -> 계좌 조회 -> 현재가 조회 -> 주문 실행이 가능한 기본적인 주식 매매 프로그램을 완성할 수 있습니다. 로그인 여부는 lblLoginStatus로 실시간 확인 가능하며, TR 데이터는 TradeManager와 MainForm 간 이벤트를 통해 화면에 표시됩니다.

Kiwoom OpenAPI는 추가적으로 TR 종류가 매우 다양하므로, 필요한 TR 요청(차트, 호가, 체결, 주문)에 맞게 SetInputValue / CommRqData를 사용해 구현을 확장할 수 있습니다. 또한, 실시간 데이터 처리를 위해서는 추가적인 이벤트(OnReceiveRealData 등)를 등록해야 합니다.

자동 매매실시간 대응 로직을 포함하면 완전 자동 매매 시스템으로 발전시킬 수 있으며, 이를 통해 더 풍부하고 강력한 투자 도구를 구축할 수 있습니다.


이상으로 Kiwoom 주식 매매 프로그램전체 코드를 빠짐없이 살펴보았습니다.

프로젝트에 바로 적용하시거나, 자동 매매 시스템을 확장하는 데 활용하시길 바랍니다.

추가 질문이나 수정 요청 사항이 있으시면 언제든지 댓글로 알려주세요! 감사합니다.