성태의 닷넷 이야기
홈 주인
모아 놓은 자료
프로그래밍
질문/답변
사용자 관리
사용자
메뉴
아티클
외부 아티클
유용한 코드
온라인 기능
MathJax 입력기
최근 덧글
[정성태] 그냥 RSS Reader 기능과 약간의 UI 편의성 때문에 사용...
[이종효] 오래된 소프트웨어는 보안 위협이 되기도 합니다. 혹시 어떤 기능...
[정성태] @Keystroke IEEE의 문서를 소개해 주시다니... +_...
[손민수 (Keystroke)] 괜히 듀얼채널 구성할 때 한번에 같은 제품 사라고 하는 것이 아...
[정성태] 전각(Full-width)/반각(Half-width) 기능을 토...
[정성태] Vector에 대한 내용은 없습니다. Vector가 닷넷 BCL...
[orion] 글 읽고 찾아보니 디자인 타임에는 InitializeCompon...
[orion] 연휴 전에 재현 프로젝트 올리자 생각해 놓고 여의치 않아서 못 ...
[정성태] 아래의 글에 정리했으니 참고하세요. C# - Typed D...
[정성태] 간단한 재현 프로젝트라도 있을까요? 저런 식으로 설명만 해...
글쓰기
제목
이름
암호
전자우편
HTML
홈페이지
유형
제니퍼 .NET
닷넷
COM 개체 관련
스크립트
VC++
VS.NET IDE
Windows
Team Foundation Server
디버깅 기술
오류 유형
개발 환경 구성
웹
기타
Linux
Java
DDK
Math
Phone
Graphics
사물인터넷
부모글 보이기/감추기
내용
<div style='display: inline'> <h1 style='font-family: Malgun Gothic, Consolas; font-size: 20pt; color: #006699; text-align: center; font-weight: bold'>C# XingAPI - ACF 검색 결과로 구한 CSV 파일을 통해 퀀트 종목 찾기</h1> <p> 지난 글에서,<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > C# XingAPI - ACF 파일을 이용한 퀀트 종목 찾기(t1857) ; <a target='tab' href='https://www.sysnet.pe.kr/2/0/13046'>https://www.sysnet.pe.kr/2/0/13046</a> </pre> <br /> ACF로 종목을 결정한 후, 그 결과로부터 다시 <a target='tab' href='https://www.sysnet.pe.kr/2/0/13034#t3320'>t3320</a>을 이용해 PER, PBR, PCR, PSR을 구하고, 이후 정렬을 통해 4가지 지표를 종합한 순위를 정하려고 했는데요, 아쉽게도 t3320의 이해할 수 없는 반환 결과로 인해 완료를 짓지 못하고 포기하게 되었습니다.<br /> <br /> 어쨌든, 살펴봤으니 굳이 완결을 지어야겠는데요. ^^; 약간 우회 방법을 이용하면 그래도 그 ACF 결과를 이용해 평균 순위를 내는 것이 가능합니다. 이를 위해, HTS의 "e종목검색" 화면을 보면 우측 하단에 "종목전송"이라는 버튼을 볼 수 있는데요,<br /> <br /> <img onclick='toggle_img(this)' class='imgView' alt='acf_search_to_csv_1.png' src='/SysWebRes/bbs/acf_search_to_csv_1.png' /><br /> <br /> 이것을 이용하면 Excel 파일로 내보내기가 가능합니다. 재미있는 것은, 이때 "전략작성"의 조건으로 지정했던 "값"도 함께 출력시킬 수 있다는 점입니다.<br /> <br /> 따라서, 일단 그 결과를 Excel로 저장하고, 그것을 다시 CSV 파일로 저장하면 이후부터는 (XingAPI와는 별개로) 그냥 C# 코드를 이용해 평균 순위를 구하는 것이 가능합니다.<br /> <br /> 가령, 지난 결과를 (Excel을 경유해) CSV로 저장하면 이렇게 나옵니다.<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > ,코드,종목명,현재가,전일대비,등락률,거래량,L일H,PCR(C),PBR(D),PSR(E),PER(F) 1,000060,메리츠화재,"44,250",300,0.68%,"114,041",,7,1.98,0.45,8.09 1,000070,삼양홀딩스,"84,900",-100,-0.12%,"4,044",,2,0.43,0.24,3.17 1,000150,두산,"90,200",600,0.67%,"115,212",,2,0.82,0.15,9.12 1,000210,DL,"63,600","-1,800",-2.75%,"98,565",,1,0.37,0.56,1.53 ...[생략]... </pre> <br /> 음... csv 출력 파일도 참... 할 말을 잃게 만드는군요. <br /> <br /> 1) 우선, 헤더 필드명 중에서 첫 번째 필드가 누락돼 있습니다. 필요 없는 필드명이라 그럴 수 있겠지만, 사실 그렇게 따지면 그 필드에 해당하는 값도 모두 1이기 때문에 굳이 값을 출력할 필요도 없었을 것입니다.<br /> <br /> 2) 그리고 "현재가", "등락률"은 "숫자"로서가 아닌 "문자열"인 것처럼 겹따옴표가 함께 있습니다. 사실 이건 중간에 콤마(,)를 포함하기 때문에 csv 파일의 특성상 어쩔 수 없습니다. 하지만, 굳이 숫자 데이터에 콤마를 넣을 필요는 없었기 때문에 그냥 "44,250"이 아닌 44250으로 출력했다면 더 좋았을 것입니다. <br /> <br /> 3) 게다가, PCR 값의 경우 HTS "e종목검색" 화면에서는 7.45와 같은 2자리 소수점 이하 값을 포함하고 있지만, csv에서는 소수점 떼고 정수만 출력이 되고 있습니다.<br /> <br /> 에효~~~, 어쨌든 계속 진행해 보겠습니다. ^^;<br /> <br /> 가장 먼저 할 일은 <a target='tab' href='https://www.sysnet.pe.kr/2/1/1144'>CSV를 읽어서 적절하게 코드에서 사용할 수 있는 목록으로 가공</a>을 해야 하는데요, 아쉽게도 csv가 저렇게 나와 있어서 어쩌면 단순 무식하게 파싱을 하는 것이 편할 수 있습니다.<br /> <br /> 하지만, 이번 기회에 <a target='tab' href='https://www.nuget.org/packages/CsvHelper/'>nuget에 있는 csv parser 라이브러리</a>의 사용법을 익히기 위해 시도를 해봤는데요, 그래서 다음과 같은 식의 코드가 나옵니다.<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > using CsvHelper; using CsvHelper.Configuration; using CsvHelper.TypeConversion; using System.Globalization; internal class Program { static void Main(string[] args) { var config = new CsvConfiguration(CultureInfo.InvariantCulture) { HasHeaderRecord = true, }; <span style='color: blue; font-weight: bold'>List<StockInfo> stocks; using (var reader = new StreamReader("임시저장.CSV")) using (var csv = new CsvReader(reader, config)) { csv.Context.RegisterClassMap<StockInfoMap>(); stocks = csv.GetRecords<StockInfo>().ToList(); }</span> } } public class PercentConverter<T> : DefaultTypeConverter { public override object ConvertFromString(string text, IReaderRow row, MemberMapData memberMapData) { text = text.Trim('%'); return float.Parse(text); } public override string ConvertToString(object value, IWriterRow row, MemberMapData memberMapData) { return value.ToString() ?? ""; } } public class CurrencyConverter<T> : DefaultTypeConverter { public override object ConvertFromString(string text, IReaderRow row, MemberMapData memberMapData) { text = text.Trim('\"'); return long.Parse(text, NumberStyles.AllowThousands | NumberStyles.AllowLeadingSign); } public override string ConvertToString(object value, IWriterRow row, MemberMapData memberMapData) { return value.ToString() ?? ""; } } public class StockInfoMap : ClassMap<StockInfo> { public StockInfoMap() { Map(m => m.Nonce).Index(0); Map(m => m.shcode).Index(1); Map(m => m.hname).Index(2); Map(m => m.price).Index(3).TypeConverter<CurrencyConverter<long>>(); Map(m => m.change).Index(4).TypeConverter<CurrencyConverter<long>>(); Map(m => m.diff).Index(5).TypeConverter<PercentConverter<float>>(); Map(m => m.volume).Index(6).TypeConverter<CurrencyConverter<long>>(); Map(m => m.LH).Index(7); Map(m => m.PCR).Index(8); Map(m => m.PBR).Index(9); Map(m => m.PSR).Index(10); Map(m => m.PER).Index(11); } } <span style='color: blue; font-weight: bold'>public class StockInfo { public int Nonce { get; set; } public string shcode { get; set; } // 코드 public string hname { get; set; } // 종목명 public long price { get; set; } // 현재가 public long change { get; set; } // 전일대비 public float diff { get; set; } // 등락률 public long volume { get; set; } // 거래량 public string LH { get; set; } // L일H public float PCR { get; set; } public float PBR { get; set; } public float PSR { get; set; } public float PER { get; set; } }</span> </pre> <br /> 솔직히, CsvHelper를 선택했을 때 저런 유형의 csv 파일을 읽을 수 있으리라고는 기대하지 않았습니다. 하지만, 해당 라이브러리 개발자가 워낙 다양한 유형의 CSV를 다뤄봤다는 듯이 ^^ 내부 확장 기능을 제공하고 있어 끝내 데이터를 읽어들이는데 문제는 없었습니다.<br /> <br /> 자, 그럼 이제 남은 작업은 PCR, PBR, PSR, PER에 대해 오름차순 정렬을 하고 그 순위를 매기는 것입니다. 이를 위해 순위를 담을 필드를 추가하고,<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > public class StockInfo { // ...[생략]... public int PCROrder { get; set; } public int PBROrder { get; set; } public int PSROrder { get; set; } public int PEROrder { get; set; } } </pre> <br /> 다음과 같이 <a target='tab' href='https://docs.microsoft.com/en-us/dotnet/api/system.collections.generic.list-1.sort'>List.Sort 메서드</a>를 사용해 각각의 필드에 대한 정렬과 순위를 매길 수 있습니다.<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > stocks.Sort(new SortField(m => m.PBR)); ApplyOrder(stocks, m => m.PBR, (m, value) => m.PBROrder = value); stocks.Sort(new SortField(m => m.PCR)); ApplyOrder(stocks, m => m.PCR, (m, value) => m.PCROrder = value); stocks.Sort(new SortField(m => m.PER)); ApplyOrder(stocks, m => m.PER, (m, value) => m.PEROrder = value); stocks.Sort(new SortField(m => m.PSR)); ApplyOrder(stocks, m => m.PSR, (m, value) => m.PSROrder = value); private static void ApplyOrder(List<StockInfo> stocks, Func<StockInfo, float> getField, Action<StockInfo, int> setOrder) { int order = 0; float oldValue = float.MinValue; int skip = 0; foreach (var stock in stocks) { float currentValue = getField(stock); if (oldValue != currentValue) // 다른 값인 경우, 누적된 같은 값의 순위를 반영 { order++; order += skip; skip = 0; oldValue = currentValue; } else // 같은 값은 등수가 같도록, { skip++; } setOrder(stock, order); } } // 오름차순 정렬 public class SortField : IComparer<StockInfo> { Func<StockInfo, float> getField; public SortField(Func<StockInfo, float> getField) { this.getField = getField; } public int Compare(StockInfo? x, StockInfo? y) { if (getField(x) == getField(y)) { return 0; } if (getField(x) < getField(y)) { return -1; } return 1; } } </pre> <br /> 자, 그럼 대충 끝나가는군요, 이제 저 순위를 종합해,<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > foreach (var item in stocks) { item.TotalOrder = item.PBROrder + item.PCROrder + item.PEROrder + item.PSROrder; } </pre> <br /> 다시 정렬을 하면,<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > stocks.Sort(new SortField(m => m.TotalOrder)); foreach (var item in stocks) { Console.WriteLine(item); } public class StockInfo { // ...[생략]... public int TotalOrder { get; set; } public override string ToString() { return $"{hname:F2}({shcode}): PCR={PCR:F2}, PBR={PBR:F2}, PSR={PSR:F2}, PER={PER:F2}, Price={price}, TotalOrder = {TotalOrder}"; } } </pre> <br /> 이렇게 4가지 지표를 종합해 낮은 값을 기준으로 목록이 출력되는 것을 확인할 수 있습니다.<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > 오가닉티코스메틱(900300): PCR=1.00, PBR=0.10, PSR=0.09, PER=0.87, Price=485, TotalOrder = 19 E1(017940): PCR=1.00, PBR=0.26, PSR=0.07, PER=2.60, Price=50000, TotalOrder = 45 한화생명(088350): PCR=2.00, PBR=0.20, PSR=0.09, PER=2.08, Price=2855, TotalOrder = 55 이마트(139480): PCR=1.00, PBR=0.35, PSR=0.14, PER=2.29, Price=129000, TotalOrder = 103 GRT(900290): PCR=1.00, PBR=0.14, PSR=0.22, PER=1.86, Price=1225, TotalOrder = 107 매일홀딩스(005990): PCR=1.00, PBR=0.41, PSR=0.08, PER=3.17, Price=10500, TotalOrder = 134 DB금융투자(016610): PCR=2.00, PBR=0.30, PSR=0.20, PER=2.36, Price=6510, TotalOrder = 139 HDC(012630): PCR=2.00, PBR=0.18, PSR=0.09, PER=3.90, Price=7400, TotalOrder = 146 코오롱(002020): PCR=2.00, PBR=0.44, PSR=0.07, PER=2.79, Price=28250, TotalOrder = 151 계룡건설(013580): PCR=2.00, PBR=0.46, PSR=0.13, PER=2.08, Price=36350, TotalOrder = 176 하림지주(003380): PCR=1.00, PBR=0.47, PSR=0.09, PER=3.17, Price=10700, TotalOrder = 176 골든센츄리(900280): PCR=2.00, PBR=0.23, PSR=0.31, PER=1.80, Price=422, TotalOrder = 183 KISCO홀딩스(001940): PCR=2.00, PBR=0.35, PSR=0.21, PER=2.98, Price=18350, TotalOrder = 187 다우기술(023590): PCR=2.00, PBR=0.47, PSR=0.15, PER=2.50, Price=20950, TotalOrder = 199 한양증권(001750): PCR=2.00, PBR=0.42, PSR=0.21, PER=2.27, Price=13600, TotalOrder = 200 ...[생략]... 지앤비에스엔지니어링(382800): PCR=9.00, PBR=2.11, PSR=1.89, PER=9.54, Price=16500, TotalOrder = 1835 위메이드(112040): PCR=9.00, PBR=4.24, PSR=8.06, PER=8.80, Price=81100, TotalOrder = 1851 메디톡스(086900): PCR=8.00, PBR=1.93, PSR=5.00, PER=9.82, Price=132400, TotalOrder = 1856 DSC인베스트먼트(241520): PCR=9.00, PBR=1.95, PSR=4.86, PER=9.38, Price=5390, TotalOrder = 1858 노바텍(285490): PCR=9.00, PBR=3.26, PSR=4.48, PER=10.00, Price=38350, TotalOrder = 1923 # of records: 486 </pre> <br /> 제가 잘은 모르겠지만, PCR, PBR, PSR, PER 중에서도 뭔가 어떤 수치는 더 중요하다는 식의 기준이 있다면 그에 따른 가중치를 둬도 될 것입니다. 어쨌든 여러분들의 취향에 맞을지는 모르겠지만, 만약 전략이 맞는다고 판단된다면 대충 상위에 있는 종목들을 매수하시면 되겠습니다. ^^ <br /> <br /> 참고로, 제가 매수했냐고요? 아쉽게도 저는 개발자이지, 투자자는 아닙니다. 언젠가 할 수도 있겠지만... 아직은 때가 아닌 듯합니다. ^^ 여전히 완전 초보라.<br /> <br /> (<a target='tab' href='https://www.sysnet.pe.kr/bbs/DownloadAttachment.aspx?fid=1928&boardid=331301885'>이 글의 예제 코드는 첨부 파일</a>에 있습니다. 예제 코드만 보면, 사실 이번 글은 퀀트가 아닌 CSV 라이브러리 사용 예제라고 봐야 할 것입니다. ^^;)<br /> <br /> 그나저나, 이전 글에서의 XingAPI 결과만 정상적으로 나왔어도 이런 식으로 CSV를 수작업으로 경유하는 단계는 필요 없었을 것입니다. 모든 것을 자동화할 수 있었는데... 아쉬울 따름입니다. ^^; csv 생성도 그렇고, XingAPI의 결과도 그렇고,,, 어쩌면 ebest 측에서 딱히 이런 식으로 외부에서 가져다 쓰는 것에 대한 긍정적인 자세를 가지고 있지는 않은 듯하다는 것이, 그동안의 제 경험에서 나온 느낌적인 느낌입니다. ^^; (덧붙여, 2022년의 이 시대에, 아직도 32비트 OCX에... 유니코드 지원도 없고...)<br /> </p><br /> <br /><hr /><span style='color: Maroon'>[이 글에 대해서 여러분들과 의견을 공유하고 싶습니다. 틀리거나 미흡한 부분 또는 의문 사항이 있으시면 언제든 댓글 남겨주십시오.]</span> </div>
첨부파일
스팸 방지용 인증 번호
1750
(왼쪽의 숫자를 입력해야 합니다.)