L-ATS 3.0은 지난 10월 초 모든 서비스 구현이 완료되어 서비스의 기능 테스트 진행중이다.

투자 프로그램 개발 시 유의 사항
프로그램을 테스트 하기 위해 보통 모의투자를 사용한다. 
모의투자를 통해 프로그램을 테스트 하는 것은 본인 생각보다 실투자와는 많은 차이가 있다. 
먼저 증권사에서 제공하는 API TR은 결과물이 실투자와 모의투자가 다른 경우가 있다. (예를 들어, 같은 TR이지만 실투자에서만 결과물을 보여주는 경우)
그리고 모의투자는 매수/매도 타이밍이 매우 늦는다. 내가 만든 프로그램은 빠른 매수와 빠른 매도가 핵심이기 때문에 이러한 성격의 프로그램에는 모의투자가 적절하지 않다. 
그래서 테스트도 소량의 현금으로 진행....한....다....

 

테스트를 현금으로 [실투자]에서 진행하고 있으므로, 실 운영과 기능 테스트를 함께 진행하고 있는 셈이다. 

 

기능적인 소소한 버그가 존재하긴 하지만 기획했던 서비스의 전체적인 흐름은 생각했던대로 정상적으로 운영되는 것을 확인했다. 더구나 시스템은 수일간 증권 시장이 운영되는 시간동안 장애없이 잘 운영되었다. 

 

이제 준비는 끝난 것일까...?


 

처음 L-ATS 3.0의 서비스를 기획할 때 캡쳐된 글자 이미지를 OCR 처리하여 종목명을 추출하는 것을 생각해내고 선택한 이유는 현재 내가 인공지능 전문 솔루션 회사에 재직중이었기 때문이었다. 

무언가 내가 만든 이 서비스에도 인공지능적인 요소를 넣고 싶었기 때문이다. 그렇게 선택한 것이 OCR 이었고, 최근 큰 이슈가 되었던 ChatGPT를 활용하는 방법을 찾고 싶었지만 서비스 기능들을 아무리 살펴봐도 ChatGPT가 할만한 일은 없었다. 

물론 저 생각은 [기획] 단계에서의 생각이었다.

 

[운영] 단계에서의 발견

 

인공지능에는 100%는 없다.

이 말은 즉, 글자 이미지가 OCR 처리된 결과물은 100% 정확하지 않을 수 있다는 것이다. 

 

내가 사용한 OCR 엔진인 Tessaract는 한국어 언어 모델을 지정해서 사용했는데 그 때문인지 영어 알파벳이 포함된 종목정보는 영어 알파벳이 아라비아 숫자로 인식되었다.

예를 들면, SK하이닉스는 59하이닉스와 같이 인식되어 결과물을 생성했다.

 

여기서 ChatGPT가 문득 떠올랐다. 

 

OCR로부터 잘못 인식된 종목정보를 ChatGPT에게 물어서 교정할 수 있지 않을까?

 

ChatGPT는 아주 다양한, 많은 업무를 해결해주니까 가능할 것이라 생각하긴 했지만, 과연 1750억개의 파라미터에 우리 한국 증권 시장의 KOSPI, KOSDAQ 정보가 있을지는 의문이긴 했다. 

 

그래서 프롬프트 작성에 열을 올리기 시작했다. 

프롬프트란? ChatGPT와 같은 LLM 모델에게 질문을 할 때, 더 나은, 더 좋은 답변을 억기 위해서 질문을 최적화 하는 기법을 말한다.
좋은 프롬프트를 만들수록 더 좋고 간결한 정보를 획득할 수 있다. 

 

처음에는 심플하게 ChatGPT에게 물어봤다.

ChatGPT Input 프롬프트
ChatGPT 응답

음...역시나 내가 원하는 답변을 얻지는 못했다.

 

ChatGPT를 잘 활용하는 방법 중에 가장 좋은 방법은 컨텍스트를 명확하게 하는 것인데

예를 들면, 부가적인 정보들을 주어주는 것이다. (갑자기 ChatGPT 활용법으로 길이 새는것 같은데.....)

 

내가 필요한 정보는 한국 증권 시장에 속한 종목 이름에 대한 것이니 좀 더 정확한 답변을 얻기 위해 Role Prompt 기법을 사용해본다. (ChatGPT에게 역할을 주어주는것!)

더 나은 ChatGPT Input 프롬프트
ChatGPT 응답

 

ChatGPT가 해야할 일을 보다 더 자세하게 지시해주면 이렇게 내가 원하는 답변을 정확하게 얻을 수 있다. 

 

이제 이것을 내 서비스에 녹아들게 하면 보다 더 많은 종목들에 대해 자동 매매을 처리할 수 있게 되고, 좀 더 인공지능적인 서비스가 되지 않을까? 

 

더 나은 L-ATS를 위해 ChatGPT를 접목시킬 수 있는 서비스의 시스템 설계를 조금씩 진행해 봐야겠다.

Posted by [ 브랜든 ]
,

L-ATS 3.0 시스템은 크게 아래 두 가지 프로그램으로 구성된다.

Windows Application
OS : Windows 11
개발 환경 : 개인 데스크탑, MS Visual Studio 2019, 이베스트 투자증권 XingAPI
개발 언어 : C#, Winform
운영 환경 : AWS Cloud
LINUX Server Program
OS : CentOS 8.x
개발 환경 : Raspberry Pi, Vim Editor, MariaDB
개발 언어 : python3
운영 환경 : AWS Cloud

 

시스템의 큰 구성은 기존 L-ATS 2.0의 구조를 승계했다.

Windows Application은 매수할 종목의 자동 인식실제 매매 업무를 담당하고 LINUX Server Program은 매수를 위한 종목 정보관리 업무를 담당한다.

 

L-ATS 3.0의 큰 흐름을 도식화 하면 다음과 같다.


L-ATS 3.0.exe
Windows Application
  • 화면 캡처 처리
    • Visual Studio를 통해 C# 프로그래밍으로 화면을 캡쳐하는 방식을 구현하기는 쉽지 않았다.
    • 관련된 예시나 소스는 많지만 원하는 크기, 원하는 위치의 화면을 캡쳐하는 방식의 화면 캡쳐 구현은 존재하지 않았다.
    • 그래서 유용한 정보들을 수집해서 몇가지의 예시 기능을 결합해서 구현했다.
  • 캡쳐된 문자 이미지의 OCR 처리
    • 캡쳐된 화면을 바로 OCR 요청해서 Text로 변환하는 기능을 한다.
    • Apple이나 Google, NAVER에서 제공하는 OCR은 한국어 인식능력이 뛰어나지만 비용적인 측면의 문제가 있었다.
    • 때문에 무료로 사용 가능한 Tessaract OCR을 사용했다. (인식률이 많이 좋아지긴 했지만...부족하다...)
Tessaract OCR의 정확도가 낮지만 그래도 효용성이 있다고 판단한 이유는 하루에 많은 뉴스 종목들 중에 정확히 인식되는 몇몇개 종목에 대해 데이터 분석을 통해 높은 확률로 수익을 낼수 있는 조건을 만든다면 사용해도 무방하다고 생각했다.
  • 이베스트 투자증권 API를 사용한 Trading 처리
    • OCR 결과 Text에서 패턴 분석을 통해 종목명을 추출한다.
패턴 분석을 위해서는 해당 뉴스방에서 어떤 형태의 문자열로 뉴스를 알려주는지 공통점을 찾아내야 한다. 동일한 방법으로 많은 수의 뉴스 분석이 가능하고 종목추출이 가능한 패턴을 분석한다.
    • 추출된 종목명은 LINUX Server Program으로 전송하여 Database에 저장된 종목의 Code 정보를 가져온다.
    • Code 정보를 확보하면 증권사 API를 사용하여 매수 요청한다.
    • 매매 결과 정보를 LINUX Server Program으로 전송하여 파일에 기록할 수 있게 한다.
L-ATS-3.0-Server.py
LINUX Python Program
  • OCR 결과에서 추출한 종목명을 수신한다.
  • 수신한 종목명으로 Database를 조회하여 종목의 존재 여부를 확인하고 존재할 경우 종목 코드를 Windows Application으로 전달한다.
  • 수신한 매매 결과 정보를 모니터링 가능하도록 로그로 기록한다.
stocks_collector.py
LINUX Python Program
  • KRX의 국내 주식 정보를 스크래핑 해온다. (pykrx 모듈 사용)
  • KOSPI, KOSDAQ 주식 정보를 Database에 저장한다.
  • crontab에 등록하여 정해진 시간에 정기적으로 수행하도록 한다.
