두번째 L-ATS 2.0 사용자 화면의 Focus는 '매매 처리중인 종목의 표시' 부분이었다.
세번째 L-ATS 3.0 사용자 화면의 핵심은 이것이다.
캡쳐된 화면 표시와 OCR 처리된 문자열
L-ATS 2.0에서 Flexible 스탑로스를 통한 매도의 자동화를 완성한 상태에서
L-ATS 3.0에서는 매수 행위의 자동화를 구현하여 매매의 완전 자동화 를 완성하는 것이 서비스 기획의 핵심 부분이다.
그렇기 때문에 사용자가 보기 편하도록 캡쳐된 화면과 해당 이미지의 OCR 결과를 실시간으로 표시하는 부분에 Focus를 맞췄다.
3.0 버전의 핵심 기능 부분을 부각시키는 것이 사용자 관점에서 서비스의 장점을 직접적으로 잘 나타낼 수 있기 때문에 그때 그때 이미지 캡쳐한 화면과 OCR 처리된 내용을 즉각적으로 표시해줌으로써 사용자의 만족감을 더 높여 줄수 있다.
① 메인 메뉴
메뉴의 구조는 [HTS 로그인], [시스템 설정], [프로그램 종료]로 구성된다.
HTS 로그인 : 이베스트 증권에 로그인할 수 있도록 기능을 제공한다.
시스템 설정 : 메인 화면에는 보이지 않는 시스템적으로 Critical한 값(예; OCR 엔진 정보, 예외처리 문자열, 문자열 파싱 패턴 등)들을 설정할 수 있는 화면을 제공한다.
프로그램 종료 : 시스템의 안정적인 운영을 위해 비정상 종료 Case를 없애고 이 버튼으로만 종료될 수 있도록 했다.
② 상태 확인
HTS 서버 연동 : 증권사(이베스트 증권) API를 통해 HTS와 연동 상태 표시
서버 연결 : LINUX Server의 L-ATS Main Server와 연동 상태 표시
③ 프로그램 조작
캡쳐 영역 [Button]: 캡쳐할 화면의 영역을 설정할 수 있는 투명한 Form 생성
시작 [Button] : 사용자 설정 값으로 자동 매매를 시작하고 정지하는 버튼
캡쳐타임(초) : 실시간으로 화면을 캡쳐할 시간 간격
[ 캡쳐 타임이 필요했던 이유 ] 자동매매의 최초 서비스 기획의 핵심은 '누군가' 실시간으로 특정 종목을 언급해 주는 조건이 되어야 하는 것이었다. 근데 과연 누가...? 그렇게 찾고 찾아 선택한 것이 카카오톡과 텔레그램의 일명 뉴스방이었다. 무료로 불특정 다수에게 특정 종목의 당일 Good/Bad 뉴스를 실시간으로(?) 알려주는 고마운 곳. 하지만 이 방들은 소프트웨어적으로 개인이 메시지를 받을 수 없었다. (캡쳐 + OCR 방식을 선택한 이유.) 때문에 이 뉴스방에 새로운 뉴스가 올라온 것을 알 수 있는 방법이 없다. 그래서 일정 간격으로 화면을 지속적으로 캡쳐해서 OCR 결과를 처리하는 방식으로 처리했다. (OCR 기술이 많이 발전했듯 이런것도 해결이 가능한 좋은 방향의 방법이 곧 생기길...)
스탑로스(%) : 자동 매도를 위한 손실률 설정
④ 자동매수 설정
보유계좌 : 자동 매매에 사용할 사용자의 증권사 계좌
매수금 : 자동으로 매수할 경우 1회 매수가
1회 매수가가 10만원일 경우, 1주당 10만원 이상인 종목은 매수하지 않는다. 1주당 10만원 이하인 종목은 10만원 이내에서 최대 매수량을 계산하여 매수한다. 예) 1주당 9천원인 종목이면 11주 매수
⑤ 잔고조회
자산총액 : 현재 선택된 사용자 증권사 계좌의 잔고 확인
예수금 : 현재 선택된 사용자 증권사 계좌의 예수금 확인
⑥ 캡쳐 이미지
캡쳐 영역으로 선택된 화면의 캡쳐된 이미지를 표시
⑦ OCR 결과
캡쳐된 이미지의 OCR 변환 결과를 표시
⑧ 실시간 잔고
매수가 완료되서 현재 계좌에 보유하고 있는 종목 리스트 표시
표시되는 종목 리스트들은 실시간으로 현재가를 갱신하여 표시하고 스탑로스를 계산
⑨ 거래 완료
당일 거래 완료된 종목 리스트 표시
정말 이베스트 증권의 거래 완료 종목 표시를 위한 API는 최악이다. HTS나 MTS에서 확인할 수 있는 '잔고 조회'의 화면에서 조회되는 값들과 동일한 값들을 표시하기 위해 정말 많은 시행착오를 겼었다. 이 내용은 따로 포스팅 예정이다.
⑩ 종목 추출 결과
OCR 결과에서 패턴 분석으로 종목명을 추출하고 LINUX Server program으로 전달하여 매수를 위한 해당 종목의 Code 정보를 요청/수신하여 표시
⑪ 실시간 건수
현재 계좌에 보유하고 있는 종목의 갯수와 당일 거래 완료된 종목의 갯수 표시
⑫ 거래 완료
당일 거래 완료된 종목들의 금액 정보 표시
⑬ 접속 정보
사용자의 증권사 계정 접속 정보 표시
L-ATS 시스템이 버전업 되면서 UX/UI는 일부분들이 계승되어 구성되는것 같다.
아무래서 버전 마다 기획하고 설계하고 구현하고 운영하다보면 배우는 것이 있기때문에 버릴건 버리고, 활용할 건 활용하게 되는것 같다.
따라서 나는 분명 Child Form을 닫는 this.Close()를 호출하였지만 Form이 닫히는 과정(Form Closing)에서 Form이 닫히는 것을 취소하고 Form을 안보이도록 숨기겠다(Hide)는 뜻이다.
여기서 중요한 점은.
Form을 사용자가 보지 못하게끔 숨기기만 하는 것으로 (사용자 입장에서는 Form이 꺼진것처럼 보이지만...)
이전에 Child Form에 입력한 값이 그대로 보인다는 것이다. Form을 숨기기만 하고 다시 보여주는 것이니 당연한 결과다.
프로그램의 설정값을 입력하는 목적의 Child Form을 만들 경우 사용자 입장에서는 이전에 입력한 값이 그대로 보여지니 이 방법이 내가 설계한 프로그램에는 가장 적합한 해결책인 것이다.
만약 Form을 삭제하고 다시 객체를 생성해서 Child Form을 띄울 경우 기존에 입력했던 값들을 다시 Parent Form에서 받아 오든, 아니면 특정 위치에 저장을 해서 불러오든 해야하기에 구현의 까다로움이 존재하므로 굳이 이 방법을 사용하지 않아도 좋은 프로그램을 만들 수 있다.
자동 매매 프로그램과 텔레그램 봇의 조합으로 텔레그램에서 종목명만 입력하면 설정된 조건으로 알아서 매수해 주는 실시간 매수 자동화
가장 빠르게(!) 매수하는게 주 목적으로 매매를 위한 종목 클릭, 계좌 비번 입력, 매수 수량 입력, 매수 버튼 클릭 등의 작업을 없앤 초 간단 매수
상승하는 종목의 최대 수익을 낼 수 있도록 현재가에 맞춰 유동적으로 변화하는 매도 기준의 실시간 변화
손실을 최소화 할 수 있는 철저한 자동 손절
L-ATS 1.0은 사실상 실패한 프로젝트였다. 실패라기 보단 매매의 목적(=수익)을 달성하기에 큰 단점이 있었다.
제대로된 검색식
L-ATS 1.0은 위에 기술했듯이 키움증권의 검색식을 통해 추출된 종목을 즉시 매매하는 방식으로 차트 기반의 수익을 만들어내는 제대로된 검색식을 필요로 했기 때문이다.
아는 사람은 알겠지만...수익을 만들어 내는 제대로된 검색식을 만들기란 정말 쉽지않다. 제대로된 검색식을 누구나 만들 수 있다면 모든 사람이 부자가 됐겠지...(나 역시도 반년간 검색식에 매달렸지만....)
L-ATS 1.0은 이렇게 약 7개월 정도 운용되다가 폐기되었다.
그렇게 시간이 흘러 2019년.
친구의 권유로 카카x톡 찌라시 매매방에 들어가게 되었고, 약 1년여의 시간동안 관찰해온 찌라시 매매방의 특징으로 나만의 독창적인 매도 방식을 결합하여 L-ATS 2.0을 설계하게 되었다.
[ 찌라시 매매방에서 찾아낸 특징 ]
찌라시 매매방은 진짜 뉴스와 가짜 뉴스가 섞여 나온다.
찌라시 매매방의 목적 상(= 뉴스 띄우고 개미들이 들러붙어 1~2% 오를때 물량 넘기기 수법) 진짜 뉴스든 가짜 뉴스든 대부분 어느 순간 일정 수준의 상승을 보인다.
높은 확률로 상한가 직행 종목이 나온다. (=진짜뉴스)
결론 : 가지 않는 종목을 빠른 손절로 손실을 최소화 하고 상한가 가능 종목 1개만 잡아도 평균적으로 수익이다.
[ 자동 손절로 최대 수익을 낼 수 있는 방법의 추론]
주식판의 모든 종목은 파형을 그리면서 움직인다.
세력이 들어와 급등하는 종목도 개미를 떨구거나 더 많은 매물을 모아서 올리기 위해 매량 매도를 통해 순간 가격을 내린다.
종목의 최고가가 갱신될 때마다 해당 값을 기준으로 일정 수준의 손절 라인을 실시간으로 변경하면서 가져간다면 자동매매로써의 최대 수익을 낼 수 있는 하나의 방법이 된다. (= 내 생각)
절대적 손절 라인을 -5%로 지정했다고 가정해 보자. 내가 1000원에 어떤 종목을 샀으면 그 시점에 손절 라인은 1000원에서 -5%인 950원이다. 이때 종목이 1000원에서 1100원이 되면 그 시점에 손절 라인은 1045원이 된다. 이런 식으로 손절 라인을 매 시점의 최고가에 맞게 높여 나간다면 1000원에서 급등했다가 다시 1000원으로 내려와도 무조건 수익이 생기게 된다. 또한, 급등하는 중간 중간에 세력이 개미를 떨구기 위해 호가를 내리는 경우 설정된 -5% 이내에 들어오면 손절되지 않고 상승 파형을 따라 끝까지 가져갈 수 있는 좋은 조건이 된다. 이것은 고정된 금액. 또는 고정된 수익/손절 %에서 자동 청산 기능을 제공하는 많은 증권사들의 기능과는 확실히 더 나은 차별점이 있다.
이렇게 탄생한 L-ATS 2.0은 약 2년간 기능 업데이트 및 버그 수정을 거듭해가며 나의 증권 계좌를 운용해 주었다.
L-ATS 2.0은 찌라시 매매방에서 언급한 종목을 무조건 사는 방식을 채택하였기에
매수에 한해서는 완전 자동화를 할 수가 없었다.
때문에, 찌라시 매매방에 종목이 올라온 것을 캐치한 후 미리켜둔 증권 앱에서 종목을 검색하여 수동으로 매수를 하였다.
여기서 L-ATS 2.0의 단점이 발생했다.
바로 매수가 느리다는 것.
찌라시 매매방에 올라오는 종목은 말그대로 찌라시에 의한 급등 종목들이기 때문에 빠른 매수가 수익과 직결된다.
하지만 수동 매수로 인해 증권사 앱에 접속하는 시간, 종목을 검색하는 시간, 계좌 비밀번호를 입력하는 시간, 매수 가격 및 매수금액 입력 시간 등..
이를 보완하기 위해 텔레그램 봇을 이용하여 L-ATS 2.0 프로그램과 연동을 통해 수동 매수의 한계점을 극복하였으나
이것 역시 찌라시 매매방의 종목을 내가 인지한 후에 매수가 가능했기에 타이밍을 놓치거나 아예 보지 못해 매수를 하지 못하는 단점들이 생겨났다.
그래서 결론은, 역시 완전 자동화!!
L-ATS 2.0은 약 7개월 전, 결국 전체 수익률 약 -3% 수준을 기록하고 멈추었다.
그래도 반자동 매매 치고는 매우 괜찮은 수준이라고 생각한다.
최근 애플에서 iOS 업데이트를 하면서 카메라 기능 중 OCR 기능을 기본 탑재하면서 전 세계적으로 인공지능 학습에 의한 OCR 정확도가 평균적으로 매우 상승하였음을 보여주었다. (실제로 써보면 거의 100% 정확하다.)
L-ATS 2.0에서 완성하지 못했던 완전 자동화를 이 인공지능 OCR 기능을 통해 구현해 보고자
다시 Visual Studio를 열게 되었다. (지금 내가 인공지능 회사에 재직 중이니까...)
2023년 L-ATS 3.0
L-ATS 3.0은 찌라시 매매방의 채팅 창을 일정 시간 간격으로 캡쳐하여 TEXT만 OCR로 변환 후
종목명을 추출하여 즉시 매수 처리하는 방식으로 구현될 것이다.
이 과정에서도 분명 문제점은 있다.
하지만 완전 자동화가 되는 프로그램을 만드는 것이 우선순위이기 때문에 이슈들은 일단 뒤로 제껴둘 생각이다.
사실, 바빠서 포스팅을 못했을 뿐 계획했던 자동 매매 프로그램은 그 당시 완성하여 (내가 당장 필요한 핵심 기능만 구현하였지만...) 계속적으로 사용하고 있었다.
당시 내가 필요했던 기능은 종목 매수는 직접하고, 설정해 놓은 StopLoss 기준에 부합할때 자동으로 매도 처리하는 프로그램이었다.
사실 이 프로그램은 찌라시방 용도로 사용하기 위해 설계되었는데 갑자기 급등하는 종목에 대해 최대한의 수익을 끌어내기 위함이었다. 때문에 당일 매시점 갱신되는 최고가 기준으로 StopLoss 기준이 같이 변하도록(Flexible) 해두었기에 'Flex'란 이름을 붙여 만들었다.
DatagridView의 데이터 표시는 이전 예제 프로그램 위에 다시 구현해 보도록 하자.
VIEW 테이블 만들기
먼저 [도구상자]에서 [DataGridView]를 선택하여 화면안에 넣고 원하는 크기로 맞춰 준다.
생성된 DataGridView를 선택하고 오른쪽 하단의 [속성]에 [Columns] 항목 오른쪽 끝의 '...'을 클릭하여 [열 편집] 화면을 띄운다.
속성에 [DataSource] 항목을 통해 프로젝트 내의 특정 데이터 집합을 바로 바인딩하여 표현할 수 있는 것 같은데 이것을 시도하다 번번히 실패하여 하나하나 컬럼을 만들어 진행했다. (나는 C#을 이번에 처음 해보는 10년차 개발자이므로....)
[열 편집] - [열 추가]를 선택하여 표시하고자 하는 데이터의 컬럼을 생성해준다.
당연한 얘기지만 프로그램 내에서 표시하기 위한 위치와 실제 데이터 값의 매칭이 필요한데 DataPropertyName이 그에 해당하는 값이다.
아래 Code라고 지정해 놓은 값은 이후에 만들 잔고 데이터 클래스의 '종목코드'를 저장하는 변수명과 일치시킬 것이다.
속성에 아랫부분으로 내려가면 HeaderText 라는 것이 있는데 이곳에 표시하고 싶은 이름을 입력하면 된다.
그리고 각각의 열에 대해서도 특정 속성을 부여할 수 있다. 예를 들면, 금액일 경우 통화 형태로, 백분율 값일 경우 소수점 자리수 표시 형태로 말이다.
최초 만들어진 DataGridView의 모습은 이렇다.
DataGridView의 속성에서 가장 앞부분의 무의미한 Column을 숨길 수 있고, 위에서 열 편집했던 창에서 각각의 열에 대해 너비 조정이 가능하므로 본인의 입맛에 맞게 모양을 수정한다.
한 화면에 표시되도록 각각의 열에 대해 너비를 조정하였다.
(오른쪽 끝에 일부 공간을 남겨둔건 항목이 많으면 스크롤 바가 생기기 때문.)
VIEW 테이블과 연결할 데이터 구조체(Class) 만들기
화면에 보여줄 VIEW는 준비가 되었으니 이제 VIEW에 Binding할 데이터 구조를 생성해야 한다.
데이터는 [클래스 리스트]를 사용했다
먼저 데이터 클래스(class)를 먼저 만들자.
Balance_List.cs
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace xingAPI_Test
{
class Balance_List
{
public string Code { get; set; } // 종목코드
public string CodeName { get; set; } // 종목명
public decimal CurPrice { get; set; } // 현재가
public decimal PurchPrice { get; set; } // 매입가
public int BalCount { get; set; } // 잔고수량
public decimal PurchAmount { get; set; } // 매입금액
public decimal EvalAmount { get; set; } // 평가금액
public decimal EvalPrice { get; set; } // 평가손익
public double EvalRate { get; set; } // 손익률
public Balance_List(string Code, string CodeName, decimal CurPrice, decimal PurchPrice, int BalCount, decimal PurchAmount, decimal EvalAmount, decimal EvalPrice, double EvalRate)
{
this.Code = Code;
this.CodeName = CodeName;
this.CurPrice = CurPrice;
this.PurchPrice = PurchPrice;
this.BalCount = BalCount;
this.PurchAmount = PurchAmount;
this.EvalAmount = EvalAmount;
this.EvalPrice = EvalPrice;
this.EvalRate = EvalRate;
}
}
}
이제 한 종목에 대한 정보를 담이 이 클래스를 리스트로 저장할 데이터 공간을 만들어 준다.
...
List<Balance_List> gBalanceList = new List<Balance_List>(); // 잔고 클래스 리스트
...
gBalanceList라는 글로벌 변수가 Balance_List라는 하나하나의 객체를 갖는 리스트로 선언되었다.
솔직히 나는 C#을 다루던 개발자가 아니라 이와 같은 기능을 개발할 때 어떤 방식이 가장 최적화된 방식인지 알지 못한다. 다만, C/C++ 개발자였던 경험으로 데이터 구조체가 가장 다루기 쉽기때문에(본인에게...) 클래서 리스트 방식을 선택했다.
클래스 리스트에 데이터를 넣을때는 아래와 같이 객체를 새로 생성하여 리스트에 ADD해 준다.
여기서는 잔고조회 TR을 통해 가져온 결과 데이터 한줄 한줄을 만들때 마다 같이 리스트에 ADD해 준다.
위의 단계가 완료된 이후부터 종목을 검색하고, 내가 가진 종목 확인을 하고, 주식 주문을 할 수 있다.
자동매매 프로그램 시작의 3단계를 간단한 테스트 프로그램으로 구현해보자.
테스트 프로그램이므로 화면 구성은 매우 간단하다.
서버 연결을 할 수 있는 버튼, 아이디/비밀번호를 입력할 수 있는 텍스트 박스와 로그인할 수 있는 버튼, 로그인 후 불러오기가 완료된 보유 계좌 목록 캄보 박스와 사용할 계좌를 선택할 수 있는 버튼.
그리고 이후 포스팅하게 될 [단일데이터 조회]용 자산현황 조회 버튼과 [반복데이터 조회]용 계좌잔고 조회 버튼이 있다.
실제로 xingAPI를 이용한 자동매매 프로그램을 설계해보면 위의 기능들외의 데이터 조회 기능을 굳이 사용하지 않아도 충분하다는 것을 깨달을 것이다.
아래는 계좌 선택까지의 Full 소스다.
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows.Forms;
using XA_SESSIONLib;
using XA_DATASETLib;
namespace xingAPI_Test
{
public partial class xingAPI_Test : Form
{
// Global 변수
bool gIsConnHTS = false;
string gAccount = string.Empty;
// xingAPI Session 클래스 선언
XASessionClass myXASessionClass;
public xingAPI_Test()
{
InitializeComponent();
// xingAPI Session 객체 생성
myXASessionClass = new XASessionClass();
}
// Form Load될 때 초기화
private void xingAPI_Test_Load(object sender, EventArgs e)
{
Realtime_Log.Items.Add("====== xingAPI Test 프로그램 시작 ======");
// HTS 로그인 핸들러 등록
myXASessionClass._IXASessionEvents_Event_Login += new XA_SESSIONLib._IXASessionEvents_LoginEventHandler(myXASessionClass_Login);
}
// HTS 서버 접속
private void Btn_Conn_Server_Click(object sender, EventArgs e)
{
string serverType = "hts.ebestsec.co.kr";
gIsConnHTS = myXASessionClass.ConnectServer(serverType, 20001); // 20001은 HTS 서버 고정 Port 값
if (gIsConnHTS == true)
{
Realtime_Log.Items.Add("이베스트 실투자 서버 접속 완료");
}
else // 연결 실패했을 경우
{
int ErrCode = myXASessionClass.GetLastError();
var ErrMsg = myXASessionClass.GetErrorMessage(ErrCode);
string err_log = String.Format("[서버연결] ERR - {0}({1})", ErrMsg, ErrCode);
}
}
// HTS 서버 로그인
private void Btn_Login_Click(object sender, EventArgs e)
{
if (gIsConnHTS == true) // HTS 서버 로그인은 서버 접속이 완료된 이후에만 가능.
{
bool reqLogin = ((XA_SESSIONLib.IXASession)myXASessionClass).Login(TB_User_ID.Text, TB_User_Passwd.Text,
"", 0, false); // 세번째 인자는 공인인증비번을 입력하는 칸이지만 보안을 위해 입력 받지 않고 보안 프로그램에서 입력하는 것이 좋다.
// reqLogin의 결과 값은 로그인 완료 여부가 아닌, 서버에 로그인 요청을 전송 완료 여부이다. 때문에 Login Handler 함수를
// 별도 추가하여 요청한 로그인 정보가 정상적으로 성공했는지 확인해야한다.
if (reqLogin == true)
{
Realtime_Log.Items.Add("HTS 로그인 요청 완료");
}
else
{
Realtime_Log.Items.Add("HTS 로그인 요청 실패");
int ErrCode = myXASessionClass.GetLastError();
var ErrMsg = myXASessionClass.GetErrorMessage(ErrCode);
string err_log = String.Format("[로그인] ERR - {0}({1})", ErrMsg, ErrCode);
}
}
}
// HTS 서버 로그인 결과
void myXASessionClass_Login(string code, string msg)
{
if (code == "0000") // code 0000은 성공을 의미
{
Realtime_Log.Items.Add("HTS 로그인 완료");
// 보유 계좌 정보 Loading
int AccountCount = myXASessionClass.GetAccountListCount();
for (int i = 0; i < AccountCount; i++)
{
string AccountNo = string.Empty;
string AccountName = string.Empty;
string AccountDetailName = string.Empty;
string AccountNickName = string.Empty;
string AccountFinal = string.Empty;
AccountNo = myXASessionClass.GetAccountList(i);
AccountName = myXASessionClass.GetAccountName(AccountNo);
AccountDetailName = myXASessionClass.GetAcctDetailName(AccountNo);
AccountNickName = myXASessionClass.GetAcctDetailName(AccountNo);
AccountFinal = AccountNo + "(" + AccountName + ")";
Realtime_Log.Items.Add(AccountFinal);
CB_Account_List.Items.Add(AccountNo);
}
Realtime_Log.Items.Add("보유 계좌 정보 Loading 완료");
}
}
// 보유 계좌 선택
private void Btn_Input_Account_Click(object sender, EventArgs e)
{
if (CB_Account_List.Text != "")
{
gAccount = CB_Account_List.Text;
string str_log = String.Format("[보유계좌선택] {0} 선택완료.", gAccount);
Realtime_Log.Items.Add(str_log);
}
}
// 자산현황 조회 [단일데이터]
private void Btn_Get_Balance_Click(object sender, EventArgs e)
{
}
// 계좌잔고 조회 [반복데이터]
private void Btn_Get_Items_Click(object sender, EventArgs e)
{
}
}
}
이제 한 단계씩 프로그램을 실행해 보자.
STEP 1. 서버 연결
에러 발생 없이 정상적으로 이베스트 HTS 실투자 서버에 접속했다.
STEP 2. 로그인
아이디와 비밀번호를 입력하고 로그인을 누르면, HTS 로그인 요청이 완료되었다는 문구와 함께 공인인증서 비밀번호를 입력하는 팝업이 하나 뜬다. 많은 xingAPI 자동매매 프로그램들이 아이디/비밀번호와 함께 공인인증서 비밀번호를 입력하도록 만들어져 있는 걸 보았는데 이렇게 보안 프로그램에서 입력하는 것이(조금 번거로울 수 있겠지만...) 더 안전하겠다.
STEP 3. 보유 계좌 선택
로그인이 성공하면 보유계좌 목록에 보유한 계좌들이 저장되게 되고, 그중에서 하나를 골라 선택 버튼을 누르면 비로소 해당 계좌에 대해 자동 매매를 진행할 준비가 완료된다.
이 이후부터는 xingAPI의 TR들을 적절히 조합하여 자신만의 자동 매매 프로그램을 만들어 적용하면 된다.
다음은 선택한 계좌의 자산현황을 조회하는 기능으로 xingAPI의 [단일데이터 조회] 기능을 구현해본다.
학창 시절 C++ 시간에 배웠던 OOP, 그중에서도 Class의 강점에 대해 귀에 박히도록 들었던 말은...
'은닉성'
Encapsulation이라고도 하는 그놈이다.
은닉성을 가장 쉽게 구현하는 방법은 class의 멤버는 외부에서 직접 접근 가능하지 못하도록 private 또는 protected로 선언하고 class에서 지정한 방법으로만 접근 가능하도록 public 함수를 두는 것이다.
그러면 외부 사용자는 class의 멤버 변수들을 원하는 방식으로 조작할 수 없을 것이고, 그로 인한 장애나 버그도 발생할 일이 없을 것이다.
class에 선언된 멤버 변수에 대해 값을 설정하고, 또 값을 얻어오는 setter와 getter에 대한 함수를 class 내부에 별도로 선언해 놓는 것이 일반적이다.
class Test
{
private int value;
...
public void setValue(int value)
{
if(value > 0)
this.value = value;
}
public int getValue()
{
return this.value;
}
}
integer 멤버 변수 value에 대해 setter와 getter가 선언된 OOP적인(?) class의 모습이다.
물론 C#에서도 위 형태의 class 사용이 가능하나...만약 class 내부에 변수가 30개면 어떻게 될까....?
setter와 getter가 각각 30개씩...엄청난 노가다와 가독성(readability)이 현저히 떨어질 것이다.
그래서 C#에서는 속성(Property)라는 아주 좋은 기능을 제공한다.
C# Property
같은 기능을 하도록 Property를 사용해 보자. 아래처럼 변경하게 되면, 사용 방법이 바뀌는데 구조체 사용하듯이 사용하면 된다.
class Test
{
private int value;
public int _value
{
get { return value; }
set { this.value = value; }
}
}
void main()
{
Test test = new Test();
test.value = 10;
Console.WriteLine(test.value);
}
훨씬 간결해 지고 간단해졌다.
C# 자동구현 Property
C#에서 제공하는 정말 간결하게 하는 방법이 있다.
이 Property의 단점을 꼽자면...바로 '접근 제한자'가 public이어야 한다는 것 같다.
class Test
{
public int value { get; set; }
}
void main()
{
Test test = new Test();
test.value = 10;
Console.WriteLine(test.value);
}
MS Visual Studio에서는 Property 작성을 쉽게 할 수 있는 방법을 제공한다.
[prop]을 입력하고 [Tab] 키를 두 번 누르면 자동구현 Property 코드가 입력된다.
[propfull]을 입력하고 [Tab] 키를 두 번 누르면 C# Property 코드가 입력된다.
namespace WinFormTest
{
public partial class Form1 : Form
{
// prop + Tab 두번
public int MyProperty { get; set; }
// propfull + Tab 두번
private int myVar;
public int MyProperty
{
get { return myVar; }
set { myVar = value; }
}
}
}
C#에 대한 공부를 하면서 L-ATS v2.0 개발을 진행하다보니 생각했던대로 되지 않거나 내 설계 방향으로는 구현이 불가능한 부분들이 종종 발생한다.
앞서 포스팅한 '주식종목'에 대한 클래스(객체) 사용을 객체 배열(Array)을 사용하였는데...(단순히 C/C++ 개발자로써 구조체 배열을 생각했기에...) 사실, 데이터의 빠른 접근 때문에 배열(Array)자체를 매우 선호하는 편이다.
객체 배열을 DatagridView에 Binding하는 과정에서 C#에서는 Collection(콜렉션)이란 강력한 도구를 제공한다는 것을 알았다. C# 기본서와 구글링을 통해 알게 된것은 C언어에서처럼 구조체 배열과 같은 형태의 데이터 구조는 DataGridView에 Binding하기에 적합하지 않은것 같다.
C#에서 제공하는 ArrayList와 Collection, 그리고 Generic Collection에 대해 짚고 넘어가야겠다.
이미 '주식종목' 부분의 설계와 개발이 거의 끝나가고 있는데...뒤집어 엎어야 할거 같다....
컬렉션(Collection)
- 같은 Type의 데이터를 모아서 처리하는 데이터 구조.
- 배열(Array)은 컬렉션 중의 하나라고 할 수 있음.
- 컬렉션은 Generic Collection 과 Non-Generic Collection이 있음.
ArrayList
- 배열과 유사한 자료구조.
- 배열과 달리 그 크기를 미리 지정하지 않고 동적으로 할당 가능.
- 모든 타입의 데이터가 저장될 수 있음. (Parameter의 Type이 Object임)
- 관리하는 데이터가 많아질 수록 속도가 느려짐.
수행할 작업
Generic Collection
Non-Generic Collection
키별로 빠르게 접근할 수 있도록 키/값의 쌍으로 저장
Dictionary(Tkey, TValue)
Hash Table
인덱스 별로 항목 액세스
List<T>
Array ArrayList
FIFO 방식으로 항목 사용
Queue<T>
Queue
LIFO 방식으로 항목 사용
Stack<T>
Stack
순서대로 항목 액세스
LinkedList<T>
-
컬렉션에 항목을 추가하거나 삭제할 떄 알림 표시
ObserverbleCollection<T>
-
정렬된 컬렉션
SortedList<t>
SortedList
수학 함수용 집합
HashSet<T> SortedSet<T>
-
위의 내용을 보면...Generic Collection의 경우 C#에서만 제공하는 Optimized 도구들인것 같은 느낌이다.
Non-Generic Collection의 경우는 구식 도구들인것 같은 느낌...C#에서는 두 가지 형태 모두 제공한다는 것이고.
사실 Generic Collection은 컴파일 타입에 Type-Safe(형식이 안전한)하기에 Non-Generic Collection보다 성능이 좋다고 한다.
Dictionary는 Python에서도 자주 사용하는 Data 구조로 그 활용성이 매우 좋음을 알고 있기에, 내가 만들려는 '주식종목' 목록에 대해 Dictionary의 TValue가 Object 형식으로 저장이 가능하다면 Dictionary를 사용해야겠다.
Dictionary(딕셔너리)
'사전'. 사전을 펼쳐보면 가장 마지막 부분에 Index가 있다. 몇몇의 [키워드]들이 나열되어 있고, 그 키워드에 대한 [내용]이 몇 페이지에 존재하는지 표시되어 있는...
때문에 찾기 원하는 내용의 [키워드]를 알고 있으면 빠르게 [내용]을 찾을 수 있다.
Dictionary의 원리는 이것과 같다.
Array(배열)도 마찬가지다. 원하는 데이터가 저장되어 있는 Index(배열의 Index)를 알면 바로 데이터를 가져올 수 있다.
Dictionary는 Index 대신, 특정 Key 값(사용자가 원하는)을 사용할 수 있기에 보다 인지하기 쉽고 가독성(Readability)이 좋다.
< 주의 사항 > 1. 중복된 키(Tkey)를 사용할 수 없음. 'ArgumentException' 발생. 2. 존재하지 않는 키(Tkey)로 검색 시 'KeyNotFoundException' 발생. 3. Array와 같이 순차적인 숫자 Index 사용 불가.
다음은 C# 기본서에 있는 Dictionary 예제이다.
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace Dictionary
{
class Program
{
static void Main(string[] args)
{
Dictionary<string, string> colorTable = new Dictionary<string, string>();
colorTable.Add("Red", "빨간색");
colorTable.Add("Green", "초록색");
colorTable.Add("Blue", "파란색)");
foreach (var v in colorTable)
Console.WriteLine("colorTable[{0}] = {1}", v.Key, v.Value);
// 동일 Key 값을 추가하는 경우
try
{
colorTable.Add("Red", "빨강");
}
catch(ArgumentException e)
{
Console.WriteLine(e.Message);
}
// 없는 Key 값을 찾는 경우
try
{
Console.WriteLine("Yellow => {0}", colorTable["Yellow"]);
}
catch(KeyNotFoundException e)
{
Console.WriteLine(e.Message);
}
}
}
}
Add() 함수가 쉽게 Dictionary Item을 추가할 수 있도록 해주며, Tkey의 위치의 영문값이 Key가 되어 빠르게 TValue에 해당하는 값을 읽어올 수 있는 구조이다.
Dictionary<string, string>은 TKey와 TValue를 모두 'string' Type으로 지정하여 Dictionary를 생성하였으므로, 객체를 사용할 결루 Dictionary<string, object명>을 사용하면 될 것 같다.
그럼...TValue 쪽에 해당하는 객체(object)는...DataGridView에 어떻게 또 Binding을 시킬 수 있을지...걱정이 앞서긴 하지만...다음 C# 포스팅은 Dictionary를 DataGridView에 Binding하는 걸 해야겠다.
C언어를 주로 사용하더 나에게는 이 Class 라는 것이 이해하기 쉬우면서도 이해하기 어려운 부분이었다.
흔히 Class와 Sturct(구조체)를 비교하는데 Struct는 평소에 아주 많이 사용하는 놈이기 때문이었다.
C에서의 구조체의 정의는 너무도 쉽다.
함께 쓰고 싶은 다양한 데이터 Type 변수들의 집합. 모음. 모둠..?
그리고 OOP를 흉내낼 수 있도록 해준 '함수 포인터'도 멤버로 가질 수 있다. (C#에서는 함수를 가질수 있다는 점이 가장 큰 차이점일까.)
구조체와 클래스의 차이점에 대해 정리되어 있는 것은 이보다 더 많은 자료들을 찾아볼 수 있다.
구분
구조체 (STRUCT)
객체 (CLASS)
형식
Value (값 형식)
Reference (참조 형식)
생성영역
Stack
Heap
인스턴스
new ()
선언
상속
X
O
생성자
X
O
인터페이스
O
O
STATIC
X
O
구조체의 경우 구조체 배열을 사용하여 동일한 형태의 여러 데이터들의 집합을 관리할 수 있다.
이 기능은 프로그램 내의 데이터 관리 측면에 있어서 엄청난 효율성을 가져다 준다.
게다가 배열이다. Index를 사용해서 가장 빠른 속도로 원하는 데이터에 접근할 수 있으니, 개발자의 입장에서는 최고의 조합이다. C#의 클래스도 같은 구조의 기능을 지원할까.
나는 아래와 같이 객체 배열을 만들어서 사용하고 싶었다.
부모 클래스의 변수 및 함수를 상속받은 자식이 사용하도록, 그것도 외부 클래스에서 사용하도록 하기 위해 부모클래스의 변수에 대해 '접근 제한자'를 사용하도록 하는 데에도 몇번의 시행착오를 겪었다.
부모 클래스의 변수를 상속받은 자식 클래스에서만 접근이 가능하도록 'protected'로 선언하였는데 아래 표의 설명이 많은 도움이 되었다.
C# 접근 제한자
public 클래스의 내부와 외부 모든 곳에서 접근이 가능하다.
protected 클래스의 내부와 해당 클래스를 상속 받은 자식 클래스에서만 접근이 가능하다.
private 해당 클래스 내부에서만 접근이 가능하다.
그리고 외부 클래스에서 자식 클래스인 Stock_RTB를 객체 배열로 선언하여 사용하기 위해 C# 기초 책과 많은 포스팅에서 아래와 같이 간단하게 적용했는데....
이상하게 계속 NullReference Exception이 발생하는 것이다.
using System;
namespace Program
{
public partial class MainClass : Form
{
... 생략 ...
// RTB 클래스 배열 선언
Stock_Realtime_Balance[] gArrStockRTB;
... 생략 ...
string hname = "이베스트증권";
gArrStockRTB = new Stock_Realtime_Balance[3];
for (int index = 0; index < 3; i++)
{
try
{
gArrStockRTB[index].setCodeName(hname);
... 생략 ...
string line = String.Format("{0}", gArrStockRTB[index].getCodeName());
... 생략 ...
}
catch (NullReferenceException e)
{
Console.WriteLine(e);
}
}
... 생략 ...
}
}
와...정말 온갖 부분을 다 예외 처리하고 로그를 찍어서 확인해 봤지만 도저히 찾지를 못했는데, 2일만에 드디어 문제가 됐던 부분을 찾았다. (2일이라면 오래 걸린것 같지만...이 프로그램을 만드는데에 하루에 1.5시간 정도만 투자하고 있으므로 생각보다 빨리 찾은 셈이다.)
using System;
namespace Program
{
public partial class MainClass : Form
{
... 생략 ...
// RTB 클래스 배열 선언
Stock_Realtime_Balance[] gArrStockRTB;
... 생략 ...
string hname = "이베스트증권";
gArrStockRTB = new Stock_Realtime_Balance[3];
for (int i = 0; i < gArrStockRTB.Length; i++)
gArrStockRTB[i] = new Stock_Realtime_Balance();
for (int index = 0; index < 3; i++)
{
try
{
gArrStockRTB[index].setCodeName(hname);
... 생략 ...
string line = String.Format("{0}", gArrStockRTB[index].getCodeName());
... 생략 ...
}
catch (NullReferenceException e)
{
Console.WriteLine(e);
}
}
... 생략 ...
}
}
다시 각 객체 배열의 위치마다 new로 잡아줘야 한다니...객체(Class)의 특성상 당연한 작업인데...
좀 더 세련되게 'Extension Method'를 사용해서 심플하게 처리하는 방법이 있다는데...C# 초보자인 나는 일단 for문을 사용하기로 했다....
역시 정확하게 다 이해하지 못한 상황에서 개발부터 진행하면 이렇게 간단한 실수를 하게된다.