C# API 프로그램 소스 코드 예제를 소개해드립니다. 프로그램 개발을 하기 위해 우선 가상자산거래와 관련한 용어부터 살펴보도록 하겠습니다. 비트코인 거래소는 주식시장 거래소를 참고한 부분이 많으므로 상당한 부분에서 유사한 단어를 많이 사용하고 있습니다. 그리하여 주식거래를 살펴본 분은 익숙한 단어가 많이 보일 것입니다. 다음과 같은 목차로 진행하도록 하겠습니다.
- 업비트 API 현재가 정보 조회하기.
- 업비트 API 접근.
- API를 이용하여 마켓 코드를 조회 및 현재가 정보 조회하기.
- 업비트 API 예제
C# 업비트 API 현재가 정보 조회하기
업비트는 가상화폐 거래를 중개하는 거래소 중 하나이며, 다른 애플리케이션과 마찬가지로 API를 제공하여 사용자로 하여금 특정한 명령을 실행 가능하도록 합니다. 현재 사용할 수 있는 API는 Exchange API와 Quotation API가 있습니다.
Exchange API
자산, 주문, 출금, 입금, 서비스 정보를 조회하는 API입니다. 서비스 정보와 자산 및 주문 조회를 주로 사용하는 편입니다.
- 자산 조회
- 주문 정보 조회
- 주문하기
- 주문 취소 접수
- 출금 및 입금
- 서비스 정보 조회
Quotation API
지금 보고 있는 것은 가상화폐 거래 시장이죠? Quotation API는 시장의 시세를 조회하는 용도로 제공합니다. 가상화폐와 관련한 용어는 다음과 같습니다.
시장, 마켓(market)
현실의 화폐는 한국 원, 미국 달러, 일본 엔, 중국 위안 등 여러 가지가 있습니다. 가상화폐시장도 마찬가지로 각 화폐마다 시장이 존재합니다. 가장 유명한 건 비트코인이 속한 시장이고 그 외에도 이더리움, 리플, 에이다, 도지코인, 솔라나, 폴카닷 등등 굉장히 많은 코인 시장이 있습니다. 모든 화폐가 상장되는 것은 아니고, 2023년 2월 기준으로 업비트에는 187개의 코인이 296개의 시장에 있습니다.
페어(pair)
사진을 보시면 원화, BTC, USDT 가 있습니다. 한국과 미국 환율을 비교할 때 원달러 환율이라고 하고, 한국과 일본 환율은 원엔 환율이라고 합니다. 마찬가지로 가상화폐에도 쌍을 이룹니다. 원화로 매매가 이루어지는 원화 시장, 비트코인(심볼 BTC) 시장, 테더(심볼 USDT) 시장이 있습니다. 이더리움을 예로 들면 원화 시장은 ETH/KRW, 비트코인 시장은 ETH/BTC, 테더 시장은 ETH/USDT로 매매합니다. 테더(USDT)는 미국 달러와 1:1로 페깅을 목표로 하는 토큰입니다. (1 USDT당 1$를 목표로 함)
캔들(candle)
캔들은 주가변동을 확인할 수 있는 양초 모양의 차트로 분석도구 차원에서 사용합니다. 업비트 API는 분, 일, 주, 월 캔들을 제공합니다.
현재가(ticker)
증권에서 시세 표시기를 티커라고 합니다. 또는 상장된 주식의 코드를 의미하기도 합니다. 업비트에서는 티커를 현재가 정보의 뜻에 중점을 두어 안내합니다.
호가정보(orderbook)
오더북(호가창)은 구매자와 판매자의 수요, 공급을 기록하는 주문창을 뜻합니다. 업비트는 각 시장별 각 호가당 매도, 매수 호가 및 잔량을 표시해 드립니다.
매도(ask)
판매(sell)를 뜻합니다. 주식에서는 보통 매각하려는 최소 가격을 나타냅니다. API 매개변수에서 매도와 관련한 접두사로 많이 사용되는 단어입니다.
매수(bid)
구매(buy)를 뜻합니다. 주식에서는 구매자가 지불하려는 용의가 있는 최대 금액을 의미합니다. API 매개변수에서 매수와 관련된 부분에서 많이 사용합니다.
업비트 API 접근
업비트 Open API에 접근하려면 고객센터 메뉴로 먼저 이동합니다. 메뉴를 내려가다보면 Open API 안내에 대한 버튼이 있습니다. 버튼을 누르면 Open API 사용하기 및 업비트 개발자 센터로 이동할 수 있는 부분이 보입니다.
업비트 API를 사용하기 위해 Open API Key 발급받기를 하겠습니다. 어느 정도의 범위에서 사용할지 지정하고, 특정 IP에서 사용을 원하다면 주소까지 등록해 주세요. 범위는 자산조회, 주문조회, 주문하기, 출금조회, 출금하기, 입금조회, 입금하기가 있습니다.
발급받기를 선택하면 API를 사용할 수 있도록 지원하는 Access Key와 Secret Key가 제공될 것입니다. 타인에게 노출이 되지 않도록 잘 관리해 주세요.
API를 이용하여 마켓 코드를 조회 및 현재가 정보 조회하기
이번 게시물에서는 마켓코드를 조회하고, 현재가 정보를 조회해 보는 예제로 마치겠습니다. 좌측의 시장조회를 누르면 API => 시세 종목 조회 => 마켓 코드 조회를 실행하고, 우측의 시작을 누르면 조회간격마다 API => 시세 현재가 조회 => ticker 정보 조회를 실행하겠습니다.
마켓과 티커를 조회하는 단계를 공부하고, 나아가 주문을 등록하고 조회하거나 취소하는 예제를 또한 다루어 볼 수 있을 것입니다.
프로그램을 사용하여 거래를 반복하는 과정으로 이와 같이 적용을 해볼 수도 있습니다.
다만, 가상자산은 누구도 그 가치를 보장하지 않는 매우 위험한 시장이니, 잃어버려도 괜찮은 돈으로 부담 없이 하시길 권장합니다. 상기의 루나코인이 한순간에 망해가는 과정을 실시간으로 보신 분 계시죠? 5월 6일에 업비트 기준으로 10만 원까지 추락한 코인이 1주일도 안 되어 가치가 -99%가 된 사건입니다.
업비트 API 예제
화면 뷰 부분 예제입니다. 시장을 조회합니다.
using AdsJumboWinForm;
using BeomUp.Model;
using Newtonsoft.Json.Linq;
using System;
using System.Collections;
using System.Collections.Generic;
using System.Data;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Text;
using System.Text.RegularExpressions;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Shapes;
using System.Windows.Threading;
namespace BeomUp.View
{
public partial class ViewBeomSang : Window
{
const string g_beomUpAdsId = "";
UpbitBeomSang m_m;
List<R_Market> m_c_markets;
List<R_Ticker> m_c_tickers;
DispatcherTimer m_timer;
OrderViewControlBeomSang[] ovcs = null;
private enum SetVarType
{
Init
}
public ViewBeomSang()
{
InitializeComponent();
m_m = new UpbitBeomSang();
m_c_markets = new List<R_Market>();
m_c_tickers = new List<R_Ticker>();
m_timer = new DispatcherTimer();
m_timer.Tick += timer_Tick;
ShowAdsJumbo();
}
private void DoAct()
{
try
{
string markets = txtMarkets.Text;
string[] s1 = markets.Split(',');
if (ovcs == null)
{
ovcs = new OrderViewControlBeomSang[s1.Length];
for (int i = 0; i < s1.Length; i++)
{
ovcs[i] = new OrderViewControlBeomSang(s1[i], i);
listBox.Items.Add(ovcs[i]);
}
}
if (GetTicker(markets))
{
foreach (var ticker in m_c_tickers)
{
foreach (var ovc in ovcs.Where(x => x.M_Market == ticker.market))
{
ovc.SetTicker(ticker);
}
}
}
goto DoShowMsg;
DoShowMsg:
if (chkNoLog.IsChecked == false)
lblRunning.Content = string.Format("Running : {0} Interval : {1}", DateTime.Now, m_timer.Interval);
}
catch (Exception ex)
{
SetLog(ex.GetExMsg(MethodBase.GetCurrentMethod().Name));
}
}
private void ShowAdsJumbo()
{
if (Debugger.IsAttached)
((BannerAds)adsHost.Child).ShowAd(468, 60, "#ID#");
else
((BannerAds)adsHost.Child).ShowAd(468, 60, g_beomUpAdsId);
}
private bool GetTicker(string _markets)
{
JArray jArray;
try
{
//****************************************************************************************************
//BEGIN : QUOTATION API - 시세 Ticker 조회 - 현재가 정보(GET)
//****************************************************************************************************
m_m.g_p_ticker.Init();
m_m.g_p_ticker = new P_Ticker(_markets);
jArray = m_m.GetJArray(UpbitBeomSang.Api.ticker);
m_c_tickers = jArray.ToObject<List<R_Ticker>>();
//****************************************************************************************************
//END
//****************************************************************************************************
return true;
}
catch (Exception ex)
{
SetLog(ex.GetExMsg(MethodBase.GetCurrentMethod().Name));
return false;
}
}
private bool GetMarket()
{
JArray jArray;
try
{
//****************************************************************************************************
//BEGIN : QUOTATION API - 시세 종목 조회 - 마켓 코드 조회(GET)
//****************************************************************************************************
m_m.g_p_market.Init();
m_m.g_p_market = new P_Market(true);
jArray = m_m.GetJArray(UpbitBeomSang.Api.marketAll);
m_c_markets = jArray.ToObject<List<R_Market>>();
//Option
if (chkExBtc.IsChecked == true)
m_c_markets.RemoveAll(x => x.market.Split('-')[0] == "BTC");
if (chkExUsdt.IsChecked == true)
m_c_markets.RemoveAll(x => x.market.Split('-')[0] == "USDT");
//Source
lvMarketTable.ClearValue(ListView.ItemsSourceProperty);
lvMarketTable.ItemsSource = m_c_markets;
//****************************************************************************************************
//END
//****************************************************************************************************
return true;
}
catch (Exception ex)
{
SetLog(ex.GetExMsg(MethodBase.GetCurrentMethod().Name));
return false;
}
}
private void SetLog(string _log, bool _setMsg = false)
{
try
{
if (txtLog.Text == string.Empty)
{
txtLog.Text = "[" + DateTime.Now + "] " + _log;
}
else
{
txtLog.Text += Environment.NewLine + "[" + DateTime.Now + "] " + _log;
}
txtLog.ScrollToEnd();
}
catch (Exception)
{
}
}
private void SetVars(SetVarType _setVarType)
{
switch (_setVarType)
{
case SetVarType.Init:
ovcs = null;
listBox.Items.Clear();
break;
default:
SetLog(string.Format("[{0}] Type Undefined", MethodBase.GetCurrentMethod().Name));
return;
}
}
private void Hyperlink_RequestNavigate(object sender, System.Windows.Navigation.RequestNavigateEventArgs e)
{
Process.Start(new ProcessStartInfo(e.Uri.AbsoluteUri));
e.Handled = true;
}
private void sli_ValueChanged(object sender, RoutedPropertyChangedEventArgs<double> e)
{
try
{
if (sender == sliOpa)
{
if (chkNoLog.IsChecked == false)
SetLog(string.Format("Opacity factor : {0}", sliOpa.Value.ToString()));
}
}
catch (Exception ex)
{
}
}
private void lv_SelectionChanged(object sender, SelectionChangedEventArgs e)
{
try
{
if (sender == lvMarketTable)
{
string vals = string.Empty;
IList items = ((ListView)sender).SelectedItems;
IEnumerable a = ((ListView)sender).SelectedItems;
if (items.Count > 0)
{
foreach (R_Market item in items)
{
vals += item.market + ",";
if (!Regex.IsMatch(txtMarkets.Text, item.market))
{
if (txtMarkets.Text == string.Empty)
txtMarkets.Text += item.market;
else
txtMarkets.Text += "," + item.market;
}
}
SetLog(string.Format("Markets : {0}", txtMarkets.Text));
}
}
else
{
SetLog(string.Format("[{0}] Event Not Delegated", MethodBase.GetCurrentMethod().Name));
}
}
catch (Exception ex)
{
SetLog(ex.GetExMsg(MethodBase.GetCurrentMethod().Name));
}
}
private void btn_Click(object sender, RoutedEventArgs e)
{
if (sender == btnSelectMarket)
{
GetMarket();
}
}
private void chk_Checked(object sender, RoutedEventArgs e)
{
if (sender == chkAct)
{
if (Regex.IsMatch(txtTimerInterval.Text, "^[0-9]+$") && !Regex.IsMatch(txtTimerInterval.Text, "^[0]+$"))
{
m_timer.Interval = TimeSpan.FromMilliseconds(Convert.ToDouble(txtTimerInterval.Text) * 1000);
m_timer.Start();
txtMarkets.IsReadOnly = true;
lblRunning.Content = string.Format("Start Running, Interval : {0}", m_timer.Interval);
lblRunning.Foreground = Brushes.Blue;
}
else
{
SetLog(string.Format("조회간격은 양의 정수 형태로 입력해 주세요.", txtTimerInterval.Text));
return;
}
}
else if (sender == chkNoLog)
{
}
}
private void chk_Unchecked(object sender, RoutedEventArgs e)
{
if (sender == chkAct)
{
m_timer.Stop();
txtMarkets.IsReadOnly = false;
lblRunning.Content = "Stop Running";
lblRunning.Foreground = Brushes.Black;
SetVars(SetVarType.Init);
}
}
private void timer_Tick(object sender, EventArgs e)
{
try
{
DoAct();
}
catch (Exception ex)
{
SetLog(ex.GetExMsg(MethodBase.GetCurrentMethod().Name));
}
}
}
}
모델 부분에서 API를 호출하는 것을 모아두었습니다. 주문하기 관련은 주석처리를 해놓았는데 필요 시 주석을 풀어서 테스트해보시면 됩니다.
using Microsoft.IdentityModel.Tokens;
using Newtonsoft.Json.Linq;
using System;
using System.Collections.Generic;
using System.IdentityModel.Tokens.Jwt;
using System.IO;
using System.Linq;
using System.Net;
using System.Security.Cryptography;
using System.Text;
using System.Threading.Tasks;
namespace BeomUp.Model
{
public class UpbitBeomSang
{
//Quotation API > 시세 종목 조회 > 마켓 코드 조회
public const string g_marketEndpoint = "https://api.upbit.com/v1/market/all";
//Quotation API > 시세 Ticker 조회 > 현재가 정보
public const string g_tickerEndpoint = "https://api.upbit.com/v1/ticker";
public P_Market g_p_market = new P_Market();
public P_Ticker g_p_ticker = new P_Ticker();
public enum Api
{
marketAll,
ticker
}
public JArray GetJArray(Api _api)
{
JwtPayload payload = null;
byte[] keyBytes = null;
SymmetricSecurityKey securityKey = null;
SigningCredentials credentials = null;
JwtHeader header = null;
JwtSecurityToken secToken = null;
string jwtToken = null;
HttpWebRequest request = null;
string requestUri = null;
string requestParam = null;
StringBuilder builder = null;
string queryString = null;
SHA512 sha512 = null;
byte[] queryHashByteArray = null;
string queryHash = null;
JArray rtn = null;
try
{
switch (_api)
{
//case Api.api_keys:
//case Api.accounts:
case Api.marketAll:
//case Api.candlesMinutes:
//case Api.trades:
case Api.ticker:
//case Api.orderbook:
//case Api.Order_GetOrders:
break;
default:
throw new Exception("Bad Param : " + _api.ToString());
}
switch (_api)
{
//case Api.api_keys:
//case Api.accounts:
// payload = new JwtPayload
// {
// { "access_key" , g_accessKey },
// { "nonce" , Guid.NewGuid().ToString() },
// };
// keyBytes = Encoding.Default.GetBytes(g_secretKey);
// securityKey = new SymmetricSecurityKey(keyBytes);
// credentials = new SigningCredentials(securityKey, "HS256");
// header = new JwtHeader(credentials);
// secToken = new JwtSecurityToken(header, payload);
// jwtToken = new JwtSecurityTokenHandler().WriteToken(secToken);
// break;
//case Api.Order_GetOrders:
// builder = new StringBuilder();
// foreach (KeyValuePair<string, string> pair in g_p_pairs)
// {
// if (pair.Value != string.Empty)
// builder.Append(pair.Key).Append("=").Append(pair.Value).Append("&");
// }
// queryString = builder.ToString().TrimEnd('&');
// sha512 = SHA512.Create();
// queryHashByteArray = sha512.ComputeHash(Encoding.UTF8.GetBytes(queryString));
// queryHash = BitConverter.ToString(queryHashByteArray).Replace("-", "").ToLower();
// payload = new JwtPayload
// {
// { "access_key", g_accessKey },
// { "nonce", Guid.NewGuid().ToString() },
// { "query_hash", queryHash },
// { "query_hash_alg", "SHA512" }
// };
// keyBytes = Encoding.Default.GetBytes(g_secretKey);
// securityKey = new SymmetricSecurityKey(keyBytes);
// credentials = new SigningCredentials(securityKey, "HS256");
// header = new JwtHeader(credentials);
// secToken = new JwtSecurityToken(header, payload);
// jwtToken = new JwtSecurityTokenHandler().WriteToken(secToken);
// break;
}
switch (_api)
{
//case Api.api_keys:
// requestUri = g_apiKeysEndpoint;
// break;
//case Api.accounts:
// requestUri = g_accountsEndpoint;
// break;
case Api.marketAll:
requestUri = g_marketEndpoint;
requestUri += CoreBeomSang.AddParam("?isDetails=", g_p_market.isDetails.ToString());
break;
//case Api.candlesMinutes:
// requestUri = string.Format(g_candlesMinutesEndpoint, g_p_candle.unit);
// requestUri += CoreBeomSang.AddParam("?market=", g_p_candle.market);
// requestUri += CoreBeomSang.AddParam("&to=", g_p_candle.to);
// requestUri += CoreBeomSang.AddParam("&count=", g_p_candle.count);
// break;
//case Api.trades:
// requestUri = g_tradesTicksEndpoint;
// requestUri += CoreBeomSang.AddParam("?market=", g_p_trade.market);
// requestUri += CoreBeomSang.AddParam("&to=", g_p_trade.to);
// requestUri += CoreBeomSang.AddParam("&count=", g_p_trade.count);
// requestUri += CoreBeomSang.AddParam("&cursor=", g_p_trade.cursor);
// requestUri += CoreBeomSang.AddParam("&daysAgo=", g_p_trade.daysAgo);
// break;
case Api.ticker:
//requestParam = m_p_ticker_markets[0];
//for (int i = 1; i < m_p_ticker_markets.Count; i++)
// requestParam += "," + m_p_ticker_markets[i];
requestUri = g_tickerEndpoint;
requestUri += CoreBeomSang.AddParam("?markets=", g_p_ticker.markets);
//?markets={0}
break;
//case Api.orderbook:
// //requestParam = m_p_ticker_markets[0];
// //for (int i = 1; i < m_p_ticker_markets.Count; i++)
// // requestParam += "," + m_p_ticker_markets[i];
// requestUri = g_orderbookEndpoint;
// requestUri += CoreBeomSang.AddParam("?markets=", g_p_orderbook.markets);
// break;
//case Api.Order_GetOrders:
// requestUri = g_ordersEndpoint;
// requestUri += CoreBeomSang.AddParam("?", queryString);
// break;
}
request = (HttpWebRequest)WebRequest.Create(requestUri);
request.Method = "GET";
switch (_api)
{
//case Api.api_keys:
//case Api.accounts:
//case Api.Order_GetOrders:
// request.Headers.Add(string.Format("Authorization:Bearer {0}", jwtToken));
// break;
}
using (WebResponse response = request.GetResponse())
{
using (Stream stream = response.GetResponseStream())
{
using (StreamReader reader = new StreamReader(stream))
{
string p = reader.ReadToEnd();
rtn = JArray.Parse(p);
return rtn;
}
}
}
throw new Exception("Parsing failed");
}
catch (Exception ex)
{
throw ex;
}
}
}
public struct P_Market
{
public bool isDetails { get; private set; }
public P_Market(bool isDetails)
{
this.isDetails = isDetails;
}
public void Init()
{
isDetails = true;
}
}
public struct P_Ticker
{
public string markets { get; private set; }
public P_Ticker(string markets)
{
this.markets = markets;
}
public void Init()
{
markets = string.Empty;
}
}
public struct R_Market
{
public string market_warning { get; set; }
public string market { get; set; }
public string korean_name { get; set; }
public string english_name { get; set; }
public string name { get { return korean_name + "-" + english_name; } }
}
public struct R_Ticker
{
public string market { get; set; }
public string trade_date { get; set; }
public string trade_time { get; set; }
public string trade_date_kst { get; set; }
public string trade_time_kst { get; set; }
public decimal opening_price { get; set; }
public decimal high_price { get; set; }
public decimal low_price { get; set; }
public decimal trade_price { get; set; }
public decimal prev_closing_price { get; set; }
public string change { get; set; }
public decimal change_price { get; set; }
public decimal change_rate { get; set; }
public decimal signed_change_price { get; set; }
public decimal signed_change_rate { get; set; }
public decimal trade_volume { get; set; }
public decimal acc_trade_price { get; set; }
public decimal acc_trade_price_24h { get; set; }
public decimal acc_trade_volume { get; set; }
public decimal acc_trade_volume_24h { get; set; }
public decimal highest_52_week_price { get; set; }
public string highest_52_week_date { get; set; }
public decimal lowest_52_week_price { get; set; }
public string lowest_52_week_date { get; set; }
public string timestamp { get; set; }
}
}
[업비트 API] 자동매매 만들기
업비트API 자동매매 프로그램을 만들어보았습니다. 그에 대한 변천사를 작성해보려고 합니다. 우선 최초 설계 내용에 관해 적어보겠습니다. 현재 사용 중인 버전과 비교하니, 그만큼 발전해 온 것이 보여서 새삼 뿌듯하네요. 초기 화면은 크게 4등분으로 구성하였습니다. (저는 개인적으로 만들 때 모두 이렇게 4등분으로 시작해요)
- 우측상단에 Access Key 와 Secret Key 를 입력하고 [1.로그인] 을 합니다.
- 로그인을 하신 다음에 좌측상단의 값을 설정하고 [2.거래 시작] 을 누르면 진행합니다. 초기버전은 [시작]을 누르지도 않았는데 목록을 가져오도록 해놓았네요.
깜짝 놀라서 거래중단을 연타했더니 Log에 3줄이나 쌓였네요. 로그는 최근에 추가한 줄 알았더니 처음부터 해놓았군요.
로그인 버튼을 눌러보았습니다. 정상적인 경우 우측상단에 파랗게 표시되며, 오류가 있는 경우 붉은색으로 사유를 설명합니다. 처음 자동매매는 다음과 같이 구현하였습니다. 모든 텍스트가 영문인 이유는 컨트롤 명칭을 바로 찾기 위함이고, 상용화 할 생각이 없기 때문입니다. 고객에게 제공할 생각이었으면 한글로 했거나 언어선택을 넣었을 거예요. 해당 프로그램의 목적은 구매>판매의 자동매매 반복 입니다. 예시사진의 값으로 안내를 해드리자면 다음과 같습니다.
- 초기구매가격(Initial Buy Price)은 3,900,000원이며 수량은 거래금액(Total For Each) 5,100원에 맞추어 소수점 8자리까지 버림처리로 진행합니다.
- 초기구매가격(3,900,000) 이후 Initial Interval 3 단위만큼 낮추어 구매를 진행합니다.
- 즉 3,897,000원에 구매합니다. (3단위 아래)
- 실시간 거래금액 반영을 어떻게 할까 하다가 이렇게 만들었던 것으로 기억해요.
- Initial Interval 은 최초 1번만 적용이고 그 다음 거래부터는 Bid Interval 을 적용합니다.
- 3,897,000원 금액 다음에는 3,895,000원, 3,893,000원... 이렇게 Bid Count 10번까지 진행합니다.
- Limit Buy Price 는 최대한 구매할 의향이 있는 거래금액입니다.
사용한 용어 관련 안내사항입니다. 해당사항은 주식, 비트코인 등 여러 분야에서 사용할 수도 있을 것입니다.
- Market : 거래 시장
- Initial Buy Price : 초기 구매 가격 (구매 > 판매 반복 프로그램입니다)
- Limit Buy Price : 구매상한가 (올라가는 가격에 계속 올려서 구매하면 위험하니까 추가했음)
- Initial Interval : 초기 구매 가격 이후 최초의 구매 단위 간격
- Bid Interval : initial interval 이후의 구매 단위 간격
- Bid Count : 구매 수량
- Total For Each : 거래금액
- Bid 는 매수를 의미하며 Buy 와 같은 뜻입니다. 명칭이 좀 일관성이 없게 되었네요.
이렇게 구매 요청을 하고, 구매가 성사되면 구매가에서 점점 금액을 올리며 이윤이 남는지 확인하고 남는다고 판단하는 경우 판매를 진행하도록 하였습니다.
소스 중 일부를 발췌해봤습니다. C#으로 만들었어요.
좌측상단을 보니 이건 2022년 02월 06일 버전이네요. 이번 포스팅 주제는 초기 설계에 대한 것이라 설명은 생략할게요.
이번 포스팅 주제는 초기 설계에 대한 것이라 설명은 생략할게요.
소스 코드 예제
업비트 API를 사용한 프로그램 소스 코드 예제를 간단히 준비해 보았습니다.
뷰 소스코드
using AdsJumboWinForm;
using BeomUp.Model;
using Newtonsoft.Json.Linq;
using System;
using System.Collections;
using System.Collections.Generic;
using System.Data;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Net;
using System.Reflection;
using System.Text;
using System.Text.RegularExpressions;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Shapes;
using System.Windows.Threading;
namespace BeomUp.View
{
public partial class ViewBeomSang : Window
{
UpbitBeomSang m_m;
List<R_Market> m_c_markets;
List<R_Ticker> m_c_tickers;
DispatcherTimer m_timer;
PriceViewBeomSang[] ovcs = null;
public ViewBeomSang()
{
InitializeComponent();
m_m = new UpbitBeomSang();
m_c_markets = new List<R_Market>();
m_c_tickers = new List<R_Ticker>();
m_timer = new DispatcherTimer();
m_timer.Tick += timer_Tick;
}
private void DoExec()
{
try
{
string markets = txtMarkets.Text;
string[] s1 = markets.Split(',');
if (ovcs == null)
{
ovcs = new PriceViewBeomSang[s1.Length];
for (int i = 0; i < s1.Length; i++)
{
ovcs[i] = new PriceViewBeomSang(s1[i], i);
listBox.Items.Add(ovcs[i]);
}
}
if (GetTicker(markets))
{
foreach (var ticker in m_c_tickers)
{
foreach (var ovc in ovcs.Where(x => x.M_Market == ticker.market))
{
ovc.SetControl(ticker);
}
}
}
goto DoShowMsg;
DoShowMsg:
lblRunning.Content = string.Format("Running Interval : {0}", m_timer.Interval);
}
catch (Exception ex)
{
Log(GetExMsg(ex, MethodBase.GetCurrentMethod().Name));
}
}
private bool GetTicker(string _markets)
{
JArray jArray;
try
{
//***********************************************************
//BEGIN : QUOTATION API - 시세 Ticker 조회 - 현재가 정보(GET)
//***********************************************************
m_m.g_p_ticker.Init();
m_m.g_p_ticker = new P_Ticker(_markets);
jArray = m_m.GetJArray(UpbitBeomSang.Api.ticker);
m_c_tickers = jArray.ToObject<List<R_Ticker>>();
return true;
}
catch (Exception ex)
{
Log(GetExMsg(ex, MethodBase.GetCurrentMethod().Name));
return false;
}
}
private bool GetMarket()
{
JArray jArray;
try
{
//************************************************************
//BEGIN : QUOTATION API - 시세 종목 조회 - 마켓 코드 조회(GET)
//************************************************************
m_m.g_p_market.Init();
m_m.g_p_market = new P_Market(true);
jArray = m_m.GetJArray(UpbitBeomSang.Api.marketAll);
m_c_markets = jArray.ToObject<List<R_Market>>();
//Option
if (chkExBtc.IsChecked == true)
m_c_markets.RemoveAll(x => x.market.Split('-')[0] == "BTC");
if (chkExUsdt.IsChecked == true)
m_c_markets.RemoveAll(x => x.market.Split('-')[0] == "USDT");
//Source
lvMarketTable.ClearValue(ListView.ItemsSourceProperty);
lvMarketTable.ItemsSource = m_c_markets;
return true;
}
catch (Exception ex)
{
Log(GetExMsg(ex, MethodBase.GetCurrentMethod().Name));
return false;
}
}
private void Log(string _log, bool _setMsg = false)
{
try
{
if (txtLog.Text == string.Empty)
{
txtLog.Text = _log;
}
else
{
txtLog.Text += Environment.NewLine + _log;
}
txtLog.ScrollToEnd();
}
catch (Exception)
{
}
}
private void Clear()
{
ovcs = null;
listBox.Items.Clear();
}
public string GetExMsg(Exception _ex, string _nm)
{
string rtn;
try
{
WebException we = _ex as WebException;
if (we != null)
{
string p = string.Empty;
if (we.Response != null)
{
using (WebResponse response = we.Response)
{
using (Stream stream = response.GetResponseStream())
{
using (StreamReader reader = new StreamReader(stream))
{
p = reader.ReadToEnd();
}
}
}
}
else
{
p = "WebException.Response is null";
}
rtn = string.Format("[WebException] [{0}] {1} {2}", _nm, we.Message, p);
}
else
{
rtn = string.Format("[Exception] [{0}] {1}", _nm, _ex.Message);
}
return rtn;
}
catch (Exception newEx)
{
rtn = string.Format("[Exception] [{0}] {1} > {2}", _nm, _ex.Message, newEx.Message);
return rtn;
}
}
private void lv_SelectionChanged(object sender, SelectionChangedEventArgs e)
{
try
{
if (sender == lvMarketTable)
{
string vals = string.Empty;
IList items = ((ListView)sender).SelectedItems;
IEnumerable a = ((ListView)sender).SelectedItems;
if (items.Count > 0)
{
foreach (R_Market item in items)
{
vals += item.market + ",";
if (!Regex.IsMatch(txtMarkets.Text, item.market))
{
if (txtMarkets.Text == string.Empty)
txtMarkets.Text += item.market;
else
txtMarkets.Text += "," + item.market;
}
}
Log(string.Format("Markets : {0}", txtMarkets.Text));
}
}
else
{
Log(string.Format("[{0}] Event Not Delegated", MethodBase.GetCurrentMethod().Name));
}
}
catch (Exception ex)
{
Log(GetExMsg(ex, MethodBase.GetCurrentMethod().Name));
}
}
private void btn_Click(object sender, RoutedEventArgs e)
{
if (sender == btnSelectMarket)
{
GetMarket();
}
}
private void chk_Checked(object sender, RoutedEventArgs e)
{
if (sender == chkAct)
{
if (Regex.IsMatch(txtTimerInterval.Text, "^[0-9]+$") && !Regex.IsMatch(txtTimerInterval.Text, "^[0]+$"))
{
m_timer.Interval = TimeSpan.FromMilliseconds(Convert.ToDouble(txtTimerInterval.Text) * 1000);
m_timer.Start();
txtMarkets.IsReadOnly = true;
lblRunning.Content = string.Format("Start Running, Interval : {0}", m_timer.Interval);
lblRunning.Foreground = Brushes.Blue;
}
else
{
Log(string.Format("조회간격은 양의 정수 형태로 입력해 주세요.", txtTimerInterval.Text));
return;
}
}
}
private void chk_Unchecked(object sender, RoutedEventArgs e)
{
if (sender == chkAct)
{
m_timer.Stop();
txtMarkets.IsReadOnly = false;
lblRunning.Content = "Stop Running";
lblRunning.Foreground = Brushes.Black;
Clear();
}
}
private void timer_Tick(object sender, EventArgs e)
{
try
{
DoExec();
}
catch (Exception ex)
{
Log(GetExMsg(ex, MethodBase.GetCurrentMethod().Name));
}
}
}
}
<Window
xmlns:AdsJumboWinForm="clr-namespace:AdsJumboWinForm;assembly=AdsJumboWinForm"
x:Name="window" x:Class="BeomUp.View.ViewBeomSang"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:MaterialDesigninXaml="http://materialdesigninxaml.net/winfx/xaml/themes"
mc:Ignorable="d"
Opacity="{Binding Value, ElementName=sliOpa}"
Title="BeomUp" Height="600" Width="1000">
<Grid Background="Silver">
<Grid.RowDefinitions>
<RowDefinition Height="1*" />
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="1*"/>
<ColumnDefinition Width="1*"/>
</Grid.ColumnDefinitions>
<Grid Grid.Row="0" Grid.Column="0">
<Grid.RowDefinitions>
<RowDefinition Height="4*" />
<RowDefinition Height="4*" />
<RowDefinition Height="3*" />
</Grid.RowDefinitions>
<Grid Grid.Row="0" Grid.Column="0">
<GroupBox Header="조회" HorizontalAlignment="Stretch" VerticalAlignment="Stretch" >
<Grid>
<Label Content="Markets" HorizontalAlignment="Left" VerticalAlignment="Top" Margin="10,3,0,0"/>
<TextBox x:Name="txtMarkets" HorizontalAlignment="Left" Height="100" Margin="68,3,0,0" TextWrapping="Wrap" Text="KRW-BTC,KRW-ETH" VerticalAlignment="Top" Width="306"/>
<Label Content="콤마(,)로 구분해서 입력해 주세요. (예: KRW-BTC, BTC-ETH)" HorizontalAlignment="Left" VerticalAlignment="Top" Margin="59,102,-3,0"/>
<Button x:Name="btnSelectMarket" Content="시장 조회" HorizontalAlignment="Left" Margin="5,34,0,0" VerticalAlignment="Top" Width="58" RenderTransformOrigin="0.247,0.433" Click="btn_Click"/>
<CheckBox x:Name="chkExBtc" IsChecked="True" Content="BTC" HorizontalAlignment="Left" Margin="10,83,0,0" VerticalAlignment="Top"/>
<CheckBox x:Name="chkExUsdt" IsChecked="True" Content="USDT" HorizontalAlignment="Left" Margin="10,98,0,0" VerticalAlignment="Top"/>
<Label Content="제외 시장" HorizontalAlignment="Left" VerticalAlignment="Top" Margin="5,57,0,0"/>
</Grid>
</GroupBox>
</Grid>
<Grid Grid.Row="1" Grid.Column="0">
<GroupBox Header="시장">
<ListView Name="lvMarketTable" SelectionChanged="lv_SelectionChanged">
<ListView.View>
<GridView>
<GridViewColumn Header="시장" DisplayMemberBinding="{Binding market}"/>
<GridViewColumn Header="한글명" DisplayMemberBinding="{Binding korean_name}"/>
<GridViewColumn Header="영문명" DisplayMemberBinding="{Binding english_name}"/>
<GridViewColumn Header="유의종목" DisplayMemberBinding="{Binding market_warning}"/>
</GridView>
</ListView.View>
</ListView>
</GroupBox>
</Grid>
<Grid Grid.Row="2" Grid.Column="0">
<GroupBox Header="Log" HorizontalAlignment="Stretch" VerticalAlignment="Stretch" >
<TextBox x:Name="txtLog"
Background="Transparent"
BorderThickness="0"
Text="{Binding Text, Mode=OneWay}"
IsReadOnly="True"
TextWrapping="Wrap"
ScrollViewer.VerticalScrollBarVisibility="Auto"
/>
</GroupBox>
</Grid>
</Grid>
<Grid Grid.Row="0" Grid.Column="1">
<Grid.RowDefinitions>
<RowDefinition Height="1.5*" />
<RowDefinition Height="7.5*" />
</Grid.RowDefinitions>
<Grid Grid.Row="0" Grid.Column="0">
<CheckBox x:Name="chkAct" Content="시작" HorizontalAlignment="Left" Margin="10,10,0,0" VerticalAlignment="Top" Checked="chk_Checked" Unchecked="chk_Unchecked"/>
<TextBox x:Name="txtTimerInterval" HorizontalAlignment="Left" Height="23" Margin="92,34,0,0" TextWrapping="Wrap" Text="1" VerticalAlignment="Top" Width="26"/>
<Label Content="조회간격(초)" HorizontalAlignment="Left" Margin="10,30,0,0" VerticalAlignment="Top"/>
<Label x:Name="lblRunning" Content="~" HorizontalAlignment="Left" Margin="10,61,0,0" VerticalAlignment="Top"/>
</Grid>
<Grid Grid.Row="1" Grid.Column="0">
<ScrollViewer VerticalScrollBarVisibility="Auto">
<ListBox x:Name="listBox"></ListBox>
</ScrollViewer>
</Grid>
</Grid>
</Grid>
</Window>
오더 뷰 (사용자 컨트롤)
using BeomUp.Model;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Text.RegularExpressions;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Navigation;
using System.Windows.Shapes;
namespace BeomUp.View
{
public partial class PriceViewBeomSang : UserControl
{
string m_market = string.Empty;
public string M_Market
{
private set
{
m_market = value;
lblMarket.Content = value;
}
get { return m_market; }
}
public PriceViewBeomSang(string market, int seq)
{
InitializeComponent();
M_Market = market;
if (seq % 2 == 0)
grdCon.Background = Brushes.Silver;
}
public PriceViewBeomSang(R_Ticker ticker)
{
InitializeComponent();
}
public void SetControl(R_Ticker ticker)
{
if (m_market != ticker.market)
return;
//시장
lblMarket.Content = ticker.market;
//전일종가
lblPrevClosingPrice.Content = string.Format("{0:#,0}", ticker.prev_closing_price);
//
lblTradePrice.Content = string.Format("{0:#,0}", ticker.trade_price);
SetColor(lblTradePrice, lblPrevClosingPrice);
//
lblHighPrice.Content = string.Format("{0:#,0}", ticker.high_price);
SetColor(lblHighPrice, lblPrevClosingPrice);
//
lblLowPrice.Content = string.Format("{0:#,0}", ticker.low_price);
SetColor(lblLowPrice, lblPrevClosingPrice);
//현재가
lblTradePriceRat.Content = GetRat(lblTradePrice, lblPrevClosingPrice);
SetColorCompareToZero(lblTradePriceRat);
//상한가
lblHighPriceRat.Content = GetRat(lblHighPrice, lblPrevClosingPrice);
SetColorCompareToZero(lblHighPriceRat);
//하한가
lblLowPriceRat.Content = GetRat(lblLowPrice, lblPrevClosingPrice);
SetColorCompareToZero(lblLowPriceRat);
}
private decimal GetRat(Label _a, Label _b)
{
decimal v1 = Convert.ToDecimal(_a.Content);
decimal v0 = Convert.ToDecimal(_b.Content);
return Math.Round(((v1 - v0) / v0) * 100, 2);
}
private void SetColor(Label _a, Label _b)
{
if (Convert.ToDecimal(_a.Content) > Convert.ToDecimal(_b.Content))
_a.Foreground = Brushes.Red;
else if (Convert.ToDecimal(_a.Content) < Convert.ToDecimal(_b.Content))
_a.Foreground = Brushes.Blue;
else
_a.Foreground = Brushes.Black;
}
private void SetColorCompareToZero(Label _a)
{
if (Convert.ToDecimal(_a.Content) > 0)
_a.Foreground = Brushes.Red;
else if (Convert.ToDecimal(_a.Content) < 0)
_a.Foreground = Brushes.Blue;
else
_a.Foreground = Brushes.Black;
}
}
}
<UserControl x:Class="BeomUp.View.PriceViewBeomSang"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:local="clr-namespace:BeomUp.View"
xmlns:Models="clr-namespace:BeomUp.Model"
xmlns:materialDesignin="http://materialdesigninxaml.net/winfx/xaml/themes"
mc:Ignorable="d"
d:DesignHeight="110" Width="400"
HorizontalAlignment="Stretch">
<Grid x:Name="grdCon">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="3*"/>
<ColumnDefinition Width="7*"/>
</Grid.ColumnDefinitions>
<Grid Grid.Row="0" Grid.Column="0">
<Label x:Name="lblMarket" Content="lblMarket" HorizontalAlignment="Left" VerticalAlignment="Center"/>
</Grid>
<Grid Grid.Row="0" Grid.Column="1">
<Grid.RowDefinitions>
<RowDefinition Height="1*"/>
<RowDefinition Height="1*"/>
<RowDefinition Height="1*"/>
<RowDefinition Height="1*"/>
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="4*"/>
<ColumnDefinition Width="3*"/>
<ColumnDefinition Width="2*"/>
</Grid.ColumnDefinitions>
<Grid Grid.Row="0" Grid.Column="0">
<Label x:Name="lblTradePrice" Content="lblTradePrice" HorizontalAlignment="Left" VerticalAlignment="Center"/>
</Grid>
<Grid Grid.Row="0" Grid.Column="1">
<Label x:Name="lblTradePriceRat" HorizontalAlignment="Left" VerticalAlignment="Center"/>
</Grid>
<Grid Grid.Row="0" Grid.Column="2">
<Label Content="현재가" HorizontalAlignment="Left" VerticalAlignment="Center"/>
</Grid>
<Grid Grid.Row="1" Grid.Column="0">
<Label x:Name="lblPrevClosingPrice" Content="lblPrevClosingPrice" HorizontalAlignment="Left" VerticalAlignment="Center"/>
</Grid>
<Grid Grid.Row="1" Grid.Column="1">
<Label x:Name="lblPrevClosingPriceRat" HorizontalAlignment="Left" VerticalAlignment="Center"/>
</Grid>
<Grid Grid.Row="1" Grid.Column="2">
<Label Content="전일종가" HorizontalAlignment="Left" VerticalAlignment="Center"/>
</Grid>
<Grid Grid.Row="2" Grid.Column="0">
<Label x:Name="lblHighPrice" Content="lblHighPrice" HorizontalAlignment="Left" VerticalAlignment="Center"/>
</Grid>
<Grid Grid.Row="2" Grid.Column="1">
<Label x:Name="lblHighPriceRat" HorizontalAlignment="Left" VerticalAlignment="Center"/>
</Grid>
<Grid Grid.Row="2" Grid.Column="2">
<Label Content="당일고가" HorizontalAlignment="Left" VerticalAlignment="Center"/>
</Grid>
<Grid Grid.Row="3" Grid.Column="0">
<Label x:Name="lblLowPrice" Content="lblLowPrice" HorizontalAlignment="Left" VerticalAlignment="Center"/>
</Grid>
<Grid Grid.Row="3" Grid.Column="1">
<Label x:Name="lblLowPriceRat" HorizontalAlignment="Left" VerticalAlignment="Center"/>
</Grid>
<Grid Grid.Row="3" Grid.Column="2">
<Label Content="당일저가" HorizontalAlignment="Left" VerticalAlignment="Center"/>
</Grid>
</Grid>
</Grid>
</UserControl>
모델
using Microsoft.IdentityModel.Tokens;
using Newtonsoft.Json.Linq;
using System;
using System.Collections.Generic;
using System.IdentityModel.Tokens.Jwt;
using System.IO;
using System.Linq;
using System.Net;
using System.Security.Cryptography;
using System.Text;
using System.Threading.Tasks;
namespace BeomUp.Model
{
public class UpbitBeomSang
{
//Quotation API > 시세 종목 조회 > 마켓 코드 조회
public const string g_marketEndpoint = "https://api.upbit.com/v1/market/all";
//Quotation API > 시세 Ticker 조회 > 현재가 정보
public const string g_tickerEndpoint = "https://api.upbit.com/v1/ticker";
public P_Market g_p_market = new P_Market();
public P_Ticker g_p_ticker = new P_Ticker();
public enum Api
{
marketAll,
ticker
}
public JArray GetJArray(Api _api)
{
JwtPayload payload = null;
byte[] keyBytes = null;
SymmetricSecurityKey securityKey = null;
SigningCredentials credentials = null;
JwtHeader header = null;
JwtSecurityToken secToken = null;
string jwtToken = null;
HttpWebRequest request = null;
string requestUri = null;
string requestParam = null;
StringBuilder builder = null;
string queryString = null;
SHA512 sha512 = null;
byte[] queryHashByteArray = null;
string queryHash = null;
JArray rtn = null;
try
{
switch (_api)
{
case Api.marketAll:
requestUri = g_marketEndpoint;
requestUri += $"?isDetails={g_p_market.isDetails}";
break;
case Api.ticker:
requestUri = g_tickerEndpoint;
requestUri += $"?markets={g_p_ticker.markets}";
break;
default:
throw new Exception("Bad Param : " + _api.ToString());
}
request = (HttpWebRequest)WebRequest.Create(requestUri);
request.Method = "GET";
using (WebResponse response = request.GetResponse())
{
using (Stream stream = response.GetResponseStream())
{
using (StreamReader reader = new StreamReader(stream))
{
string p = reader.ReadToEnd();
rtn = JArray.Parse(p);
return rtn;
}
}
}
throw new Exception("Parsing failed");
}
catch (Exception ex)
{
throw ex;
}
}
}
public struct P_Market
{
public bool isDetails { get; private set; }
public P_Market(bool isDetails)
{
this.isDetails = isDetails;
}
public void Init()
{
isDetails = true;
}
}
public struct P_Ticker
{
public string markets { get; private set; }
public P_Ticker(string markets)
{
this.markets = markets;
}
public void Init()
{
markets = string.Empty;
}
}
public struct R_Market
{
public string market_warning { get; set; }
public string market { get; set; }
public string korean_name { get; set; }
public string english_name { get; set; }
public string name { get { return korean_name + "-" + english_name; } }
}
public struct R_Ticker
{
public string market { get; set; }
public string trade_date { get; set; }
public string trade_time { get; set; }
public string trade_date_kst { get; set; }
public string trade_time_kst { get; set; }
public decimal opening_price { get; set; }
public decimal high_price { get; set; }
public decimal low_price { get; set; }
public decimal trade_price { get; set; }
public decimal prev_closing_price { get; set; }
public string change { get; set; }
public decimal change_price { get; set; }
public decimal change_rate { get; set; }
public decimal signed_change_price { get; set; }
public decimal signed_change_rate { get; set; }
public decimal trade_volume { get; set; }
public decimal acc_trade_price { get; set; }
public decimal acc_trade_price_24h { get; set; }
public decimal acc_trade_volume { get; set; }
public decimal acc_trade_volume_24h { get; set; }
public decimal highest_52_week_price { get; set; }
public string highest_52_week_date { get; set; }
public decimal lowest_52_week_price { get; set; }
public string lowest_52_week_date { get; set; }
public string timestamp { get; set; }
}
}