eBEST XingAPI의 C# 래퍼 버전 - XingAPINet Nuget 패키지
C# 래퍼 버전을 만들어 NuGet에 배포했습니다. 얼마나 사용 방법이 쉬운지 ^^ 한 번 작성해 볼까요?
예제를 위해 .NET Framework 4.0 이상의 프로젝트를 만들고, Target Platform을 "AnyCPU"에서 "x86"으로 맞춰줍니다. (왜냐하면, 증권사 API가 64비트를 지원하지 않습니다.)
그다음, NuGet으로부터 XingAPINet 라이브러리를 참조 추가하면,
// https://www.nuget.org/packages/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 패키지에 포함된 바이너리가 함께 출력됩니다.
또한, 예전에 키움 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 정적 메서드를 사용하지 않고 이를 풀어서 다음과 같이 사용하는 것도 가능합니다. (업데이트 2021-02-24: 현재 Get 버전의 내부에서 사용하는 XAQuery COM 개체에 메모리 누수가 있으므로 하루 종일 켜두면서 초당 몇 번씩 호출하는 식의 응용 프로그램에서는 사용하지 않는 것이 권장됩니다. 따라서 그런 경우에는 아래의 방법으로 query 인스턴스를 하나 생성해 두고 Request 호출을 반복적으로 처리하는 것이 좋습니다.)
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/업종
t0167, t1475, t1511,... 등의 코드가 들어있으니 이를 참고해 코딩하셔도 무방합니다.
[이 글에 대해서 여러분들과 의견을 공유하고 싶습니다. 틀리거나 미흡한 부분 또는 의문 사항이 있으시면 언제든 댓글 남겨주십시오.]