Microsoft MVP성태의 닷넷 이야기
글쓴 사람
정성태 (techsharer at outlook.com)
홈페이지
첨부 파일

eBEST XingAPI의 C# 래퍼 버전 - XingAPINet Nuget 패키지

C# 래퍼 버전을 만들어 NuGet에 배포했습니다. 얼마나 사용 방법이 쉬운지 ^^ 한 번 작성해 볼까요?

예제를 위해 .NET Framework 4.0 이상의 프로젝트를 만들고, Target Platform을 "AnyCPU"에서 "x86"으로 맞춰줍니다. (왜냐하면, 증권사 API가 64비트를 지원하지 않습니다.)

how_to_use_xing_1.png

그다음, NuGet으로부터 XingAPINet 라이브러리를 참조 추가하면,

Install-Package XingAPINet

끝입니다! ^^

그런데... 제가 주식 분야는 거의 몰라서 ^^ 할 줄 아는 게 없으니 그냥 종목 코드를 가져오는 예제를 작성해 보겠습니다. XingAPI에서 종목 코드를 구할 수 있는 API는 t8430이므로, 따라서 다음과 같이 코딩을 하면 됩니다.

LoginInfo user = LoginInfo.FromPlainText("...id...", "...password...",
        "...인증서 password..."); // 만약 demo 서버 접속이라면 인증서 계정은 필요 없습니다.

using (XingClient xing = new XingClient(true)) // true == demo.etrade.co.kr, false == hts.etrade.co.kr
{
    if (xing.ConnectWithLogin(user) == false)  // 서버 접속
    {
        Console.WriteLine(xing.ErrorMessage);
        return;
    }

    // Query이므로 t8430에 "XQ" 접두사가 붙은 타입을 사용
    XQt8430OutBlock [] items = XQt8430.Get('1'); // 1 == 코스피 종목
    foreach (var item in items)
    {
        Console.WriteLine(item.uplmtprice); // 상한가 출력

        // item.Dump(Console.Out, DumpOutputType.Inline80Cols);
    }
}

오~~~ 엄청 간단합니다. 하는 김에 t8425로 가져오는 테마주도 다뤄볼까요? ^^ 위의 코드에 다음과 같은 코드만 추가하면,

using (XingClient xing = new XingClient(true))
{
    // ...[생략]...

    XQt8425OutBlock [] items2 = XQt8425.Get();
    foreach (var item in items2)
    {
        item.Dump(Console.Out, DumpOutputType.Inline);
    }
}

테마주의 목록이 화면에 출력됩니다. 게다가 모든 타입에 속성으로 API의 필드까지 모두 포함되어 있어 문자열을 하드 코딩할 필요 없이 인텔리센스의 도움을 받아 가며 작업할 수 있습니다. ^^




프로그램을 빌드하면 출력 폴더로 eBEST의 xingAPI 패키지에 포함된 바이너리가 함께 출력됩니다.

how_to_use_xing_2.png

또한, 예전에 키움 OpenAPI처럼,

C# - 키움 Open API+ 사용 시 Registry 등록 없이 KHOpenAPI.ocx 사용하는 방법
; https://www.sysnet.pe.kr/2/0/12129

app.manifest 파일을 추가해 다음의 내용으로 채워주면 말 그대로 XCopy 배포 방식도 가능합니다. 즉, COM 객체 등록도 필요가 없는 것입니다. (만약 이 파일을 추가하지 않으면 빌드 출력 폴더의 reg.bat 파일을 관리자 권한으로 실행해야 합니다.)

