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)
12210정성태4/20/2020935.NET Framework: 903. .NET Framework의 Strong-named 어셈블리 바인딩 (1) - app.config을 이용한 바인딩 리디렉션 [1]파일 다운로드1
12209정성태4/13/2020547오류 유형: 614. 리눅스 환경에서 C/C++ 프로그램이 Segmentation fault 에러가 발생한 경우 (2)
12208정성태4/12/2020702Linux: 29. 리눅스 환경에서 C/C++ 프로그램이 Segmentation fault 에러가 발생한 경우
12207정성태4/2/2020620스크립트: 19. Windows PowerShell의 NonInteractive 모드
12206정성태4/2/2020717오류 유형: 613. 파일 잠금이 바로 안 풀린다면? - The process cannot access the file '...' because it is being used by another process.
12205정성태4/2/2020550스크립트: 18. Powershell에서는 cmd.exe의 명령어를 지원하진 않습니다.
12204정성태4/1/2020469스크립트: 17. Powershell 명령어에 ';' (semi-colon) 문자가 포함된 경우
12203정성태3/18/2020831오류 유형: 612. warning: 'C:\ProgramData/Git/config' has a dubious owner: '...'.
12202정성태3/18/20201154개발 환경 구성: 486. .NET Framework 프로젝트를 위한 GitLab CI/CD Runner 구성
12201정성태3/18/2020715오류 유형: 611. git-credential-manager.exe: Using credentials for username "Personal Access Token".
12200정성태11/5/20201050VS.NET IDE: 145. NuGet + Github 라이브러리 디버깅 관련 옵션 3가지 - "Enable Just My Code" / "Enable Source Link support" / "Suppress JIT optimization on module load (Managed only)"
12199정성태3/17/2020547오류 유형: 610. C# - CodeDomProvider 사용 시 Unhandled Exception: System.IO.DirectoryNotFoundException: Could not find a part of the path '...\f2_6uod0.tmp'.
12198정성태3/17/2020688오류 유형: 609. SQL 서버 접속 시 "Cannot open user default database. Login failed."
12197정성태3/17/2020740VS.NET IDE: 144. .NET Core 콘솔 응용 프로그램을 배포(publish) 시 docker image 자동 생성 - 두 번째 이야기
12196정성태3/17/2020650오류 유형: 608. The ServicedComponent being invoked is not correctly configured (Use regsvcs to re-register).
12195정성태3/17/2020821.NET Framework: 902. C# - 프로세스의 모든 핸들을 열람 - 세 번째 이야기
12194정성태3/16/2020852오류 유형: 607. PostgreSQL - Npgsql.NpgsqlException: sorry, too many clients already
12193정성태3/16/2020704개발 환경 구성: 485. docker - SAP Adaptive Server Enterprise 컨테이너 실행
12192정성태3/14/2020863개발 환경 구성: 484. docker - Sybase Anywhere 16 컨테이너 실행
12191정성태3/14/20201381개발 환경 구성: 483. docker - OracleXE 컨테이너 실행 [1]
12190정성태3/14/2020477오류 유형: 606. Docker Desktop 업그레이드 시 "The process cannot access the file 'C:\Program Files\Docker\Docker\resources\dockerd.exe' because it is being used by another process."
12189정성태3/13/20201425개발 환경 구성: 482. Facebook OAuth 처리 시 상태 정보 전달 방법과 "유효한 OAuth 리디렉션 URI" 설정 규칙
12188정성태9/24/2020749Windows: 169. 부팅 시점에 실행되는 chkdsk 결과를 확인하는 방법
12187정성태3/12/2020560오류 유형: 605. NtpClient was unable to set a manual peer to use as a time source because of duplicate error on '...'.
12186정성태3/12/2020667오류 유형: 604. The SysVol Permissions for one or more GPOs on this domain controller and not in sync with the permissions for the GPOs on the Baseline domain controller.
12185정성태3/11/2020559오류 유형: 603. The browser service was unable to retrieve a list of servers from the browser master...
1  2  3  4  5  6  7  8  9  [10]  11  12  13  14  15  ...