L-ATS Monitoring (v3.5 구현 예정)
LINUX Python Program | Mobile APP
  • 일일 매매 결과를 요청하고 수신하여 표시한다.
  • 실시간 시스템 처리 로그를 표시한다.

대략적인 시스템 구조와 구현해야 할 기능, 그리고 어떤 모듈들을 사용할지 선정을 완료했다.

현재 Windows 화면 캡쳐와 OCR 처리 테스트 프로그램을 만들고 있는데 테스트가 완료되면 바로 서비스 구축에 들어갈 예정이다. 테스트 프로그램 완성에 따라 하반기에는 전체 솔루션 검증을 진행할 예정이다.

 

Posted by [ 브랜든 ]
,

벌써 세 번째 L-ATS 사용자 화면을 설계한다.

 

첫번째 L-ATS 1.0 사용자 화면의 Focus는 '수익과 손실의 설정' 부분이었고,

두번째 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는 일부분들이 계승되어 구성되는것 같다. 

아무래서 버전 마다 기획하고 설계하고 구현하고 운영하다보면 배우는 것이 있기때문에 버릴건 버리고, 활용할 건 활용하게 되는것 같다.

 

새로 도전할 캡쳐 및 OCR 처리 부분 개발을 위해 부지런히 손을 움직여야겠다.

Posted by [ 브랜든 ]
,

회사에서 우리팀의 Windows Application 상용 프로그램을 만들어야 하는 이슈가 발생했다.

논의 끝에 VS 기반의 C#으로 기능을 구현하기로 하였고

Windows Application 개발 경험(개인 프로젝트인데.....;;;)이 있는 내가 설계부터 구현, 테스트까지 1인 프로젝트를 맡게 되었다.


나 역시도 C#이 주종목이 아니기에 무한한 지식의 조력자(Google-감사!)와 함께 진행하는 것으로 

내 프로그램의 모든 기능들은 C# 프로그래밍의 정석으로 만들어 지는 것이 아니다. 

그저 같은 기능을 구현함에 있어 수많은 구현 방식 중에 내 경험 및 지식에 빗대어 가장 유연하게 동작할 수 있는 방법을 채택하여 재조립 할 뿐.

 

때문에 구현하는 중간중간 다양한 버그 및 오류에 부딪친다.

 

이번 Exception Case는 기존 개인 프로젝트(L-ATS)를 진행하면서는 발생하지 않았던 문제였었는데 이번에 겪게되어 현재 내가 설계한 프로그램의 성격에 알맞는 방식으로 처리한 내용을 기록해 본다.

 

C#이나 WFP가 주종목이신 분들은...아주 사소한 문제일거 같다...


Ocurred Exception

System.ObjectDisposedException: '삭제된 개체에 액세스할 수 없습니다.'

Exception 발생 Case

  • Parent Form과 Child Form이 존재
  • Parent Form에서 Child Form.show()를 호출하여 Form을 띄우고 Child Form에 입력한 Data를 Parent Form에서 받아서 처리 (Delegate Event 사용)     

