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);
}
}
}
핵심 포인트
- **AxKHOpenAPI**가 WinForms 컨트롤이므로, 일반 클래스가 아닌 Control을 상속한 내부 클래스로 생성.
- 싱글턴을 통해 전역 어디서나 동일한 AxKHOpenAPI를 사용할 수 있음.
- 이벤트(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를 기반으로 하는 자동 주식 매매 프로그램의 전체 코드가 완성되었습니다.
✅ 작동 방식 요약
- SingletonBase: 어디서든지 하나의 인스턴스만을 공유하는 싱글턴 패턴 구현
- APIContext: 싱글턴을 통해 관리되는 AxKHOpenAPI 컨트롤 생성 및 이벤트 등록
- TradeManager: 로그인/계좌조회/주문 등의 기능을 담당, 이벤트를 통해 MainForm으로 결과 전달
- MainForm: UI 이벤트를 TradeManager에 연결, TR 결과를 라벨 등에 표시
✅ 추가 개발 가능성
- 자동 매매 로직: 특정 지표(RSI, MACD 등)를 이용한 진입/청산 로직 구현
- 실시간 체결 정보: OnReceiveRealData, OnReceiveChejanData 이벤트를 활용하여 실시간 체결/잔고 갱신
- DB 연동: 매매 이력, 체결 정보 등을 데이터베이스에 저장
- 타 UI 프레임워크: 이 코드는 WinForms 기반이지만, WPF나 다른 UI에서 WinForms Host를 사용해 구현 가능
9. 결론
위 코드를 통해 로그인 -> 계좌 조회 -> 현재가 조회 -> 주문 실행이 가능한 기본적인 주식 매매 프로그램을 완성할 수 있습니다. 로그인 여부는 lblLoginStatus로 실시간 확인 가능하며, TR 데이터는 TradeManager와 MainForm 간 이벤트를 통해 화면에 표시됩니다.
Kiwoom OpenAPI는 추가적으로 TR 종류가 매우 다양하므로, 필요한 TR 요청(차트, 호가, 체결, 주문)에 맞게 SetInputValue / CommRqData를 사용해 구현을 확장할 수 있습니다. 또한, 실시간 데이터 처리를 위해서는 추가적인 이벤트(OnReceiveRealData 등)를 등록해야 합니다.
자동 매매나 실시간 대응 로직을 포함하면 완전 자동 매매 시스템으로 발전시킬 수 있으며, 이를 통해 더 풍부하고 강력한 투자 도구를 구축할 수 있습니다.
이상으로 Kiwoom 주식 매매 프로그램의 전체 코드를 빠짐없이 살펴보았습니다.
프로젝트에 바로 적용하시거나, 자동 매매 시스템을 확장하는 데 활용하시길 바랍니다.
추가 질문이나 수정 요청 사항이 있으시면 언제든지 댓글로 알려주세요! 감사합니다.
'IT개발' 카테고리의 다른 글
한영 타자 변환기 (0) | 2025.03.10 |
---|---|
키움증권 Open API를 활용하여 갭 매매 전략 구현- C# (0) | 2025.03.10 |
C# 10 & C# 11 주요 업데이트 (0) | 2025.03.06 |
C#에서 비동기 프로그래밍과 병렬 처리 (0) | 2025.03.06 |
C#에서의 메모리 관리 기술 (0) | 2025.03.06 |