[xingAPI][차트 데이터 수집기 만들기](6) 주식마스터 조회

반응형

증권사 API로 차트 데이터를 수집하려면 종목코드를 알아야 하는데 전 종목에 대한 종목코드는 주식마스터조회 API를 호출하여 얻을 수 있습니다. 이베스트투자증권의 xingAPI의 TR 에는 t8430(주식종목조회), t8436(주식종목조회 API용), t9945(주식마스터조회 API용)가 있는데 그중 t8430(주식종목조회)를 이용하여 전체 주식종목(주식마스터)을 조회해 보겠습니다.

 

1. 주식마스터 API 사용방법 알아보기

먼저 DevCenter를 통해 t8430의 속성을 확인합니다. 초당 2회 조회할 수 있고 Block Mode로 응답이 온다고 되어 있네요. Request는 t8430Inblock에 속성을 설정하여 전송하고 응답은 t8430OutBlock에 담겨서 옵니다. 1개 종목의 정보가 t8430OutBlock 1개의 메모리 공간에 담겨 오고, 종목이 3200개면 T8430OutBlock이 3200개만큼 할당되어 옵니다.

Block Mode이면 이 OutBlock이 메모리공간에 연속적으로 할당되어 하나의 Block으로 응답이 오니까 수신 패킷의 Data사이즈를 sizeof(t8430OutBlock)으로 나누면 몇 개의 종목정보를 수신했는지 알 수 있습니다.

DevCenter의 t8430 속성

아래는 실제 API 호출에 필요한 헤더파일 t8430.h의 내용입니다.

t8430.h

이전 글에서 xingAPI 응답메시지 수신을 담당하는 CDlgXingMsg 스레드를 생성하고 MainLoop 역할을 할 CThreadLooper 스레드를 생성하였습니다. CThreadLooper에서 Request, CDlgXingMsg에서 Response를 담당하는 기본 구조는 완성되었지만 수신데이터의 원활한 처리를 위하여 CThreadDequeue라는 이름의 스레드를 추가 생성하겠습니다. CDlgXingMsg에서 수신패킷을 바로 처리하는 것이 아니라 CDlgXingMsg는 받은 패킷을 메모리 복사를 통해 다른 버퍼에 저장하고 Queue에 Push 한 후 CThreadDequeue에 패킷 수신을 알리는 이벤트를 발생시킵니다. 그럼 CThreadDequeue는 EVENT_WAIT 상태에서 깨어나 Queue의 데이터를 Pop 하여 처리하게 됩니다. 일종의 Producer-Consumer 구조가 되는 것이죠.

그리고 2종의 Data Structure를 추가로 정의합니다. 각 TR의 Property와 Method를 관리할 클래스 CDataStore와 비동기 데이터 처리를 관리하는 클래스 CAsyncControl입니다.

 

2. CDataStore 자료 구조 정의

"t8430.h"헤더파일의 t8430OutBlock을 그대로 TST__DS_T8430_OUT_OBJ로 정의하였습니다.

그리고 주식 전체 종목이 3200개 정도 되는 것을 파악했으니 #define _COUNT_OF_TST_DS_T8430_OUT_OBJ (3300)으로 설정하고 TST_DS_STOCK_MASTER를 정의하였습니다. 추가로 Hash Map 구조체를 정의하였습니다. 3000개의 종목 중 3000번째 배열에 저장되어 있는 종목코드를 찾기 위해 3000번 비교하지 않도록 Map에 <종목코드, 배열인덱스>로 저장해 두고 종목코드만 넣으면 해당 종목의 인덱스를 바로 반환받을 수 있게 합니다.

CDataStore 코드

3. CAsyncControl 정의

TR을 Request 함수를 호출하면 Request 함수는 Request ID를 반환하고 종료됩니다. 서버 응답은 일정시간 경과 후 해당하는 MSG_ID를 통해 Callback 함수의 파라미터로 넘어옵니다. Request와 Response가 sync가 아닌 async로 처리되고 있기 때문에 아래와 같이 관련 자료구조를 정의하였습니다. 위 CDataStore의 TST_DS_STOCK_MASTER의 TEN_DataState는 데이터가 요청 중인지, 저장완료상태인지, 비어있는지 등을 나타냅니다. 이 변수를 활용하면 굳이 비동기처리를 하지 않아도 Polling 방식으로 해당 메모리를 주기적으로 감시하며 데이터 수신여부를 알 수 있기도 합니다. Polling 방식과 Callback Interrupt 방식 중 어떤 방식으로 응답을 확인할 것인지는 해당 TR을 Request 하는 Caller의 선택입니다. Callback Function Pointer를 설정하지 않으면 Caller가 Polling을 하고 있으면 되고, 아니라면 Callback Function이 호출될 때까지 대기하고 있으면 됩니다.