2020.06.20 - [C#] - [C#] delegate, 폼 간의 DATA 공유

 

[C#] delegate, 폼 간의 DATA 공유

Trading 시스템을 내가 아닌 제 3자의 사용자적 입장에서 사용하도록 시뮬레이션 해 보았을 때 자식 Form을 새로 띄워서 값을 입력 받거나 확인할 수 있게 하는 작업이 많이 필요하단 것을 알게 되

brandon-dev.tistory.com

  • Child Form에서 Data 입력 후 [ 확인 ] 시, Form Close() 후, 해당 Parent Form에서 Child Form을 다시 show() 할 때 발생

Form.show() 호출 시 ObjectDisposedException 발생

Child Form에서의 Close() 함수 호출에 의해 완전 삭제된 Child Form의 객체를 다시 show() 호출로 로딩하려고 하니 이미 삭제된 객체를 불러올 수 없다는 말이다.(인것 같다...)

Child Form 내부에서 self로 Close()를 하게되면 해당 객체를 생성한 Parent Form에서 객체 삭제를 하지 않아도 자동으로 삭제 된다는 것일까...?

 

사실 이 Disposed Exception은 비단 Form에 한정되어 나타나는 현상은 아니다. 

WinForm의 어떠한 객체라도 삭제된 후 접근하려 할 경우 같은 현상이 나타날테니 말이다. 

때문에 이 Exception을 해결하는 방법은 아주 다양하고 상황에 따라 합리적인 예시들이 많이 있다. 


내가 만들고 있는 프로그램의 Child Form의 역할은 

프로그램을 운용하기 위한 기본 설정 값들을 사용자에게 입력 받아 Parent Form으로 전달하는 역할을 한다.

쉽게 프로그램의 설정 POP-UP이라고 생각하면 된다. 

 

나는 Child Form의 [ FormClosing() ] 이벤트에 아래 코드를 추가하는 방식을 택했다.

private void micSetting_FormClosing(object sender, FormClosingEventArgs e)
{
    e.Cancel = true;
    this.Hide();
}

e.Cancel = true

FormClosing 작업을 취소하겠다는 것

this.Hide()

Form을 숨기겠다는 것

 

따라서 나는 분명 Child Form을 닫는 this.Close()를 호출하였지만 Form이 닫히는 과정(Form Closing)에서 Form이 닫히는 것을 취소하고 Form을 안보이도록 숨기겠다(Hide)는 뜻이다. 

 

여기서 중요한 점은.

Form을 사용자가 보지 못하게끔 숨기기만 하는 것으로 (사용자 입장에서는 Form이 꺼진것처럼 보이지만...)

이전에 Child Form에 입력한 값이 그대로 보인다는 것이다. Form을 숨기기만 하고 다시 보여주는 것이니 당연한 결과다.

 

프로그램의 설정값을 입력하는 목적의 Child Form을 만들 경우 사용자 입장에서는 이전에 입력한 값이 그대로 보여지니 이 방법이 내가 설계한 프로그램에는 가장 적합한 해결책인 것이다.

만약 Form을 삭제하고 다시 객체를 생성해서 Child Form을 띄울 경우 기존에 입력했던 값들을 다시 Parent Form에서 받아 오든, 아니면 특정 위치에 저장을 해서 불러오든 해야하기에 구현의 까다로움이 존재하므로 굳이 이 방법을 사용하지 않아도 좋은 프로그램을 만들 수 있다.

Posted by [ 브랜든 ]
,

L-ATSLim's Automatic Trading System의 약자로 2016년 성(임씨)이 같은 나(프로그램 개발)와 내 친구(검색식 생성)의 합작으로 시작된 프로젝트였다.

 

Trading 프로그램들은 모두 같은 목적을 갖고 있다. 

[ 매매의 자동화 ]

 

프로그램을 필요로하는 사람들마다 매매의 자동화의 방식, 기준은 다 제각각이다

L-ATS은 매매의 자동화를 위해 이러한 기능들을 제공했다.

 

2016년 L-ATS 1.0

2020.05.24 - [Trading] - [Trading] 첫번째 프로그램

 

[Trading] 첫번째 프로그램

프로그램 명 : L-ATS (Lim's Automatic Trading System) 버전 : 1.0 환경 : Visual Basic 주요기능 : 검색식 기반 주식 매수, 설정값 기반 주식 매도, 스탑로스 API : 키움증권 API 제작기간 : 2015.05 ~ 2015.08 키움에서

brandon-dev.tistory.com


  • 키움증권 API 사용
  • 키움증권 [조건검색]의 자체 개발한 검색식을 사용한 자동 매수

손익 수준(%) 및 수량을 단계적으로 커스텀 가능한 자동 매도

 

2020년 L-ATS 2.0

2020.06.07 - [Trading] - [Trading] 두번째 프로그램

 

[Trading] 두번째 프로그램

프로그램 명 : L-ATS (Lim's Automatic Trading System) 버전 : 2.0 환경 : Visual C# 주요기능 : HTS/MTS 매수 종목 정보 실시간 수신, 설정값 기반 주식 매도, Flexable 스탑로스 API : 이베스트 증권 XingAPI 제작기간 : 2

brandon-dev.tistory.com


  • 이베스트증권 XingAPI 사용
  • 자동 매매 프로그램과 텔레그램 봇의 조합으로 텔레그램에서 종목명만 입력하면 설정된 조건으로 알아서 매수해 주는 실시간 매수 자동화
  • 가장 빠르게(!) 매수하는게 주 목적으로 매매를 위한 종목 클릭, 계좌 비번 입력, 매수 수량 입력, 매수 버튼 클릭 등의 작업을 없앤 초 간단 매수
  • 상승하는 종목의 최대 수익을 낼 수 있도록 현재가에 맞춰 유동적으로 변화하는 매도 기준의 실시간 변화
  • 손실을 최소화 할 수 있는 철저한 자동 손절

 

L-ATS 1.0은 사실상 실패한 프로젝트였다. 실패라기 보단 매매의 목적(=수익)을 달성하기에 큰 단점이 있었다.

제대로된 검색식

L-ATS 1.0은 위에 기술했듯이 키움증권의 검색식을 통해 추출된 종목을 즉시 매매하는 방식으로 차트 기반의 수익을 만들어내는 제대로된 검색식을 필요로 했기 때문이다.

아는 사람은 알겠지만...수익을 만들어 내는 제대로된 검색식을 만들기란 정말 쉽지않다. 
제대로된 검색식을 누구나 만들 수 있다면 모든 사람이 부자가 됐겠지...(나 역시도 반년간 검색식에 매달렸지만....)

L-ATS 1.0은 이렇게 약 7개월 정도 운용되다가 폐기되었다.


그렇게 시간이 흘러 2019년.

친구의 권유로 카카x톡 찌라시 매매방에 들어가게 되었고, 약 1년여의 시간동안 관찰해온 찌라시 매매방의 특징으로 나만의 독창적인 매도 방식을 결합하여 L-ATS 2.0을 설계하게 되었다.

 

[ 찌라시 매매방에서 찾아낸 특징 ]

  1. 찌라시 매매방은 진짜 뉴스와 가짜 뉴스가 섞여 나온다.
  2. 찌라시 매매방의 목적 상(= 뉴스 띄우고 개미들이 들러붙어 1~2% 오를때 물량 넘기기 수법) 진짜 뉴스든 가짜 뉴스든 대부분 어느 순간 일정 수준의 상승을 보인다.
  3. 높은 확률로 상한가 직행 종목이 나온다. (=진짜뉴스)
   결론 : 가지 않는 종목을 빠른 손절로 손실을 최소화 하고 상한가 가능 종목 1개만 잡아도 평균적으로 수익이다.

[ 자동 손절로 최대 수익을 낼 수 있는 방법의 추론]

  1. 주식판의 모든 종목은 파형을 그리면서 움직인다.
  2. 세력이 들어와 급등하는 종목도 개미를 떨구거나 더 많은 매물을 모아서 올리기 위해 매량 매도를 통해 순간 가격을 내린다.
  3. 종목의 최고가가 갱신될 때마다 해당 값을 기준으로 일정 수준의 손절 라인을 실시간으로 변경하면서 가져간다면 자동매매로써의 최대 수익을 낼 수 있는 하나의 방법이 된다. (= 내 생각)
절대적 손절 라인을 -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로 변환 후

종목명을 추출하여 즉시 매수 처리하는 방식으로 구현될 것이다.

 

이 과정에서도 분명 문제점은 있다. 

하지만 완전 자동화가 되는 프로그램을 만드는 것이 우선순위이기 때문에 이슈들은 일단 뒤로 제껴둘 생각이다.

Posted by [ 브랜든 ]
,

나는 약  7년이란 시간을 시스템 소프트웨어를 설계하고 개발하는 

개발자(Developer)로 살아왔다. 

 

그저 필요한 기능에 대한 Architecture와 Design, 그리고 Data Structure에 의한 프로그램을 개발하고 Release하면

회사의 영업 파트와 고객사에 납품/적용만 하면 되는 업무를 주로 해왔기에 

내 인생에 [영업]이란 것은 전혀 끼어들 일이 없을 것이라 생각했다.

 


2018년 10월.

7년이란 세월을 몸담았던 곳을 등지고 

새로움, 비전, 미래지향적인 기대에 부풀어 인공지능(AI) 솔루션 중소기업으로 이직했다.

 

이 곳에서의 나의 첫 업무는...

PM(Project Manager) 였다. 

 

와...정말 새로운 도전 of 도전이었다. 

7년간 누군가(PM)의 밑에서만 일해왔지 내가 그 자리에서 업무를 보게될 줄은 몰랐지만 

"뭐 다들 하는데 나라고 못하겠어?" 라는 맘으로 정말 열심히 했다.

정말...

'고객을 직접 상대하는 일'은 일반 업무의 5배는 어렵다.

 

PM 업무가 익숙해져 갈 때 즈음...

나는 한 사업부의 '이사'로 승진하여 우리 사업부의 Pre-Sales/계약/입찰 + PM의 업무를 맡게 되었다. 


아는 사람은 잘 알겠지만

인공지능(AI)이라는 것이 현 시점에서 기술적 한계가 분명히 있고

실 사용에서의 효용성이 약 70%정도(본인피셜) 밖에 되지 않음은 명확한 사실이다. 

 

때문에, 국가적 차원에서 인공지능(AI)을 활성화 시키려는 정책에 따른 국가 사업 및 과제가 대부분의 인공지능(AI) 솔루션 업체들의 매출을 차지할 수 밖에 없다.

 

그래서 계약/입찰 등 행정 업무를 처리할 수 있는 "나라장터"와 친해져야 한다.

https://www.g2b.go.kr/index.jsp

 

나라장터: 국가종합전자조달

 

www.g2b.go.kr

 

국가 기관 / 지방 자치단체 등의 사업 공고 입찰 및 계약 처리를 위해서는 이 "나라장터"라고 불리는 국가종합전자조달 시스템을 통해야 하는데 도움줄 사람 없이 혼자서 맨땅에 헤딩으로 일처리하기는 정보도 많지 않아 눈앞이 깜깜할 정도로 막막하다. 더구나 계약의 경우 돈이 왔다갔다하는 업무이므로 신중할 수 밖에 없다. 

 

나는 이미 진행해봤고 지금도 처리하고 있는 일련의 업무들을 정리하여 기록해두려 한다.

 

  • 나라장터 사업 공고 입찰
  • 나라장터 수의계약
  • 나라장터 등록 업체 제조물품 등록
  • 상품정보 시스템 물품식별번호 발급

 

등등...

 

지금은 우리 사업부의 제품중 일부를 'GS(Good Software)인증'을 받기 위한 절차를 준비중이므로

GS인증 받기까지의 과정 또한 기록할 예정이다. 

 

여담이지만 국가 사업의 행정업무는 정말 사람이 할 짓이 못된다....

Posted by [ 브랜든 ]
,

바쁘다는 핑계로 오랫만에 포스팅을 한다.

핑계인거 같지만 정말 바쁘긴 했다. (우리회사가 드디어 11월에 코스닥 입성을 하게 됐음.)

고통의 결실이 달달하길 바랄뿐이다.

 

앞 포스팅을 보니 정확히 1년전 게시물이므로...

1년만에 다시 포스팅을 한다.

 

이번 포스팅은 xingAPI를 통해서 가져온 내 계좌의 종목 정보를 

C#의 DataGridView에 Binding하여 미려한 표 형식으로 표시하고, 실시간으로 변하는 [현재가] 를 스스로 갱신하면서

보여주도록 해보자.

 

이전 포스팅에 보면 계좌 잔고 정보가 TEXT 타입의 표로 심플하게 표시되어 있다.

 


2020.10.10 - [Trading] - [Trading] xingAPI - 반복 데이터 조회

 

[Trading] xingAPI - 반복 데이터 조회

이번 포스팅은 단일 데이터 조회에 이어 xingAPI의 [반복 데이터 조회]에 대해 포스팅 해본다. 단일 데이터 조회처럼 C#으로 코드를 짤 것이며, xingAPI COM 개발 가이드에는 엑셀 VBA를 이용한 프로그

brandon-dev.tistory.com


사실, 바빠서 포스팅을 못했을 뿐 계획했던 자동 매매 프로그램은 그 당시 완성하여 (내가 당장 필요한 핵심 기능만 구현하였지만...) 계속적으로 사용하고 있었다.

당시 내가 필요했던 기능은 종목 매수는 직접하고, 설정해 놓은 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해 준다.

그리고 DataGridView에 Binding 해주면 끝!!

...
    gBalanceList.Add(new Balance_List(expcode, hname, price, pamt, janqty, mamt, appamt, dtsunik, sunikrt));
...
	dataGridView1.DataSource = gBalanceList;
...

이제 결과물을 한번 보자.

이렇게 간단하게 미려한 표 형태의 데이터 표현이 가능하다니 이보다 편리하고도 편리할 수가 없다....

로그 찍는 부분의 어설픈 표형태(?)의 데이터만 봐도 서버 개발자들은 자괴감이 든다...

 

[ 전체 소스 ]

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;

        // xingAPI TR 클래스 선언
        XAQueryClass CSPAQ12200;        // 자산현황 조회
        XAQueryClass t0424Occurs;       // 잔고조회 - 종목리스트

        List<Balance_List> gBalanceList = new List<Balance_List>(); // 잔고 클래스 리스트

        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);
            // xingAPI TR 이벤트 등록
            CSPAQ12200 = new XAQueryClass();
            CSPAQ12200.ResFileName = @"C:\eBEST\xingAPI\Res\CSPAQ12200.res";  // xingAPI 서버로부터 수신한 응답 메시지를 COM 버전의 API에서 사용할 수 있도록 Format을 맞춰주는 파일
            CSPAQ12200.ReceiveData += CSPAQ12200ReceiveData;  // xingAPI 서버로부터 CSPAQ12200의 응답 메시지를 수신할 함수 등록

            // xingAPI 반복 데이터 조회 TR 이벤트 등록
            t0424Occurs = new XAQueryClass();
            t0424Occurs.ResFileName = @"C:\eBEST\xingAPI\Res\t0424.res";
            t0424Occurs.ReceiveData += t0424ReceiveData;
        }
        
        // HTS 서버 접속
        private void Btn_Conn_Server_Click(object sender, EventArgs e)
        {
            string serverType = "demo.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)
        {
            // CSPAQ12200 TR의 InBlock 데이터 세팅
            CSPAQ12200.SetFieldData("CSPAQ12200InBlock1", "RecCnt", 0, "00001");
            CSPAQ12200.SetFieldData("CSPAQ12200InBlock1", "MgmtBrnNo", 0, " ");
            CSPAQ12200.SetFieldData("CSPAQ12200InBlock1", "AcntNo", 0, gAccount);
            CSPAQ12200.SetFieldData("CSPAQ12200InBlock1", "Pwd", 0, "실제계좌비번");
            CSPAQ12200.SetFieldData("CSPAQ12200InBlock1", "BalCreTp", 0, "0");

            // TR 요청
            var result = CSPAQ12200.Request(false);
            if (result > 0)
                MessageBox.Show("CSPAQ12200 요청 완료");
            else
                MessageBox.Show("CSPAQ12200 요청 실패");
        }

        private void CSPAQ12200ReceiveData(string trCode)
        {
            // 요청한 CSPAQ12200 TR에 대한 응답 메시지 수신
            // CSPAQ12200 TR의 OutBlock2에서 필요한 데이터 추출
            // 1. 예탁자산 총액
            var temp_value = CSPAQ12200.GetFieldData("CSPAQ12200OutBlock2", "DpsastTotamt", 0);
            decimal estimated_assets = Convert.ToDecimal(temp_value);
            // 2. D+1 예수금
            temp_value = CSPAQ12200.GetFieldData("CSPAQ12200OutBlock2", "D1Dps", 0);
            decimal d1_deposit = Convert.ToDecimal(temp_value);
            // 3. D+2 예수금
            temp_value = CSPAQ12200.GetFieldData("CSPAQ12200OutBlock2", "D2Dps", 0);
            decimal d2_deposit = Convert.ToDecimal(temp_value);
            // 4. 손익률
            temp_value = CSPAQ12200.GetFieldData("CSPAQ12200OutBlock2", "PnlRat", 0);
            double valuation_rate = Convert.ToDouble(temp_value);
            // 5. 잔고평가금액
            temp_value = CSPAQ12200.GetFieldData("CSPAQ12200OutBlock2", "BalEvalAmt", 0);
            decimal evaluation_amounts = Convert.ToDecimal(temp_value);

            string text = String.Format("[예탁자산총액 : {0:#,0}] [D+1 예수금 : {1:#,0}] [D+2 예수금 : {2:#,0}] [손익률 : {3:f2}%] [잔고평가금액 : {4:#,0}]",
                estimated_assets, d1_deposit, d2_deposit, valuation_rate, evaluation_amounts);
            MessageBox.Show(text);
        }

        // 계좌잔고 조회 [반복데이터]
        private void Btn_Get_Items_Click(object sender, EventArgs e)
        {
            // 잔고 리스트 조회
            string inform = String.Format("-- 잔고 리스트 조회");
            Realtime_Log.Items.Add(inform);

            t0424Occurs.SetFieldData("t0424InBlock", "accno", 0, gAccount);
            t0424Occurs.SetFieldData("t0424InBlock", "passwd", 0, "실제계좌비번");
            t0424Occurs.SetFieldData("t0424InBlock", "prcgb", 0, "1");
            t0424Occurs.SetFieldData("t0424InBlock", "chegb", 0, "2");
            t0424Occurs.SetFieldData("t0424InBlock", "dangb", 0, "0");
            t0424Occurs.SetFieldData("t0424InBlock", "charge", 0, "0");
            t0424Occurs.SetFieldData("t0424InBlock", "cts_expcode", 0, " ");
            int result = t0424Occurs.Request(false);
            if (result < 0)
            {
                inform = string.Format("[보유계좌] 계좌 잔고리스트(t0424) 요청이 실패했습니다. ret={0}", result);
                Realtime_Log.Items.Add(inform);
            }
        }

        private void t0424ReceiveData(string trCod)
        {
            // 전체 잔고 종목 횟수 가져오기
            var count = t0424Occurs.GetBlockCount("t0424OutBlock1");
            string inform = String.Format("잔고조회 종목 리스트 갯수 : {0}", count);
            Realtime_Log.Items.Add(inform);

            Realtime_Log.Items.Add("-------------------------------------------------------------------------------------------------------------------------------");           
            string title = String.Format("| {0,20} | {1,10} | {2,10} | {3,10} | {4,10} | {5,10} | {6,10} | {7,10} | {8,10} |", "종목명", "종목코드", "현재가", "매입가", "잔고수량", "매입금액", "평가금액", "평가손익", "수익률");
            Realtime_Log.Items.Add(title);
            Realtime_Log.Items.Add("-------------------------------------------------------------------------------------------------------------------------------");

            // 가져온 종목 횟수 만큼 반복하면서 실제 종목 정보 가져오기
            for (int index = 0; index < count; ++index)
            {
                var temp_value = t0424Occurs.GetFieldData("t0424OutBlock1", "expcode", index); // 종목코드
                string expcode = Convert.ToString(temp_value);
                temp_value = t0424Occurs.GetFieldData("t0424OutBlock1", "hname", index);       // 종목명
                string hname = Convert.ToString(temp_value);
                temp_value = t0424Occurs.GetFieldData("t0424OutBlock1", "price", index);       // 현재가
                decimal price = Convert.ToDecimal(temp_value);
                temp_value = t0424Occurs.GetFieldData("t0424OutBlock1", "pamt", index);        // 매입가
                decimal pamt = Convert.ToDecimal(temp_value);
                temp_value = t0424Occurs.GetFieldData("t0424OutBlock1", "janqty", index);      // 잔고수량
                int janqty = Convert.ToInt16(temp_value);
                temp_value = t0424Occurs.GetFieldData("t0424OutBlock1", "mamt", index);        // 매입금액
                decimal mamt = Convert.ToDecimal(temp_value);
                temp_value = t0424Occurs.GetFieldData("t0424OutBlock1", "appamt", index);      // 평가금액
                decimal appamt = Convert.ToDecimal(temp_value);
                temp_value = t0424Occurs.GetFieldData("t0424OutBlock1", "dtsunik", index);     // 평가손익
                decimal dtsunik = Convert.ToDecimal(temp_value);
                temp_value = t0424Occurs.GetFieldData("t0424OutBlock1", "sunikrt", index);     // 수익율
                double sunikrt = Convert.ToDouble(temp_value);

                string contents = String.Format("| {0,20} | {1,10} | {2,10} | {3,10} | {4,10} | {5,10} | {6,10} | {7,10} | {8,10} |",
                    expcode, hname, price, pamt, janqty, mamt, appamt, dtsunik, sunikrt);

                gBalanceList.Add(new Balance_List(expcode, hname, price, pamt, janqty, mamt, appamt, dtsunik, sunikrt));

                Realtime_Log.Items.Add(contents);
            }
            Realtime_Log.Items.Add("-------------------------------------------------------------------------------------------------------------------------------");

            // Data Binding
            dataGridView1.DataSource = gBalanceList;
        }
    }
}

 

DataGridView는 데이터가 없는 초기상태에서 회색 바탕의 빈칸이 덩그러니~ 표시된다. (디자인적으로 보기 싫게....)

개인 취향에 맞게 초기에 일정 갯수의 빈칸을 넣고, 실제 데이터가 발생시에 교체하거나 아니면 새로 다시 그리거나 나는 등의 방식으로 개발하면 좀 더 미려한 프로그램이 될 것이다.

 

 

최근 회사의 상장 준비 때문에 바빠서 (드디어 11월에 코스닥 입성!!! 갬동 ㅠㅠ) 퇴근 후라도 조금씩 잡았던 개발을 손놨었는데 이거 조금 손댔다고 다시 개발이 하고 싶어진다.....

현재 사용하고 있는 프로그램의 단점이 하나 있어 새로운 프로그램을 설계 중인데

다음 포스팅에 새로운 프로그램 개발의 시작과 함께 그 내용을 다뤄 봐야 겠다.

 

Posted by [ 브랜든 ]
,

이번 포스팅은 단일 데이터 조회에 이어 xingAPI의 [반복 데이터 조회]에 대해 포스팅 해본다.

단일 데이터 조회처럼 C#으로 코드를 짤 것이며, xingAPI COM 개발 가이드에는 엑셀 VBA를 이용한 프로그램의 코드가 소개되어 있다.

 

이름에서 알 수 있듯이 반복적으로 구성되어 있는 데이터를 가져올때 사용하는 TR이다.

자동매매 프로그램에 어떤 기능을 넣느냐에 따라 다르겠지만, 이 TR이 많이 필요해 보이진 않는다.

 

나는 계좌에 매수되어있는 종목들의 정보를 가져오는데에 사용하겠다.

 

사실은 본인이 사용하고 싶다고 하더라도 xingAPI에서 해당 기능을 제고하는 TR에 대해서만 사용 가능하다. 

먼저 원하는 기능에 맞는 TR을 찾아보자. 

 

잔고 정보를 가져오기 위해 xingAPI의 '[t0424] 주식잔고2' TR을 사용하면 되겠다.

적절한 TR을 찾기 위해 DevCenter에 접속한 후 각 TR들을 눌러보면 뒤에 'OCCURS'라고 붙는 TR들이 존재한다. 이 TR들은 반복(OCCUR) 데이터 요청이 가능하다는 얘기다. 

 

이 TR을 사용해서 내 계좌에 매수되어 있는 종목들의 정보를 한번에 받아와 보자.

 

데이터를 수신하는 방법만 다를 뿐 단일데이터 조회 방식과 같다. 

 

반복데이터 조회는 XAQuery 객체를 사용.

 

  1. XAQuery 선언
  2. XAQuery 생성
  3. RES 등록
  4. 입력데이터 설정
  5. 입력데이터를 서버로 전송하기
  6. 서버로부터 데이터 받기

프로그램은 이전 포스팅에 사용한 프로그램에 기능을 추가해 구현해 본다.

 

xingAPI 테스트 프로그램, 종목 출력때문에 가로로 조금 넓혔다.

이제 구현 후 테스트 해보자.

종목이 여러개 있어야하므로 모의투자로 접속해서 테스트 해보자.

 

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;

        // xingAPI TR 클래스 선언
        XAQueryClass CSPAQ12200;        // 자산현황 조회
        XAQueryClass t0424Occurs;       // 잔고조회 - 종목리스트

        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);
            // xingAPI TR 이벤트 등록
            CSPAQ12200 = new XAQueryClass();
            CSPAQ12200.ResFileName = @"C:\eBEST\xingAPI\Res\CSPAQ12200.res";  // xingAPI 서버로부터 수신한 응답 메시지를 COM 버전의 API에서 사용할 수 있도록 Format을 맞춰주는 파일
            CSPAQ12200.ReceiveData += CSPAQ12200ReceiveData;  // xingAPI 서버로부터 CSPAQ12200의 응답 메시지를 수신할 함수 등록

            // xingAPI 반복 데이터 조회 TR 이벤트 등록
            t0424Occurs = new XAQueryClass();
            t0424Occurs.ResFileName = @"C:\eBEST\xingAPI\Res\t0424.res";
            t0424Occurs.ReceiveData += t0424ReceiveData;
        }
        
        // HTS 서버 접속
        private void Btn_Conn_Server_Click(object sender, EventArgs e)
        {
            string serverType = "demo.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)
        {
            // CSPAQ12200 TR의 InBlock 데이터 세팅
            CSPAQ12200.SetFieldData("CSPAQ12200InBlock1", "RecCnt", 0, "00001");
            CSPAQ12200.SetFieldData("CSPAQ12200InBlock1", "MgmtBrnNo", 0, " ");
            CSPAQ12200.SetFieldData("CSPAQ12200InBlock1", "AcntNo", 0, gAccount);
            CSPAQ12200.SetFieldData("CSPAQ12200InBlock1", "Pwd", 0, "실제계좌비번");
            CSPAQ12200.SetFieldData("CSPAQ12200InBlock1", "BalCreTp", 0, "0");

            // TR 요청
            var result = CSPAQ12200.Request(false);
            if (result > 0)
                MessageBox.Show("CSPAQ12200 요청 완료");
            else
                MessageBox.Show("CSPAQ12200 요청 실패");
        }

        private void CSPAQ12200ReceiveData(string trCode)
        {
            // 요청한 CSPAQ12200 TR에 대한 응답 메시지 수신
            // CSPAQ12200 TR의 OutBlock2에서 필요한 데이터 추출
            // 1. 예탁자산 총액
            var temp_value = CSPAQ12200.GetFieldData("CSPAQ12200OutBlock2", "DpsastTotamt", 0);
            decimal estimated_assets = Convert.ToDecimal(temp_value);
            // 2. D+1 예수금
            temp_value = CSPAQ12200.GetFieldData("CSPAQ12200OutBlock2", "D1Dps", 0);
            decimal d1_deposit = Convert.ToDecimal(temp_value);
            // 3. D+2 예수금
            temp_value = CSPAQ12200.GetFieldData("CSPAQ12200OutBlock2", "D2Dps", 0);
            decimal d2_deposit = Convert.ToDecimal(temp_value);
            // 4. 손익률
            temp_value = CSPAQ12200.GetFieldData("CSPAQ12200OutBlock2", "PnlRat", 0);
            double valuation_rate = Convert.ToDouble(temp_value);
            // 5. 잔고평가금액
            temp_value = CSPAQ12200.GetFieldData("CSPAQ12200OutBlock2", "BalEvalAmt", 0);
            decimal evaluation_amounts = Convert.ToDecimal(temp_value);

            string text = String.Format("[예탁자산총액 : {0:#,0}] [D+1 예수금 : {1:#,0}] [D+2 예수금 : {2:#,0}] [손익률 : {3:f2}%] [잔고평가금액 : {4:#,0}]",
                estimated_assets, d1_deposit, d2_deposit, valuation_rate, evaluation_amounts);
            MessageBox.Show(text);
        }

        // 계좌잔고 조회 [반복데이터]
        private void Btn_Get_Items_Click(object sender, EventArgs e)
        {
            // 잔고 리스트 조회
            string inform = String.Format("-- 잔고 리스트 조회");
            Realtime_Log.Items.Add(inform);

            t0424Occurs.SetFieldData("t0424InBlock", "accno", 0, gAccount);
            t0424Occurs.SetFieldData("t0424InBlock", "passwd", 0, "실제계좌비번");
            t0424Occurs.SetFieldData("t0424InBlock", "prcgb", 0, "1");
            t0424Occurs.SetFieldData("t0424InBlock", "chegb", 0, "2");
            t0424Occurs.SetFieldData("t0424InBlock", "dangb", 0, "0");
            t0424Occurs.SetFieldData("t0424InBlock", "charge", 0, "0");
            t0424Occurs.SetFieldData("t0424InBlock", "cts_expcode", 0, " ");
            int result = t0424Occurs.Request(false);
            if (result < 0)
            {
                inform = string.Format("[보유계좌] 계좌 잔고리스트(t0424) 요청이 실패했습니다. ret={0}", result);
                Realtime_Log.Items.Add(inform);
            }
        }

        private void t0424ReceiveData(string trCod)
        {
            // 전체 잔고 종목 횟수 가져오기
            var count = t0424Occurs.GetBlockCount("t0424OutBlock1");
            string inform = String.Format("잔고조회 종목 리스트 갯수 : {0}", count);
            Realtime_Log.Items.Add(inform);

            Realtime_Log.Items.Add("-------------------------------------------------------------------------------------------------------------------------------");           
            string title = String.Format("| {0,20} | {1,10} | {2,10} | {3,10} | {4,10} | {5,10} | {6,10} | {7,10} | {8,10} |", "종목명", "종목코드", "현재가", "매입가", "잔고수량", "매입금액", "평가금액", "평가손익", "수익률");
            Realtime_Log.Items.Add(title);
            Realtime_Log.Items.Add("-------------------------------------------------------------------------------------------------------------------------------");

            // 가져온 종목 횟수 만큼 반복하면서 실제 종목 정보 가져오기
            for (int index = 0; index < count; ++index)
            {
                var temp_value = t0424Occurs.GetFieldData("t0424OutBlock1", "expcode", index); // 종목코드
                string expcode = Convert.ToString(temp_value);
                temp_value = t0424Occurs.GetFieldData("t0424OutBlock1", "hname", index);       // 종목명
                string hname = Convert.ToString(temp_value);
                temp_value = t0424Occurs.GetFieldData("t0424OutBlock1", "price", index);       // 현재가
                decimal price = Convert.ToDecimal(temp_value);
                temp_value = t0424Occurs.GetFieldData("t0424OutBlock1", "pamt", index);        // 매입가
                decimal pamt = Convert.ToDecimal(temp_value);
                temp_value = t0424Occurs.GetFieldData("t0424OutBlock1", "janqty", index);      // 잔고수량
                int janqty = Convert.ToInt16(temp_value);
                temp_value = t0424Occurs.GetFieldData("t0424OutBlock1", "mamt", index);        // 매입금액
                decimal mamt = Convert.ToDecimal(temp_value);
                temp_value = t0424Occurs.GetFieldData("t0424OutBlock1", "appamt", index);      // 평가금액
                decimal appamt = Convert.ToDecimal(temp_value);
                temp_value = t0424Occurs.GetFieldData("t0424OutBlock1", "dtsunik", index);     // 평가손익
                decimal dtsunik = Convert.ToDecimal(temp_value);
                temp_value = t0424Occurs.GetFieldData("t0424OutBlock1", "sunikrt", index);     // 수익율
                double sunikrt = Convert.ToDouble(temp_value);

                string contents = String.Format("| {0,20} | {1,10} | {2,10} | {3,10} | {4,10} | {5,10} | {6,10} | {7,10} | {8,10} |",
                    expcode, hname, price, pamt, janqty, mamt, appamt, dtsunik, sunikrt);

                Realtime_Log.Items.Add(contents);
            }
            Realtime_Log.Items.Add("-------------------------------------------------------------------------------------------------------------------------------");
        }
    }
}