<?xml version="1.0" encoding="utf-8"?>
<asmv1:assembly manifestVersion="1.0" xmlns="urn:schemas-microsoft-com:asm.v1" xmlns:asmv1="urn:schemas-microsoft-com:asm.v1" xmlns:asmv2="urn:schemas-microsoft-com:asm.v2" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
    <assemblyIdentity version="1.0.0.0" name="MyApplication.app"/>
    <trustInfo xmlns="urn:schemas-microsoft-com:asm.v2">
        <security>
            <requestedPrivileges xmlns="urn:schemas-microsoft-com:asm.v3">
                <requestedExecutionLevel level="asInvoker" uiAccess="false" />
            </requestedPrivileges>
        </security>
    </trustInfo>

    <compatibility xmlns="urn:schemas-microsoft-com:compatibility.v1">
        <application>
        </application>
    </compatibility>

    <dependency>
        <dependentAssembly asmv2:dependencyType="install" asmv2:codebase="XA_Session.dll.manifest" asmv2:size="1035">
            <assemblyIdentity name="XA_Session.dll" version="1.0.0.1" processorArchitecture="x86" type="win32" />
        </dependentAssembly>
    </dependency>

    <dependency>
        <dependentAssembly asmv2:dependencyType="install" asmv2:codebase="XA_DataSet.dll.manifest" asmv2:size="1654">
            <assemblyIdentity name="XA_DataSet.dll" version="1.0.0.1" processorArchitecture="x86" type="win32" />
        </dependentAssembly>
    </dependency>

</asmv1:assembly>

이 때문에, eBEST의 데모 서버에 접속하는 용도라면 인증서까지도 필요 없기 때문에 아무 PC에나 .NET Frmaework 4.0만 설치되어 있다면 그대로 복사해 실행하는 것이 가능합니다. 물론, 실 서버에 접속한다면 공인 인증서가 설치된 PC이기만 하면 됩니다.




직접 해보니, 키움 API보다 eBEST의 XingAPI가 더 좋습니다. 왜냐하면 API 단에서 로그인할 수 있는 방법도 제공하기 때문에 "NT 서비스" 형식의 프로그램도 만들 수 있어 컴퓨터에 수동으로 직접 로그인하지 않아도 시스템 트레이딩 작업이 가능합니다. (혹은 autohotkey 같은 꼼수나 기타 UAC를 꺼서 보안 위협이 될 만한 조치가 전혀 필요 없습니다.) 여전히 Unicode 지원이 되지 않는다는 정도의 단점은 있지만 그래도 이 정도면 키움에 비해서는 훌륭합니다. ^^

또한, 해당 API들의 명세를 "RES" 확장자 파일로 제공하는데 제법 규격화가 잘 되어 있습니다. 예를 들어 아래는 t0167.res 파일의 내용인데,

BEGIN_FUNCTION_MAP
    .Func,서버시간조회(t0167),t0167,block,headtype=A;
    BEGIN_DATA_MAP
    t0167InBlock,기본입력,input;
    begin
        id,id,id,char,8;
    end
    t0167OutBlock,출력,output;
    begin
        일자(YYYYMMDD),dt,dt,char,8;
        시간(HHMMSSssssss),time,time,char,12;
    end
    END_DATA_MAP
END_FUNCTION_MAP

제가 만든 Res2Query 프로젝트는 위의 RES 파일을 읽어들여 다음과 같은 형식의 C# 소스 코드 파일을 생성합니다. (그리고 이 파일은 XingAPINet 바이너리에 포함됩니다.)

using System;
using System.IO;
using System.Collections.Generic;
using System.Linq;
using System.Runtime.InteropServices;
using System.Text;
using XingAPINet;

namespace XingAPINet
{
    public partial class XQt0167InBlock : XingBlock
    {
        /// <summary>
        /// t0167InBlock
        /// </summary>
        public const string _blockName = "t0167InBlock";
        /// <summary>
        /// 기본입력
        /// </summary>
        public const string _blockDesc = "기본입력";
        /// <summary>
        /// input
        /// </summary>
        public const string _blockType = "input";
        /// <summary>
        /// false
        /// </summary>
        public const bool _hasOccurs = false;
        /// <summary>
        /// t0167InBlock
        /// </summary>
        public override string GetBlockName() => _blockName;
        /// <summary>
        /// t0167InBlock
        /// </summary>
        public static string BlockName => _blockName;
        /// <summary>
        /// 기본입력
        /// </summary>
        public string BlockDesc => _blockDesc;
        /// <summary>
        /// input
        /// </summary>
        public string BlockType => _blockType;
        /// <summary>
        /// false
        /// </summary>
        public bool HasOccurs => _hasOccurs;