관련 자료 구조

TST_XING_MSG_ITEM은 MSG_ID(ex.MSG_ID_XM_RECEIVE_DATA)와 WPARAM, LPARAM을 담는 기본 메시지 구조체입니다.(afx_msg LRESULT CDlgXingMsg::OnMsgIdXmReceiveData(WPARAM wParam, LPARAM lParam))

 

4. t8430 조회 방법

t8430 TR 조회를 예로 설명하겠습니다.

CTreadLooper의 MainState가 REQUEST_MASTER 면 t8430 TR 조회를 합니다. 이를 위해 int iRequest_T8430(DATASTORE::TEN_DS_MARKET_CAT enMkCat, ASYNCCONTROL::PTST_TR_CALLBACK pstCb)이라는 별도의 Helper Function을 작성하였는데 첫 번째 파라미터로 코스피, 코스닥 구분값을 넣고 두 번째 파라미터로 콜백구조체 포인터를 넘겨줍니다. TST_TR_CALLBACK의 pvCbFunc는 수신데이터를 처리할 함수포인터, pvPreCbFunc는 수신데이터처리 함수 호출 전 호출하는 함수포인터, pvPostCBFunc는 수신데이터 처리 함수 호출 후 호출하는 함수포인터입니다. pvPreCbFunc와 pvPostCbFunc를 통해 비동기 처리를 원활하게 할 수 있습니다. stParams의 Opaque 변수에는 Caller와 Callee(수신데이터 처리 함수) 간 약속한 필요한 값을 넘겨주면 됩니다.

조회 코드

xingAPI Request() 호출이 성공하면 0 < RequestID < 256 의 값을 반환하는데 이 값을 인덱스로 하여 CAsyncControl의

TST_TR_REQUEST_STATUS[256];에 Request에 사용한 TST_TR_REQUEST_ITEM을 임시 저장하고 있다가 Callback에 넘겨주는 것입니다.

t8430 API 호출 코드

Request 호출이 정상적으로 처리되면 CDlgXingMsg의 OnMsgIdXmReceiveData에 수신데이터가 넘어옵니다.

그럼 이 수신데이터가 어떤 Request의 응답인지 알아야겠죠. LPARAM이 RECEVE_PACKET구조체 타입이기 때문에 RECEVE_PACKET::nRqID값을 통해 Request ID를 알 수 있고 이 Request ID에 해당하는 Request ITEM은 CAsyncControl의 TR_REQUEST_STATUS ARRAY에 저장해 뒀으므로 거기서 REQUEST ITEM을 꺼낸 뒤 수신한 WPARAM과 LPARAM을 REQUEST_ITEM::pXingMsg에 붙여서 Queue에 PUSH 하면 됩니다. Push 후에 SetEvent(m_hEvent_Wakeup_ThreadDequeue) 호출을 하면 Dequeue 스레드가 깨어나 Queue에 뭐가 들어왔나 확인 후 처리함수를 호출하게 됩니다.

응답 수신부 코드

ThreadDequeue에 이벤트가 발생하면 Queue를 뒤적여서 수신데이터를 꺼내고 어떤 MSG_ID를 가진 패킷인지 확인 후 ProcessMsgxxxx()를 호출합니다.

데이터 큐 코드

아까 CThreadLooper에서 iRequest_t8430() 호출할 때 Callback parameter로 vTRCb_T8430_UpdateMaster를 설정했죠. 그럼 pvPreCbFunc, vTRCb_T8430_UpdateMaster, pvPostCbFunc 순으로 호출됩니다.

수신데이터 처리 콜백 호출

vTRCb_T8430_UpdateMaster()에서는 t8430OutBlock의 수만큼 for() 구문을 돌면서 주식마스터 구조체에 데이터를 저장합니다. 주식마스터에 저장을 끝내면 Hash Map에도 <종목코드, 주식마스터테이블 인덱스>를 저장해 줍니다.