아래의 이전 포스팅을 참조해서 [서버연결]-[로그인]-[계좌선택]까지 완료해보자.  

2020/08/24 - [Trading] - [Trading] xingAPI - 서버연결, 로그인, 보유계좌 불러오기

 

[Trading] xingAPI - 서버연결, 로그인, 보유계좌 불러오기

오늘은 xingAPI를 사용한 자동매매 프로그램을 만들기 위한 기본 작업들에 대한 포스팅을 하려 한다. 이 내용은 이베스트 투자증권의 xingAPI 소개자료에도 동일하게 소개되어 있는 내용이지만, 엑�

brandon-dev.tistory.com

서버접속 -> 로그인 -> 계좌 선택까지 완료한 화면.

이제 '계좌잔고 조회' 버튼을 누르면 종목 리스트가 표시될 것이다. 

계좌에 매수되어 있는 종목 리스트 표시.

이제 이 정보들을 객체 List로 저장하도록 해서 DataGridView에 Binding하면, 실시간으로 계좌 종목 리스트를 갱신하며 리스트로 보여줄 수 있겠다. 

 

다음은 DataGridView에 Binding하는 법을 포스팅 해보자. 

 

 

Posted by [ 브랜든 ]
,

앞서 포스팅한 xingAPI의 여러 데이터 조회 방법 중 [단일 데이터 조회]에 대해 포스팅 해본다.