        /// <summary>
        /// id
        /// </summary>
        [XAQueryFieldAttribute("id")]
        public string id;

        public static class F
        {
            /// <summary>
            /// id
            /// </summary>
            public const string id = "id";
        }

        public static string[] AllFields = new string[]
        {
            F.id,
        };


        public override Dictionary<string, XAQueryFieldInfo> GetFieldsInfo()
        {
            Dictionary<string, XAQueryFieldInfo> dict = new Dictionary<string, XAQueryFieldInfo>();
            dict["id"] = new XAQueryFieldInfo("char", id, id, "id", (decimal)8);

            return dict;
        }

        public override void SetFieldValue(string fieldName, XAQueryFieldInfo fieldInfo)
        {
            switch (fieldName)
            {
                case "id":
                    this.id = fieldInfo.FieldValue.TrimEnd('?');
                break;


            }
        }

        public bool VerifyData()
        {
            if (id?.Length > 8) return false; // char 8

            return true;
        }
    }

    public partial class XQt0167OutBlock : XingBlock
    {
        /// <summary>
        /// t0167OutBlock
        /// </summary>
        public const string _blockName = "t0167OutBlock";
        /// <summary>
        /// 출력
        /// </summary>
        public const string _blockDesc = "출력";
        /// <summary>
        /// output
        /// </summary>
        public const string _blockType = "output";
        /// <summary>
        /// false
        /// </summary>
        public const bool _hasOccurs = false;
        /// <summary>
        /// t0167OutBlock
        /// </summary>
        public override string GetBlockName() => _blockName;
        /// <summary>
        /// t0167OutBlock
        /// </summary>
        public static string BlockName => _blockName;
        /// <summary>
        /// 출력
        /// </summary>
        public string BlockDesc => _blockDesc;
        /// <summary>
        /// output
        /// </summary>
        public string BlockType => _blockType;
        /// <summary>
        /// false
        /// </summary>
        public bool HasOccurs => _hasOccurs;

        /// <summary>
        /// 일자(YYYYMMDD)
        /// </summary>
        [XAQueryFieldAttribute("일자(YYYYMMDD)")]
        public string dt;
        /// <summary>
        /// 시간(HHMMSSssssss)
        /// </summary>
        [XAQueryFieldAttribute("시간(HHMMSSssssss)")]
        public string time;

        public static class F
        {
            /// <summary>
            /// 일자(YYYYMMDD)
            /// </summary>
            public const string dt = "dt";
            /// <summary>
            /// 시간(HHMMSSssssss)
            /// </summary>
            public const string time = "time";
        }

        public static string[] AllFields = new string[]
        {
            F.dt,
            F.time,
        };


        public override Dictionary<string, XAQueryFieldInfo> GetFieldsInfo()
        {
            Dictionary<string, XAQueryFieldInfo> dict = new Dictionary<string, XAQueryFieldInfo>();
            dict["dt"] = new XAQueryFieldInfo("char", dt, dt, "일자(YYYYMMDD)", (decimal)8);
            dict["time"] = new XAQueryFieldInfo("char", time, time, "시간(HHMMSSssssss)", (decimal)12);

            return dict;
        }

        public override void SetFieldValue(string fieldName, XAQueryFieldInfo fieldInfo)
        {
            switch (fieldName)
            {
                case "dt":
                    this.dt = fieldInfo.FieldValue.TrimEnd('?');
                break;

                case "time":
                    this.time = fieldInfo.FieldValue.TrimEnd('?');
                break;


            }
        }

        public static XQt0167OutBlock FromQuery(XQt0167 query)
        {
            XQt0167OutBlock block = new XQt0167OutBlock();
            block.IsValidData = true;
            block.InvalidReason = "";
            if (query.QueryResult != null && query.QueryResult.IsSystemError == true)
            {
                block.IsValidData = false;
                block.InvalidReason = query.ReceiveMessage;
                return block;
            }
            try
            {
                block.dt = query.GetFieldData(block.GetBlockName(), "dt", 0).TrimEnd('?'); // char 8
                block.time = query.GetFieldData(block.GetBlockName(), "time", 0).TrimEnd('?'); // char 12

            } catch (InvalidDataFormatException e) {
                block.IsValidData = false;
                block.InvalidReason = $"FieldName == {e.DataFieldName}, FieldData == \"{e.DataValue}\"";
            }
            return block;

        }

