하아찡

[C#/WPF] Upbit프로젝트 Balance 본문

C#/코인프로그램 - 코드

[C#/WPF] Upbit프로젝트 Balance

하아찡 2023. 11. 29. 01:43

등록된 UpbitAPI키의 잔고를 확인해 줌.

 

미리 보기

 

 

Balance.xaml

<UserControl x:Class="Upbit.Views.Balance"
             xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
             xmlns:prism="http://prismlibrary.com/"             
             xmlns:conv="clr-namespace:Upbit.Converter"
             xmlns:custom="clr-namespace:TextBoxPlaceHolder;assembly=TextBoxPlaceHolder"
             prism:ViewModelLocator.AutoWireViewModel="True">
    <UserControl.Resources>
        <conv:IsVisibilityConverter x:Key="isVisiblityConverter"/>
        <conv:TwoMultipleConverter x:Key="TwoMultipleConverter"/>
        <conv:SubConverter x:Key="SubConverter"/>
        <Style x:Key="ListviewHeader" TargetType="{x:Type GridViewColumnHeader}">
            <Setter Property="Foreground" Value="{Binding MyColors.ColorText}"/>
            <Setter Property="FontWeight" Value="Bold"/>
            <Setter Property="Background" Value="{Binding MyColors.ColorBack}"/>
        </Style>
        
    </UserControl.Resources>
    <Grid>
        <Grid.RowDefinitions>
            <RowDefinition Height="1*"/>
        </Grid.RowDefinitions>

        <Grid Grid.Row="0"  >
            <!-- 인증 안됐을때 텍스트로 출력 -->
            <TextBlock Panel.ZIndex="1" Foreground="{Binding MyColors.ColorText}" VerticalAlignment="Center" HorizontalAlignment="Center" Text="{Binding Lang.LNotAccess}" Visibility="{Binding NoneAccessView}" />
            <ListView  ItemsSource="{Binding Accounts}" Foreground="{Binding MyColors.ColorText}" >
                <ListView.ItemTemplate>
                    <DataTemplate>
                        <Grid>
                            <StackPanel Orientation="Horizontal">
                                <TextBlock Foreground="{Binding Color}" Text="{Binding CoinName}" Margin="0,0,10,0" Width="80"/>
                                <TextBlock Foreground="{Binding Color}" Margin="0,0,10,0"  Width="80">
                                    <TextBlock.Text>
                                        <MultiBinding Converter="{StaticResource SubConverter}">
                                            <Binding Path="TotalPrice" />
                                            <Binding Path="Cost" />
                                        </MultiBinding>
                                    </TextBlock.Text>
                                </TextBlock>
                                <TextBlock Foreground="{Binding Color}" Text="{Binding Rate,StringFormat={}{0:0.##}%}" Width="50" Margin="0,0,10,0"/>
                                <TextBlock Foreground="{Binding Color}" Text="{Binding AvgBuyPrice}" Margin="0,0,10,0"  Width="80"/>
                                <TextBlock Foreground="{Binding Color}" Text="{Binding NowPrice}" Width="100" Margin="0,0,10,0"/>
                                <TextBlock Foreground="{Binding Color}" Text="{Binding TotalPrice}" Margin="0,0,10,0"/>
                            </StackPanel>
                        </Grid>
                    </DataTemplate>
                </ListView.ItemTemplate>
            </ListView>
        </Grid>
    </Grid>
</UserControl>

 

 

API인증이 안 됐을 경우 API인증을 해달라고 띄워주는 TextBlock

<TextBlock Panel.ZIndex="1" Foreground="{Binding MyColors.ColorText}" VerticalAlignment="Center" HorizontalAlignment="Center" Text="{Binding Lang.LNotAccess}" Visibility="{Binding NoneAccessView}" />

 

 

 

평가손익

<TextBlock Foreground="{Binding Color}" Margin="0,0,10,0"  Width="80">
    <TextBlock.Text>
        <MultiBinding Converter="{StaticResource SubConverter}">
            <Binding Path="TotalPrice" />
            <Binding Path="Cost" />
        </MultiBinding>
    </TextBlock.Text>
</TextBlock>

TotalPrice -> 현재가 * 현재보유량

Cost -> 평단가 * 현재보유량

MultiBinding을 사용해서 TotalPrice랑 Cost를 뺀 가격이 평가손익

SubConvert역할은 해당 두 데이터를 빼가지고 출력해 줌.

 

 

SubConvert.cs

using System;
using System.Globalization;
using System.Windows.Data;

namespace Upbit.Converter
{
    class SubConverter : IMultiValueConverter
    {
        public object Convert(object[] values, Type targetType, object parameter, CultureInfo culture)
        {
            // 여기서 values 배열에 여러 개의 값이 포함됩니다.
            // 원하는 계산을 수행하고 계산 결과를 반환하세요.
            double result = 0; // 여기에 원하는 계산을 수행하세요
            bool b = true;
            try
            {
                foreach (var item in values) 
                {
                    if (b)
                    {
                        result = (double)item;
                        b = false;
                    }
                    else
                    {
                        result -= (double)item;
                    }
                }

            }
            catch (Exception ex)
            {

            }

            return result.ToString().Split(".")[0];
        }

        public object[] ConvertBack(object value, Type[] targetTypes, object parameter, CultureInfo culture)
        {
            throw new NotImplementedException();
        }
    }
}

Xaml에서 출력할 때 데이터를 받아서 변환시켜서 출력하는 법이 있어서 사용했습니다.

 

다른 xaml 쪽은 그냥 데이터를 바인딩해서 사용해서 별다른 설명은 생략하겠습니다.

 

 

BalanceViewModel.cs

using Prism.Commands;
using Prism.Mvvm;
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using System.Windows.Threading;
using Upbit.UpbitFunctions;
using System.Windows;
using Upbit.UpbitFunctions.Interface;
using Language;
using System.Windows.Media;
using PublicColor;
using Prism.Events;
using Upbit.Event;
using Upbit.Functions.Interface;
using Upbit.Functions;
using System.Windows.Controls.Primitives;
using System.Linq;

namespace Upbit.ViewModels
{
    public class BalanceViewModel : BindableBase,IAccess, ILanguage, IColors, IWebSocketTicker, IMyTrade
    {
        #region Lang
        private Language.Lang.Balance lang;
        public Language.Lang.Balance Lang
        {
            get { if (lang is null) lang = new Language.Lang.Balance(); return lang; }
            set { SetProperty(ref lang, value); }
        }
        public virtual void SetLanguage()
        {
            Lang.UpdateLang();
            SetCoinName();
        }
        
        #endregion

        #region Colors
        /// <summary>
        /// 색상 업데이트 이벤트
        /// </summary>
        public void EventUpdateColor()
        {
            MyColors.ColorText = PublicColor.Colors.Colorinstance.ColorText;
            MyColors.ColorBack = PublicColor.Colors.Colorinstance.ColorBack;
            MyColors.ColorBid = PublicColor.Colors.Colorinstance.ColorBid;
            mycolors.ColorAsk = PublicColor.Colors.Colorinstance.ColorAsk;
        }

        private PublicColor.Colors mycolors;
        public PublicColor.Colors MyColors
        {
            get {  if (mycolors is null) mycolors = new PublicColor.Colors();
                return mycolors; }
            set { SetProperty(ref mycolors, value); }
        }
        #endregion Colors

        #region Prism
        private IEventAggregator _ea;
        #endregion Prism

        #region Models

        private Structs.Accounts myaccount;
        public Structs.Accounts MyAccount
        {
            get { return myaccount; }
            set { SetProperty(ref myaccount, value); }
        }

        private Visibility noneaccessview;
        public Visibility NoneAccessView
        {
            get { return noneaccessview; }
            set { SetProperty(ref noneaccessview, value); }
        }

        public bool IsBalance { get; private set; } 

        private UpbitFunctions.Accounts FAccount = new UpbitFunctions.Accounts();

        private List<Structs.Accounts> accounts;
        public List<Structs.Accounts> Accounts
        {
            get { return accounts; }
            set { 
                SetProperty(ref accounts, value);
            }
        }



        private List<Structs.MarketCodes> Markets;
        #endregion Models



        public void TradeEvent(Structs.Trade tr)
        {
            throw new NotImplementedException();
        }

        public void OrderEvent(Structs.Orderbook ob)
        {
            throw new NotImplementedException();
        }

        public void TickerEvent(Structs.Ticker tick)
        {
            if(Accounts is not null)
            {
                if(Accounts.Count > 0)
                {
                    for(int i = 0; i < Accounts.Count; i++)
                    {
                        if(tick.cd == Accounts[i].Market)
                        {
                            Accounts[i].NowPrice = tick.tp;
                            Accounts[i].TotalPrice = tick.tp;
                            Accounts[i].Rate = tick.tp;
                            Accounts[i].Color = SetTextColor(Accounts[i].NowPrice , Convert.ToDouble(Accounts[i].AvgBuyPrice));

                            break;
                        }
                    }
                }
            }
        }

        private Brush SetTextColor(double now, double avg)
        {
            Brush val = Brushes.Transparent;

            if (now >= avg)
                val = MyColors.ColorBid;
            else
                val = MyColors.ColorAsk;


            return val;
        }

        /// <summary>
        /// key값을 전달받았을때 이벤트 
        /// </summary>
        /// <param name="b"></param>
        public void D_AccessEvent(bool b)
        {
            IsBalance = Access.UpbitInstance.GetAccess();
            if (IsBalance)
            {
                NoneAccessView = Visibility.Collapsed;
                Accounts = Task.Run(() => FAccount.AccountsAsync()).Result;
                PullOutMyAccount(ref accounts);
                SetCoinName();
            }
            else
            {
                Accounts = new List<Structs.Accounts>();
                NoneAccessView = Visibility.Visible;
            }
        }

        private void SetCoinName()
        {
            if (Accounts is null)
                return;
            for(int i = 0; i < Accounts.Count; i++) 
            {
                foreach(var v in Markets)
                {
                    if(v.Market == Accounts[i].Market)
                    {
                        string str = "";
                        if (Language.Language.Lang.LangType == "kr")
                            str = v.Korean_name;
                        else
                            str = v.English_name;
                        Accounts[i].CoinName = str;
                        break;  
                    }
                }
            }
        }
        private void PullOutMyAccount(ref List<Structs.Accounts> list)
        {
            if (Accounts is null)
                return;
            for (int i = 0; i < list.Count; i++)
            {
                if("KRW" == list[i].Currency)
                {
                    myaccount = list[i];
                    list.RemoveAt(i);
                    break;
                }
            }
            
        }
        #region Account Timer Functions

        
        /// <summary>
        /// 잔고 갱신
        /// </summary>
        public void RefreshAccount()
        {
            List<Structs.Accounts> list = Task.Run(() => FAccount.AccountsAsync()).Result;
            PullOutMyAccount(ref list);
            
            for (int j = 0; j < list.Count; j++)
            {
                
                for (int i = 0; i < Accounts.Count; i++)
                {
                    if (Accounts[i].Market == list[j].Market)
                    {
                        list[j].CoinName = Accounts[i].CoinName;
                        list[j].NowPrice = Accounts[i].NowPrice;
                        list[j].TotalPrice = Accounts[i].NowPrice;
                        list[j].Color = SetTextColor(Accounts[i].NowPrice, Convert.ToDouble(Accounts[i].AvgBuyPrice));
                        

                        break;
                    }
                }
            }
            Accounts = list;
            SetCoinName();
        }

        /// <summary>
        /// 코인 주문 성공했을때 현재 잔고 갱긴을위해사용
        /// </summary>
        /// <param name="b"></param>
        public void RefreshAccount(bool b)
        {
            if (b)
            {
                RefreshAccount();
            }
        }



        public virtual void MyTradeEvent(Structs.MyTrade mytrade)
        {

            //원화는 그냥 불러와서 계산하는게 정확할거같아서 불러다씀
            RefreshAccount();
        }




        #endregion Account Timer Functions


        public BalanceViewModel(IEventAggregator ea)
        {
            _ea = ea;

            EventUpdateColor();
            SetLanguage();

            Accounts = new List<Structs.Accounts>();

            _ea.GetEvent<MessageBoolEvent>().Subscribe(RefreshAccount);

            //Key값 이벤트 등록
            Access.CheckedAccess += (D_AccessEvent);
            PublicFunctions Upbitpf = new PublicFunctions();
            Markets = Task.Run(() => Upbitpf.MarketsAsync()).Result;
            //색변경 이벤트 등록
            PublicColor.Colors.ColorUpdate += (EventUpdateColor);

            //언어 변경시 이벤트 등록
            Language.Language.LangChangeEvent += (SetLanguage);

            //실시간 틱데이터를 받아오기위해 이벤트등록
            WebSocketTicker.TickerEvent += new WebSocketTicker.TickEventHandler(TickerEvent);

            MyTradeClientWebSocket.MyTradeEvent += new MyTradeClientWebSocket.MyTradeEventHandler(MyTradeEvent);

            IsBalance = Access.UpbitInstance.GetAccess();
            if (IsBalance)
            {
                NoneAccessView = Visibility.Collapsed;
                Accounts = Task.Run(() => FAccount.AccountsAsync()).Result;
                PullOutMyAccount(ref accounts);
                SetCoinName();

            }
            else
            {
                Accounts = new List<Structs.Accounts>();
                NoneAccessView = Visibility.Visible;
            }
        }

    }
}

 

 

 

생성자

public BalanceViewModel(IEventAggregator ea)
{
    _ea = ea;

    EventUpdateColor();
    SetLanguage();

    Accounts = new List<Structs.Accounts>();

    _ea.GetEvent<MessageBoolEvent>().Subscribe(RefreshAccount);

    //Key값 이벤트 등록
    Access.CheckedAccess += (D_AccessEvent);
    PublicFunctions Upbitpf = new PublicFunctions();
    Markets = Task.Run(() => Upbitpf.MarketsAsync()).Result;
    //색변경 이벤트 등록
    PublicColor.Colors.ColorUpdate += (EventUpdateColor);

    //언어 변경시 이벤트 등록
    Language.Language.LangChangeEvent += (SetLanguage);

    //실시간 틱데이터를 받아오기위해 이벤트등록
    WebSocketTicker.TickerEvent += new WebSocketTicker.TickEventHandler(TickerEvent);

    MyTradeClientWebSocket.MyTradeEvent += new MyTradeClientWebSocket.MyTradeEventHandler(MyTradeEvent);

    IsBalance = Access.UpbitInstance.GetAccess();
    if (IsBalance)
    {
        NoneAccessView = Visibility.Collapsed;
        Accounts = Task.Run(() => FAccount.AccountsAsync()).Result;
        PullOutMyAccount(ref accounts);
        SetCoinName();

    }
    else
    {
        Accounts = new List<Structs.Accounts>();
        NoneAccessView = Visibility.Visible;
    }
}

 

UI쪽 Textblock을 보여주거나 숨기는 역할

보여주기 -> NoneAccessView = Visibility.Visible;

숨기기 -> NoneAccessView = Visibility.Collapsed;

 

 

잔고 내부 KRW(원화)제거

private void PullOutMyAccount(ref List<Structs.Accounts> list)
{
    if (Accounts is null)
        return;
    for (int i = 0; i < list.Count; i++)
    {
        if("KRW" == list[i].Currency)
        {
            myaccount = list[i];
            list.RemoveAt(i);
            break;
        }
    }
    
}

내 잔고를 원화와 코인잔고를 구분하기 위해 Account에 들어가 있는 KRW값을 제거함.

제거하는 이유는 Account값 자체가 UI쪽에 Binding 해놔 가지고 제거하지 않으면 원화도 출력하게 됨.

 

 

잔고에 들어있는 코인이름을 언어로 변경

private void SetCoinName()
{
    if (Accounts is null)
        return;
    for(int i = 0; i < Accounts.Count; i++) 
    {
        foreach(var v in Markets)
        {
            if(v.Market == Accounts[i].Market)
            {
                string str = "";
                if (Language.Language.Lang.LangType == "kr")
                    str = v.Korean_name;
                else
                    str = v.English_name;
                Accounts[i].CoinName = str;
                break;  
            }
        }
    }
}

해당 작업을 하는 이유는 잔고를 서버에서 받아왔을 때 예를 들어 KRW BTC 이런 식으로 전달받기 때문에

"BTC -> 비트코인"이런식으로 변경하는 작업을 해줌.

해당작업에서 선택된 언어가 변경됐을 때 언어 변경하는 처리도 넣어뒀음.

언어가 변경된 처리를 하는 부분은 코드 내의 Language.Language.Lang.LangType 살펴보시면 됩니다.

 

 

TickerEvent

public void TickerEvent(Structs.Ticker tick)
{
    if(Accounts is not null)
    {
        if(Accounts.Count > 0)
        {
            for(int i = 0; i < Accounts.Count; i++)
            {
                if(tick.cd == Accounts[i].Market)
                {
                    Accounts[i].NowPrice = tick.tp;
                    Accounts[i].TotalPrice = tick.tp;
                    Accounts[i].Rate = tick.tp;
                    Accounts[i].Color = SetTextColor(Accounts[i].NowPrice , Convert.ToDouble(Accounts[i].AvgBuyPrice));

                    break;
                }
            }
        }
    }
}

해당 함수는 Websocket에서 Tick이벤트 발생했을때 활성화 되는 이벤트 입니다.

내 잔고에 존재하는 코인들 현재가를 받아오기 위해 사용함.

해당 코드가 추가됨으로 평가손익, 평가금액 등등 여러 계산을 할 수 있게됨.

 

 

 

MyTradeEvent

public virtual void MyTradeEvent(Structs.MyTrade mytrade)
{

    //원화는 그냥 불러와서 계산하는게 정확할거같아서 불러다씀
    RefreshAccount();
}

내 체결이 일어났을경우 해당 함수가 실행이됩니다.

 

생성자쪽에 보시면 아래 코드가 해당 함수를 이벤트 발생시 실행시켜주는얘

MyTradeClientWebSocket.MyTradeEvent += new MyTradeClientWebSocket.MyTradeEventHandler(MyTradeEvent);

 

 

 

RefreshAccount

/// <summary>
/// 잔고 갱신
/// </summary>
public void RefreshAccount()
{
    List<Structs.Accounts> list = Task.Run(() => FAccount.AccountsAsync()).Result;
    PullOutMyAccount(ref list);
    
    for (int j = 0; j < list.Count; j++)
    {
        
        for (int i = 0; i < Accounts.Count; i++)
        {
            if (Accounts[i].Market == list[j].Market)
            {
                list[j].CoinName = Accounts[i].CoinName;
                list[j].NowPrice = Accounts[i].NowPrice;
                list[j].TotalPrice = Accounts[i].NowPrice;
                list[j].Color = SetTextColor(Accounts[i].NowPrice, Convert.ToDouble(Accounts[i].AvgBuyPrice));
                

                break;
            }
        }
    }
    Accounts = list;
    SetCoinName();
}

잔고를 갱신해주는 함수입니다.

기존에 데이터를 새로받아온 데이터에다가 다시 뒤엎는 방식으로 진행했음.. 더 깔끔한 방법이 있을거같은데 List.Add랑 List.Remove를 사용시 UI쪽에 갱신이 안되가지고 그냥 엎어버렸음.

 

 

 

작업후기

원래 Balance코드에는 타이머가 존재했는데 myTrade가 생겨가지고 타이머를 제거한 후 재 작업이 들어갔습니다.

타이머로 했을땐 너무 조잡했는데 확실하게 깔끔해진 기분 :)

 

 

일부 수정

https://thesh.tistory.com/61

 

[C#/WPF/수정] Upbit프로젝트 Balance

솔직히 UI디자인 감도안잡히고 뭐가 나은지도모르겠어가지고 일단 수정한 부분 업로드함. Balance.xaml BalanceViewModel.cs using Prism.Commands; using Prism.Mvvm; using System; using System.Collections.Generic; using System.Th

thesh.tistory.com

 

반응형