보면 알겠지만 C#을 사용해서 자동 매매 프로그램을 만들고 있기 때문에 C#으로 코드를 짤 것이다. 

xingAPI COM 개발 가이드에는 엑셀 VBA를 이용한 프로그램의 코드가 소개되어 있다.

 

개발이 익숙한 사람이라면 COM 형식의 개발은 라이브러리 참조 후 객체의 함수들을 보면 대충 어떤 경우에 어떤 함수를 사용하면 될지 감이 온다. 

 

우선 xingAPI 가이드에 나온 내용부터 보자. 아래는 가이드에서 정리한 [단일 데이터 조회] 순서이다.

 

단일데이터 조회는 XAQuery 객체를 사용.

 

  1. XAQuery 선언
  2. XAQuery 생성
  3. RES 등록
  4. 입력데이터 설정
  5. 입력데이터를 서버로 전송하기
  6. 서버로부터 데이터 받기

물론 가이드에는 위의 과정들이 엑셀 VBA로 되어 있어서 엑셀 VBA로 프로그램을 만들 예정이면 가이드를 보고 따라 하면 된다. 나는 C#으로 위의 과정들을 만들어 본다.

 

xingAPI Test 프로그램 화면


우선 단일데이터 조회를 위해 [자산현황 조회] 기능을 사용한다. 