        public bool VerifyData()
        {
            if (dt?.Length > 8) return false; // char 8
            if (time?.Length > 12) return false; // char 12

            return true;
        }
    }

    /// <summary>
    /// 서버시간조회(t0167)
    /// </summary>
    public partial class XQt0167 : XingQuery
    {
        /// <summary>
        /// t0167
        /// </summary>
        public const string _typeName = "t0167";
        /// <summary>
        /// 서버시간조회(t0167)
        /// </summary>
        public const string _typeDesc = "서버시간조회(t0167)";
        /// <summary>
        /// 
        /// </summary>
        public const string _service = "";
        /// <summary>
        /// A
        /// </summary>
        public const string _headType = "A";
        /// <summary>
        /// 
        /// </summary>
        public const string _creator = "";
        /// <summary>
        /// 
        /// </summary>
        public const string _createdDate = "";
        /// <summary>
        /// false
        /// </summary>
        public const bool _attr = false;
        /// <summary>
        /// true
        /// </summary>
        public const bool _block = true;
        /// <summary>
        /// false
        /// </summary>
        public const bool _encrypt = false;
        /// <summary>
        /// false
        /// </summary>
        public const bool _signature = false;

        /// <summary>
        /// t0167
        /// </summary>
        public string TypeName => _typeName;
        /// <summary>
        /// 서버시간조회(t0167)
        /// </summary>
        public string TypeDesc => _typeDesc;
        /// <summary>
        /// 
        /// </summary>
        public string Service => _service;
        /// <summary>
        /// A
        /// </summary>
        public string HeadType => _headType;
        /// <summary>
        /// 
        /// </summary>
        public string Creator => _creator;
        /// <summary>
        /// 
        /// </summary>
        public string CreatedDate => _createdDate;
        /// <summary>
        /// false
        /// </summary>
        public bool Attr => _attr;
        /// <summary>
        /// true
        /// </summary>
        public bool Block => _block;
        /// <summary>
        /// false
        /// </summary>
        public bool Encrypt => _encrypt;
        /// <summary>
        /// false
        /// </summary>
        public bool Signature => _signature;

        public XQt0167() : base("t0167") { }


        public static XQt0167OutBlock Get(string id = default)
        {
            using (XQt0167 instance = new XQt0167())
            {
                instance.SetFieldData(XQt0167InBlock.BlockName, XQt0167InBlock.F.id, 0, id); // char 8

                if (instance.Request() < 0)
                {
                    return null;
                }

                var outBlock = instance.GetBlock();
                if (outBlock.IsValidData == false)
                {
                    return null;
                }
                return outBlock;
            }
        }

        public bool SetBlock(XQt0167InBlock block)
        {
            if (block.VerifyData() == false)
            {
                return false; // throw new ApplicationException("Failed to verify: " + block.BlockName);
            }

            _xaQuery.SetFieldData(block.GetBlockName(), "id", 0, block.id); // char 8

            return true;
        }

        public XQt0167OutBlock GetBlock()
        {
            XQt0167OutBlock instance = XQt0167OutBlock.FromQuery(this);
            return instance;

        }


    }

}

그렇기 때문에 여러분들은 하드 코딩할 필요 없이 다음과 같은 식으로 In/Out 데이터를 다룰 수 있습니다.

// XQt0167OutBlock outBlock = XQt0167.Get();

var outBlock = XQt0167.Get();
Console.WriteLine("dt == " + outBlock.dt);
Console.WriteLine("time == " + outBlock.time);

심지어 해당 Block의 필드 자체도 클래스의 필드로 추가해 두었기 때문에 다음과 같이 하드 코딩을 없앨 수 있습니다. (제가 인텔리센스 광팬입니다. ^^)