/*
 * @fn vTRCb_T8430_UpdateMaster
 * @ASYNCCONTROL::PTST_PARAMS_TR_CALLBACK::pOpaque1: PTST_DS_STOCK_MASTER
 * @ASYNCCONTROL::PTST_PARAMS_TR_CALLBACK::pOpaque2: PTST_DS_STOCK_MASTER_INDEX_MAP
*/
void vTRCb_T8430_UpdateMaster(ASYNCCONTROL::PTST_TR_REQUEST_ITEM pRq, ASYNCCONTROL::PTST_PARAMS_TR_CALLBACK pParams)
{
	PTST_DS_STOCK_MASTER pstMaster;
	PTST_DS_STOCK_MASTER_INDEX_MAP pstIndex;
	PTST__DS_T8430_OUT_OBJ p;
	LPRECV_PACKET pRcv;
	LPt8430OutBlock pOut;
	int iCount;	// number of outblocks received
	TCHAR tchBuf[16 * sizeof(TCHAR)];
	const int ciSLOTS = sizeof(TST_DS_STOCK_MASTER::astStockMaster) / sizeof(TST__DS_T8430_OUT_OBJ);	// number of slots(TST__DS_T8430_OUT_OBJ) in TST_DS_STOCK_MASTER

	DPRINTF("ENTER\n");

	if (NULL == pRq) { return; }

	if (NULL == (pRq->pXingMsg)) { return; }
	
	if (NULL == pParams) { return; }

	if ((NULL == pParams->pOpaque1) || (NULL == pParams->pOpaque2)) { return; }

	pstMaster = (PTST_DS_STOCK_MASTER)(pParams->pOpaque1);
	pstIndex = (PTST_DS_STOCK_MASTER_INDEX_MAP)(pParams->pOpaque2);

	pRcv = (LPRECV_PACKET)(pRq->pXingMsg->lParam);
	if (_tcsncmp(pRcv->szBlockName, NAME_t8430OutBlock, _countof(NAME_t8430OutBlock)) != 0) {
		return;
	}

	/* TODO: check datastate , if not empty, clear memory and map, ??
	*/
	/* Empty HashMap */
	AcquireSRWLockExclusive(&(pstIndex->srwlock));
	pstIndex->mapIndexSH.RemoveAll();
	pstIndex->mapIndexEXP.RemoveAll();
	ReleaseSRWLockExclusive(&(pstIndex->srwlock));

	/* Init Memory */
	AcquireSRWLockExclusive(&(pstMaster->srwlock));
	pstMaster->enState = TEN_DataState::enState_SAVING;
	GetLocalTime(&(pstMaster->stTimeLastUpdate));
	pstMaster->wCountTotal = 0;
	pstMaster->wCountKOSPI = 0;
	pstMaster->wCountKOSDAQ = 0;
	p = pstMaster->astStockMaster;
	for (int i = 0; i < ciSLOTS; i++) {
		AcquireSRWLockExclusive(&(p[i].srwlock));
		ZeroMemory(p[i].tchHname, sizeof(TST__DS_T8430_OUT_OBJ::tchHname));
		ZeroMemory(p[i].tchShcode, sizeof(TST__DS_T8430_OUT_OBJ::tchShcode));
		ZeroMemory(p[i].tchExpcode, sizeof(TST__DS_T8430_OUT_OBJ::tchExpcode));
		p[i].enStockCat = TEN_DS_STOCK_CAT::enST_UNKNOWN;
		p[i].lUplmtprice = 0;
		p[i].lDnlmtprice = 0;
		p[i].lJnilclose = 0;
		p[i].lMemedan = 0;
		p[i].lRecprice = 0;
		p[i].enMarketCat = TEN_DS_MARKET_CAT::enMK_UNKNOWN;
		ReleaseSRWLockExclusive(&(p[i].srwlock));
	}
	ReleaseSRWLockExclusive(&(pstMaster->srwlock));

	iCount = pRcv->nDataLength / sizeof(t8430OutBlock);
	pOut = (LPt8430OutBlock)pRcv->lpData;

	AcquireSRWLockExclusive(&(pstMaster->srwlock));
	for (int i = 0; i < iCount; i++) {
		if (i >= ciSLOTS) {
			DPRINTF("WARN] Out of memory space. Mem:[%d], Data:[%d]\n", ciSLOTS, iCount);
			break;
		}
		AcquireSRWLockExclusive(&(p[i].srwlock));
		_tcsncpy_s(p[i].tchHname, _countof(TST__DS_T8430_OUT_OBJ::tchHname), pOut[i].hname, _countof(t8430OutBlock::hname));	/* string 20 */
		_tcsncpy_s(p[i].tchShcode, _countof(TST__DS_T8430_OUT_OBJ::tchShcode), pOut[i].shcode, _countof(t8430OutBlock::shcode));	/* string 6 */
		_tcsncpy_s(p[i].tchExpcode, _countof(TST__DS_T8430_OUT_OBJ::tchExpcode), pOut[i].expcode, _countof(t8430OutBlock::expcode));	/* string 12 */
		tchrTrim_s(p[i].tchHname, sizeof(TST__DS_T8430_OUT_OBJ::tchHname));
		tchrTrim_s(p[i].tchShcode, sizeof(TST__DS_T8430_OUT_OBJ::tchShcode));
		tchrTrim_s(p[i].tchExpcode, sizeof(TST__DS_T8430_OUT_OBJ::tchExpcode));

		if ('0' == pOut[i].etfgubun[0])	/* string 1 */
			p[i].enStockCat = TEN_DS_STOCK_CAT::enST_NORMAL;
		else if ('1' == pOut[i].etfgubun[0])
			p[i].enStockCat = TEN_DS_STOCK_CAT::enST_ETF;
		else if ('2' == pOut[i].etfgubun[0])
			p[i].enStockCat = TEN_DS_STOCK_CAT::enST_ETN;
		else {
			; /* should never reach here */
		}

		if ('1' == pOut[i].gubun[0])	/* string 1 */
			p[i].enMarketCat = TEN_DS_MARKET_CAT::enMK_KOSPI;
		else if ('2' == pOut[i].gubun[0])
			p[i].enMarketCat = TEN_DS_MARKET_CAT::enMK_KOSDAQ;
		else {
			;	/* should never reach here */
		}

		ZeroMemory(tchBuf, sizeof(tchBuf));	/* long 8 */
		_tcsncpy_s(tchBuf, _countof(tchBuf), pOut[i].uplmtprice, _countof(t8430OutBlock::uplmtprice));
		p[i].lUplmtprice = _ttoi(tchBuf);

		ZeroMemory(tchBuf, sizeof(tchBuf));	/* long 8 */
		_tcsncpy_s(tchBuf, _countof(tchBuf), pOut[i].dnlmtprice, _countof(t8430OutBlock::dnlmtprice));
		p[i].lDnlmtprice = _ttoi(tchBuf);

		ZeroMemory(tchBuf, sizeof(tchBuf));	/* long 8 */
		_tcsncpy_s(tchBuf, _countof(tchBuf), pOut[i].jnilclose, _countof(t8430OutBlock::jnilclose));
		p[i].lJnilclose = _ttoi(tchBuf);

		ZeroMemory(tchBuf, sizeof(tchBuf));	/* long 8 */
		_tcsncpy_s(tchBuf, _countof(tchBuf), pOut[i].recprice, _countof(t8430OutBlock::recprice));
		p[i].lRecprice = _ttoi(tchBuf);

		ZeroMemory(tchBuf, sizeof(tchBuf));	/* string 5 */
		_tcsncpy_s(tchBuf, _countof(tchBuf), pOut[i].memedan, _countof(t8430OutBlock::memedan));
		tchrTrim_s(tchBuf, sizeof(tchBuf));
		p[i].lMemedan = _ttoi(tchBuf);

		ReleaseSRWLockExclusive(&(p[i].srwlock));

		if (TEN_DS_MARKET_CAT::enMK_KOSPI == p[i].enMarketCat) {
			pstMaster->wCountKOSPI++;
		}
		else if (TEN_DS_MARKET_CAT::enMK_KOSDAQ == p[i].enMarketCat) {
			pstMaster->wCountKOSDAQ++;
		}

		pstMaster->wCountTotal++;
	}

	pstMaster->enState = TEN_DataState::enState_VALID;
	ReleaseSRWLockExclusive(&(pstMaster->srwlock));

	AcquireSRWLockExclusive(&(pstIndex->srwlock));
	AcquireSRWLockExclusive(&(pstMaster->srwlock));
	for (int i = 0; i < pstMaster->wCountTotal; i++) {
		pstIndex->mapIndexSH.SetAt(p[i].tchShcode, i);
		pstIndex->mapIndexEXP.SetAt(p[i].tchExpcode, i);
	}
	ReleaseSRWLockExclusive(&(pstMaster->srwlock));
	ReleaseSRWLockExclusive(&(pstIndex->srwlock));

	DPRINTF("LEAVE\n");
	return;
}

 

수신 패킷 처리가 끝나면 동적으로 할당한 메모리를 아래 함수 호출을 통해 메모리 해제 해줍니다.

메모리 해제 코드

 

메모리 데이터를 파일로 저장하는 덤프함수를 아래와 같이 작성하여, t8430 수신 완료 후 호출해 보았습니다.

수신 데이터 덤프 코드
실제 실행 결과

로그인 완료 후 "수집 시작"을 누르니 디스크에 덤프파일이 생겼네요. t8430 주식마스터조회 성공입니다.

 

반응형