해당 버튼을 클릭할 때마다 1회, 단일적으로 데이터를 요청하고 수신하는 방식이다.

 

xingAPI의 요청-응답은 요청하는 함수, 그리고 서버로부터 데이터를 수신하는 함수의 두 개의 함수가 쌍을 이루는 형태로 구성된다. 

 

하나의 TR 구성은 요청과 수신의 두 개 함수가 쌍을 이루게 된다.

즉, xingAPI에서 제공되는 모든 TR 처리는 요청하는 함수와 응답을 수신하는 함수, 두 개의 함수를 구현해야지만 완성이 된다. 

 

먼저 [자산현황 조회]를 위한 TR을 찾기 위해 'DevCenter'에 접속하여 TR을 찾아보자. 

'DevCenter' 사용 법은 아래에...

2020/08/14 - [Trading] - [Trading] XingAPI - DevCenter 사용하기

 

[Trading] XingAPI - DevCenter 사용하기

키움증권 API도 마찬가지지만 이베스트 XingAPI에서도 개발할 때 유용한 도구를 제공한다. 이베스트 XingAPI에서는 'DevCenter'라는 것인데, XingAPI에서 제공하는 다양한 TR(Transaction)들을 실제로 내가 보�

brandon-dev.tistory.com

'DevCenter'의 TR 목록 중, 계좌 자산 현황 정보를 볼 수 있을 것 같은 TR은 아래처럼 4가지가 가능해 보인다.

계좌 자산 정보 조회 TR 목록

위의 4가지 종류의 TR을 하나하나 눌러가며 'OutBlock'의 항목들을 잘 살펴보고 내가 필요한 정보들을 모두 획득할 수 있는 TR을 선택한다. 

 

내가 필요로 하는 정보들은 아래와 같다.

  • 실자산총액
  • D+1 예수금
  • D+2 예수금
  • 총 매입금액
  • 평가손익
  • 평가금액
  • 손익률

나는 저 위의 4가지 TR을 모두 사용해보고 많은 시간을 삽질(?)하며 결국 [CSPAQ12200] TR을 선택했다.

나와 같은 삽질을 하지 않도록...각 TR에 대한 내용을 조금 정리해 본다.


1. CSPAQ12200 현물계좌 예수금/주문가능금액/총평가조회(API)

 

이름만 보면...내가 필요한 정보들이 다~있을 것만 같은 TR 이름이다. 하지만 '총 매입금액'과 '평가손익'에 대한 정보는 존재하지 않아 존재하는 나머지 정보들을 이용해서 두 개 정보를 얻어냈다. 간단한 수학 공식으로 얻어낼 수 있다. 

다시 CSPAQ12200에 대해 얘기해 보자. 나의 경우는 이 TR을 내가 원할 때 1회 조회함으로써 내 자산 정보를 확인할 수 있도록 하는 하는데에 사용하였는데, 의외로 주기적으로 이 정보를 요청하도록 개발하는 사람이 많은지 TR 요청 폭주로 인해서 잦은 장애가 발생한다고 한다. 그래서 이베스트 측에서는 이 TR 대신 CSPAQ22200을 새로 만들어 사용하게 했다고 했다. 하지만....CSPAQ22200은 정말 적은 정보를 제공하여 나에게는 적합하지 않은 TR이었다. 

 

2. CSPAQ12300 현물계좌 잔고내역 조회(API)

 

이 TR이야말로 내가 원하는 정보들을 다~ 제공해주는 TR이다. 하지만 공교롭게도 이 TR은 모의투자에서만 지원하고 실투자에서는 지원하지 않는다. 이유는 나도 모르겠다. 이것은 이베스트에서 공식적으로 실투자에서는 제공되지 않는다고 한 사항이며, 그걸 알기 전까지 정말 엄청난 시간 소모와 삽질을 했다...실투자용 자동매매 프로그램을 만들 경우 이 TR은 사용하지 않는다.

 

3. t0424 주식잔고2

 