Console.WriteLine(XQt0167OutBlock.F.dt + " == " + outBlock.dt);
Console.WriteLine(XQt0167OutBlock.F.time + " == " + outBlock.time);

취향에 따라, Get 정적 메서드를 사용하지 않고 이를 풀어서 다음과 같이 사용하는 것도 가능합니다.

using (XQt0167 query = new XQt0167())
{
    query.SetFieldData(XQt0167InBlock.BlockName, XQt0167InBlock.F.id, 0, "");
    if (query.Request() < 0)
    {
        Console.WriteLine("Failed to send request");
    }

    XQt0167OutBlock outBlock = query.GetBlock();
    if (outBlock.IsValidData == true)
    {
        outBlock.Dump(Console.Out, DumpOutputType.FormattedKeyValue);
    }
    else
    {
        Console.WriteLine($"Invalid: {outBlock.InvalidReason}");
    }
}

이 정도면... 좀 쓸만하겠죠? ^^

(첨부 파일은 t0167 예제 코드 및 app.manifest까지 구성한 완전한 예제 프로젝트입니다.)




유튜브에 보니까, python으로 xingAPI를 강의하는 것이 있습니다. 일단 21강 정도까지는,

[21강] xingAPI를 이용한 데이타 수신 : 종목코드조회(t8430) 
; https://www.youtube.com/watch?v=loO2isn8OXk

제가 만든 C# API로 무리 없이 따라 할 수 있었습니다. 말인즉, XingAPINet 라이브러리가 완벽하지 않을 있다는 점을 감안하시고 혹시 개선해야 할 점이 있다면 소스 코드가 모두 공개되었으니,

stjeong / XingAPI
; https://github.com/stjeong/XingAPI

PR을 날리셔도 좋겠고, "Issues" 게시판을 이용해 개선점을 남기시면 최대한 시간 날 때 반영해 보겠습니다. 물론, 저도 틈나는 대로 위에서 소개한 Youtube 강의를 보면서 그에 대응하는 예제를 직접 C#으로 만들어 추가하고 있습니다. 그래서 현재 다음의 경로에 보면,

XingAPI/DevCenterSample/TR/업종
; https://github.com/stjeong/XingAPI/tree/master/DevCenterSample/TR/%EC%97%85%EC%A2%85

t0167, t1475, t1511,... 등의 코드가 들어있으니 이를 참고해 코딩하셔도 무방합니다.




[이 글에 대해서 여러분들과 의견을 공유하고 싶습니다. 틀리거나 미흡한 부분 또는 의문 사항이 있으시면 언제든 댓글 남겨주십시오.]

[연관 글]





[최초 등록일: ]
[최종 수정일: 2/5/2020 ]

Creative Commons License
이 저작물은 크리에이티브 커먼즈 코리아 저작자표시-비영리-변경금지 2.0 대한민국 라이센스에 따라 이용하실 수 있습니다.
by SeongTae Jeong, mailto:techsharer@outlook.com

비밀번호

댓글 쓴 사람
 



2020-07-17 10시56분
[나그네] 사용자 개체가 꾸준히 증가하네요. 사용자 개체가 많다는 오류를 만들고 죽는데요.
dispose가 안되는것이지.. ^^

[손님]
2020-07-17 01시16분
[나그네] XQt2101 같은 현재가를 초당 5번 가져오는것들은.. 내부적으로 dispose에
Marshal.ReleaseComObject(_xaQuery);
이 잘 선언되어 있음에도.. 해제가 안되고.. 사용자 개체가 늘어나는군요.
C#의 문제가 아니라 COM의 문제라고 보입니다. 어떻게 호출을 해도.. GC를 호출를 해도 해제가 안되네요.
[손님]