이 TR도 CSPAQ12300과 마찬가지로 내가 원하는 정보들은 다~ 제공해 주는 TR이다. t0424 TR 정보를 보면 알겠지만, 이 TR 하나로 자산현황과 해당 계좌에 존재하는 체결된 종목 리스트들을 함께 전달해 준다. 설계한 내 프로그램의 기능상 이 두 가지 기능이 분리되어 사용되어야 하기에 이 TR은 자산현황 조회 시 사용하지 않고 오로지 계좌 잔고 조회에만 사용하였다. 설계한 프로그램이 굳이 이 두 가지 기능을 따로 분리할 필요가 없다면, t0424 TR을 사용하는 것을 추천한다. 이 TR을 사용하는 법은 다음 포스팅에...

 


이제 구현 후 테스트를 해보자.

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;

        // xingAPI TR 클래스 선언
        XAQueryClass CSPAQ12200;        // 자산현황 조회

        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);
            // xingAPI TR 이벤트 등록
            CSPAQ12200 = new XAQueryClass();
            CSPAQ12200.ResFileName = @"C:\eBEST\xingAPI\Res\CSPAQ12200.res";  // xingAPI 서버로부터 수신한 응답 메시지를 COM 버전의 API에서 사용할 수 있도록 Format을 맞춰주는 파일
            CSPAQ12200.ReceiveData += CSPAQ12200ReceiveData;  // xingAPI 서버로부터 CSPAQ12200의 응답 메시지를 수신할 함수 등록
        }
        
        // 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)
        {
            // CSPAQ12200 TR의 InBlock 데이터 세팅
            CSPAQ12200.SetFieldData("CSPAQ12200InBlock1", "RecCnt", 0, "00001");
            CSPAQ12200.SetFieldData("CSPAQ12200InBlock1", "MgmtBrnNo", 0, " ");
            CSPAQ12200.SetFieldData("CSPAQ12200InBlock1", "AcntNo", 0, gAccount);
            CSPAQ12200.SetFieldData("CSPAQ12200InBlock1", "Pwd", 0, "실제비밀번호");
            CSPAQ12200.SetFieldData("CSPAQ12200InBlock1", "BalCreTp", 0, "0");

            // TR 요청
            var result = CSPAQ12200.Request(false);
            if (result > 0)
                MessageBox.Show("CSPAQ12200 요청 완료");
            else
                MessageBox.Show("CSPAQ12200 요청 실패");
        }

        private void CSPAQ12200ReceiveData(string trCode)
        {
            // 요청한 CSPAQ12200 TR에 대한 응답 메시지 수신
            // CSPAQ12200 TR의 OutBlock2에서 필요한 데이터 추출
            // 1. 예탁자산 총액
            var temp_value = CSPAQ12200.GetFieldData("CSPAQ12200OutBlock2", "DpsastTotamt", 0);
            decimal estimated_assets = Convert.ToDecimal(temp_value);
            // 2. D+1 예수금
            temp_value = CSPAQ12200.GetFieldData("CSPAQ12200OutBlock2", "D1Dps", 0);
            decimal d1_deposit = Convert.ToDecimal(temp_value);
            // 3. D+2 예수금
            temp_value = CSPAQ12200.GetFieldData("CSPAQ12200OutBlock2", "D2Dps", 0);
            decimal d2_deposit = Convert.ToDecimal(temp_value);
            // 4. 손익률
            temp_value = CSPAQ12200.GetFieldData("CSPAQ12200OutBlock2", "PnlRat", 0);
            double valuation_rate = Convert.ToDouble(temp_value);
            // 5. 잔고평가금액
            temp_value = CSPAQ12200.GetFieldData("CSPAQ12200OutBlock2", "BalEvalAmt", 0);
            decimal evaluation_amounts = Convert.ToDecimal(temp_value);

            string text = String.Format("[예탁자산총액 : {0:#,0}] [D+1 예수금 : {1:#,0}] [D+2 예수금 : {2:#,0}] [손익률 : {3:f2}%] [잔고평가금액 : {4:#,0}]",
                estimated_assets, d1_deposit, d2_deposit, valuation_rate, evaluation_amounts);
            MessageBox.Show(text);
        }

        // 계좌잔고 조회 [반복데이터]
        private void Btn_Get_Items_Click(object sender, EventArgs e)
        {


        }
    }
}

TR을 사용할 때 어떤 값을 입력해야 하는지, 그리고 응답 메시지에서 어떤 값을 추출해야 하는 지는 'DevCenter'에서 TR 정보를 보면 쉽게 알 수 있다. 

 

빨간색 표시 부분에서 InBlock과 OutBlock의 필드 값들을 확인하여 사용.

TR을 사용하기 전에 반드시 해야하는 작업이 있다.
바로 res 파일들을 다운로드하여 저장해 두고 있어야 하는데 해당 작업은 'DevCenter'에서 제공한다.

필요한 TR만 다운로드할 수 있긴 하지만, 그냥 전부다 다운로드하여두면 편하다.
다운로드하면 아래 경로에 자동적으로 저장이 되고, 개발할 때 해당 경로의 .res 파일을 ResFileName에 넣어 주면 된다.

 

아래의 이전 포스팅을 참조해서 [서버연결]-[로그인]-[계좌선택]까지 완료해보자.  

2020/08/24 - [Trading] - [Trading] xingAPI - 서버연결, 로그인, 보유계좌 불러오기

 

[Trading] xingAPI - 서버연결, 로그인, 보유계좌 불러오기

오늘은 xingAPI를 사용한 자동매매 프로그램을 만들기 위한 기본 작업들에 대한 포스팅을 하려 한다. 이 내용은 이베스트 투자증권의 xingAPI 소개자료에도 동일하게 소개되어 있는 내용이지만, 엑�

brandon-dev.tistory.com

위 상태에서 [자산현황 조회] 버튼을 누르면 메시지 박스에 결과가 표시될 것이다.

 

xingAPI의 기본 TR 요청/응답처리를 구현해 봤다. 모든 TR이 이와 같은 원리로 동작하니 이것만 할 수 있다면 다른 TR들에 대한 구현도 어렵지 않을 것이다.

Posted by [ 브랜든 ]
,

나는 Back-End 개발자여서 개인적으로 사용하기 위한 Linux 서버가 하나 필요했다.

하지만 대부분 간단한 C/C++ 프로그램이나 Python 또는 Database 사용 용도였기 때문에 성능이 좋은 PC가 필요한 게 아니어서 라즈베리파이를 하나 구입했었다. 

 

https://namu.wiki/w/%EB%9D%BC%EC%A6%88%EB%B2%A0%EB%A6%AC%20%ED%8C%8C%EC%9D%B4(%EC%BB%B4%ED%93%A8%ED%84%B0)

 

라즈베리 파이(컴퓨터) - 나무위키

저장장치, 전원, USB, 케이스, 모니터, 사운드, 방열판, 카메라, 디스플레이 등에 대해 다루고 있다. 라즈베리 파이도 물론 의의가 있고 충분히 좋은 보드이지만, 용도에 맞지 않는다면 당연히 의��

namu.wiki

내가 갖고 구매한 모델은 '라즈베리파이 3B+'로 Ubuntu MATE를 설치하여 사용하고 있었다. 

상시 전원 ON 상태로 집의 중앙 유무선 공유기의 Port forwarding으로 언제 어디서든 접속 가능한 상태로 사용해 왔었는데, 최근 추가로 라즈베리파이를 구매해야 할 일이 생겨 적절한 케이스를 찾고 있었다. 

 

기존에 아크릴로 된 라즈베리파이용 Case와 기본 제공되는 라즈베리파이용 쿨링팬을 사용했는데....

사용해본 사람을 알겠지만 기본재공 팬의 소음이 정말 장난이 아니다. 

이 부분이 항상 불만이었는데 이 참에 3개의 라즈베리파이를 보관할 수 있는 전용 Rack을 아크릴로 만들어 보려 한다.

(사실 4개 이상 정도를 보관할 수 있는 라즈베리파이 전용 Rack 기성품은 검색해보면 많이 나온다.)

 

딱 내가 원하는 크기와 원하는 개수만 보관할 수 있고, 내가 사용하고 싶은 저소음의 효과 좋은 쿨링팬을 사용하기 위한 나만의 라즈베리 파이 Rack이다. (실제로 만드는 것은 매우 쉽다...집에서 아크릴 가공을 할 수 없을 뿐...)


1. 라즈베리파이 Rack 설계하기

3D 모델링을 할 수 있는 금손이라면...좋겠지만...그건 못하고...

PPT로 대충 그려본다. 아크릴이니까...투명 효과...