1  2  3  4  5  6  7  8  [9]  10  11  12  13  14  15  ...
NoWriterDateCnt.TitleFile(s)
12177정성태3/9/2020621개발 환경 구성: 479. docker - MySQL 컨테이너 실행
12176정성태3/9/2020466개발 환경 구성: 478. 파일의 (sha256 등의) 해시 값(checksum) 확인하는 방법
12175정성태3/8/2020669개발 환경 구성: 477. "Docker Desktop for Windows"의 "Linux Container" 모드를 위한 tcp 바인딩 추가
12174정성태3/8/2020817개발 환경 구성: 476. DockerDesktopVM의 파일 시스템 접근 [2]
12173정성태3/8/2020759개발 환경 구성: 475. docker - SQL Server 2019 컨테이너 실행 [1]
12172정성태3/8/20201243개발 환경 구성: 474. docker - container에서 root 권한 명령어 실행(sudo)
12171정성태3/6/2020736VS.NET IDE: 143. Visual Studio - ASP.NET Core Web Application의 "Enable Docker Support" 옵션으로 달라지는 점
12170정성태3/6/2020645오류 유형: 599. "Docker Desktop is switching..." 메시지와 DockerDesktopVM CPU 소비 현상
12169정성태3/5/20201143개발 환경 구성: 473. Windows nanoserver에 대한 docker pull의 태그 사용 [1]
12168정성태3/8/2020945개발 환경 구성: 472. 윈도우 환경에서의 dockerd.exe("Docker Engine" 서비스)가 Linux의 것과 다른 점
12167정성태3/5/2020737개발 환경 구성: 471. C# - 닷넷 응용 프로그램에서 DB2 Express-C 데이터베이스 사용 (3) - ibmcom/db2express-c 컨테이너 사용
12166정성태3/14/2020612개발 환경 구성: 470. Windows Server 컨테이너 - DockerMsftProvider 모듈을 이용한 docker 설치
12165정성태8/18/2020591.NET Framework: 900. 실행 시에 메서드 가로채기 - CLR Injection: Runtime Method Replacer 개선 - 네 번째 이야기(Monitor.Enter 후킹)파일 다운로드1
12164정성태2/29/2020778오류 유형: 598. Surface Pro 6 - Windows Hello Face Software Device가 인식이 안 되는 문제
12163정성태2/27/2020651.NET Framework: 899. 익명 함수를 가리키는 delegate 필드에 대한 직렬화 문제
12162정성태2/28/2020852디버깅 기술: 166. C#에서 만든 COM 객체를 C/C++로 P/Invoke Interop 시 메모리 누수(Memory Leak) 발생파일 다운로드2
12161정성태2/26/2020456오류 유형: 597. manifest - The value "x64" of attribute "processorArchitecture" in element "assemblyIdentity" is invalid.
12160정성태2/26/2020525개발 환경 구성: 469. Reg-free COM 개체 사용을 위한 manifest 파일 생성 도구 - COMRegFreeManifest
12159정성태2/26/2020394오류 유형: 596. Visual Studio - The project needs to include ATL support
12158정성태2/26/2020595디버깅 기술: 165. C# - Marshal.GetIUnknownForObject/GetIDispatchForObject 사용 시 메모리 누수(Memory Leak) 발생파일 다운로드1
12157정성태2/27/2020597디버깅 기술: 164. C# - Marshal.GetNativeVariantForObject 사용 시 메모리 누수(Memory Leak) 발생 및 해결 방법파일 다운로드1
12156정성태2/25/2020461오류 유형: 595. LINK : warning LNK4098: defaultlib 'nafxcw.lib' conflicts with use of other libs; use /NODEFAULTLIB:library
12155정성태2/25/2020504오류 유형: 594. Warning NU1701 - This package may not be fully compatible with your project
12154정성태2/25/2020420오류 유형: 593. warning LNK4070: /OUT:... directive in .EXP differs from output filename
12153정성태7/15/2020590.NET Framework: 898. Trampoline을 이용한 후킹의 한계파일 다운로드1
12152정성태2/23/2020555.NET Framework: 897. 실행 시에 메서드 가로채기 - CLR Injection: Runtime Method Replacer 개선 - 세 번째 이야기(Trampoline 후킹)파일 다운로드1
1  2  3  4  5  6  7  8  [9]  10  11  12  13  14  15  ...