라즈베리파이 Rack 만들기 - 전체 구성도

  • 모양은 사각형의 평범한 Case 형태.
  • 크기는 120mm 쿨링팬의 크기에 맞춰 120 mm * 120 mm * 80 mm 크기로 제작.
  • 좌측면에는 120mm의 쿨링팬 장착.
  • 우측면에는 통풍 및 Micro 5V/3A 전원과 HDMI를 연결할 수 있도록 3개의 오픈 공간 생성.
  • 전면 및 상/하면은 통 아크릴 장착.
  • 후면은 라즈베리파이를 장착한 아크릴 판(지지대)을 탈착 할 수 있도록 개방.
  • 운반이 쉽도록 손잡이 장착.

후면에서 보면 아래처럼 라즈베리파이가 장착된 3개의 지지판이 간격을 유지해서 3개 층으로 자리 잡게 될 것이다.

라즈베리파이 Rack 만들기 - 후면 구성도

실은 현재 1개짜리 Prototype을 만들어서 이미 사용 중인데 쿨링 효과는 정말 100점 중에 100점이다. 

소음도 없고 한여름에 더운 내 방에서도 수일째 아주 열심히 잘~ 돌아간다. 

 

이제 각 부분의 아크릴 조각들을 하나하나 살펴보자.

라즈베리파이 Rack 만들기 - 각 부분별 아크릴 조각 사이즈및 갯수

구분되기 쉽도록 위의 전체 구성도와 맞춰 각 Parts에 색을 입혔다.

 

구멍이나 공간이 비어있는 아크릴 판의 경우 저렇게 가공해주는 곳이 있으면 좋겠지만 소량은 원하는 모양으로 가공해주는 업체를 찾기 어려웠다. 아크릴 칼을 사서 내가 직접 해봤지만...아크릴판 여럿 날려먹었다...

그냥 조각으로 사서 아크릴 본드로 붙여 만드는 게 가장 베스트다.


2. 아크릴판 주문하기

아크릴판을 주문할 때 사이즈를 잘 고려해서 주문해야 한다. 

아크릴 본드를 사용해서 접합시킬 것이기 때문에 아크릴 두께만큼 겹쳐지는 부분을 고려해야 삐져나오지 않고 정사각형의 모양을 만들 수 있기 때문이다. 때문에 위의 Parts 그림에서 [전면 판]만 6mm가 더 길다.

 

Prototype은 2T로 주문해서 만들었는데 생각보다 부실한 느낌이라 이번엔 3T로 주문해 본다. 알록달록 여러 가지 색의 아크릴 판이 있지만 내부가 훤히 보이는 투명이 좋다. 

 

  1) 전면 판

    - 126mm * 80mm (1EA)

  2) 상/하판

    - 120mm * 80mm (2EA)

  3) 우측판

    - 120mm * 20mm (2EA)

    - 80mm * 20mm (2EA)

    - 80mm * 16mm (2EA)

  4) 좌측판 (FAN 장창)

    - 120mm * 20mm (2EA)

    - 80mm * 20mm (2EA)

  5) 라즈베리 장착판

    - 120mm * 75mm (3EA)

  6) 라즈베리 장착판 지지대

    - 120mm * 10mm (12EA)

    

총 28개의 아크릴 조각 주문 완료.

 

그리고 이제 쿨링팬을 주문해야하는데...우선 라즈베리파이 기본 제공 쿨링팬보다 큰 120mm 이기 때문에...혹시나 전력문제가 발생하지 않을까 하여 USB 연결용으로 구매해 본다. 

그래도 PC 케이스라도 생각하고 만드는데 LED는 하나 달아주고 싶군...깔끔한 WHITE LED로...

http://prod.danawa.com/info/?pcode=5304248

 

[다나와] COOLERTEC USB-12025 (BLUE)

최저가 6,890원

prod.danawa.com

++ 쿨링팬을 그냥 달았더니 무심코 만지다가 돌고 있는 팬에 자꾸 손가락이 치였다.

먼지 유입도 고려해볼 때 먼지필터도 같이 달아줘야 할 것 같아 아래 제품을 찾았다. 아쉽게도 White는 품절....

심플하니 딱 내가 찾던 바로 그 제품!!

https://smartstore.naver.com/comtu/products/4645514214?NaPm=ct%3Dke5o5dg8%7Cci%3D405cafd795db666d83cbec993e76b0130a1c55f3%7Ctr%3Dslsl%7Csn%3D684031%7Chk%3De2c329ea87fb3e21c6386c822565eddcbc537092

 

컴퓨터먼지필터 : 컴튜

컴퓨터먼지필터

smartstore.naver.com

모든 재료의 주문이 끝났다!!


3. 라즈베리파이 Rack 조립하기

우선 위에서 주문한 대로 도착한 아크릴 조각들과 쿨링팬이다. 

 

라즈베리파이 Rack 만들기 - 실물 재료 및 공구

일반 쿨러는 밋밋하니...화이트 LED가 장착된 쿨러를 사고, 쿨러가 부착되는 아크릴판을 형광색으로 구매해 봤다.

다 만들면...형광빛 나는 Rack이 되어 있겠지...?

 

 

먼저 집에 있는 전동 드릴로 아크릴 판들에 구멍을 내야 한다. 

쿨러가 장착될 연두색 아크릴 판과 라즈베리파이가 장착될 노란색 아크릴 판을 구멍 내면 된다. 참고로 쿨러 고정 나사와 라즈베리파이 고정 나사의 직경이 다르니 당연히 다른 크기의 드릴로 작업해야 한다. (드릴 작업은 당연히 안전하게 홈이 있는 바닥에서 한다.)

처음 아크릴을 받으면 양면에 비닐이 씌워져 있기 때문에 그곳에 표시를 하고 구멍을 뚫은 후 비닐을 벗기면 깔끔하다.

 

++ 위의 구성에는 없지만 상만에 부착할 손잡이를 별도 구해했다. Rack의 이동이 쉽도록...

따라서 상판도 손잡이 위치에 맞게 구멍을 내준다. 

 

쿨러 구멍에 맞게 드릴로 구멍을 내준다.

++ 쿨러의 구멍에 맞게 아크릴 구멍을 뚫기 위해 테이프로 쿨러에 아크릴판을 고정 후 구멍을 뚫었다. 

나사가 4방을 통해 고정되기 때문에 구멍이 조금 삐뚤게 뚫릴 경우 고정되지 않을 수 있다. 구멍 뚫을 위치에 표시 후에 하는 것보다 위 방법으로 하는 것이 가장 좋을 듯싶다. 

 

아크릴 본드로 부착 전 우측/좌측 판 모양을 만들어 보고 아크릴 본드로 붙여서 완성한다. 

좌측판에 구입한 쿨러 팬을 장착하고, 라즈베리 장착 판에도 라즈베리파이를 장착해 보자. 

이제 시작이다....

라즈베리 장착판 지지대를 일정한 간격, 그리고 반대편과 동일한 위치의 수평으로 붙이는 게 젤 어렵다...

좀 비스듬하지만... 괜찮다.

 

라즈베리파이 Rack 만들기 - 내부 조립이 끝난 상태

 

라즈베리 장착판 지지대까지 완료되었으면 이제 마무리 조립만 하면 끝이다. 

전면 판의 높이를 3T 두께에 맞춰 늘려놨으니, 우측/좌측 판이 상/하판 안으로 들어오게 조립하면 된다. 


짠! 드디어 완성~

 

라즈베리파이 Rack 만들기 - 완성

 

이제 라즈베리파이를 장착하고 켜보자. 손잡이를 단건 정말 신의 한 수...

형광색 빛을 내는게 더 보기좋긴 좋다.

 

실은 지금 이 포스팅을 하기 전에 시험 삼아 만들었던 라즈베리파이 Rack 이 있었다. 

 

라즈베리파이 Rack Mark-Ⅰ

 

120 * 120의 쿨러 크기를 기준으로 만들었기에 라즈베리파이의 크기와 상관없이 좀 더 큰 Rack이 만들어졌고, 게다가 통 아크릴 판을 아크릴 칼로 네모난 구멍을 낼 수 있을 거라 생각했기에 많은 우여곡절(아크릴판이 많이 깨짐....)이 있었다. 

게다가 장착판의 정 가운데 부분에 라즈베리파이를 장착해서 랜선을 꽂고 뽑기가 매우 불편...

그래서 불편했던 부분들을 개선해서 이번에 새로 만든 것. 

라즈베리파이 Rack Mark-Ⅱ 인 셈.

 

 

돈 주고 사도 크게 부담되는 가격이 아니지만 가능한 수준이라면 이렇게 만들어 쓰는 것도 나쁘지 않은 것 같다.

안녕 라즈베리파이 Rack Mark-Ⅰ~

 

 

Posted by [ 브랜든 ]
,