평범한 이야기들

[PHP] PHP를 이용해 업비트(upbit) API 통신하기 #4 리뉴얼 본문

평범한 개발 이야기/PHP

[PHP] PHP를 이용해 업비트(upbit) API 통신하기 #4 리뉴얼

songsariya 2021. 12. 10. 16:33
728x90
2021.03.18 - [평범한 개발 이야기/PHP] - [PHP] PHP를 이용해 업비트(upbit) API 통신하기 #1 계좌정보 가져오기
2021.03.22 - [평범한 개발 이야기/PHP] - [PHP] PHP를 이용해 업비트(upbit) API 통신하기 #2 분, 일 캔들 정보 가져오기
2021.03.29 - [평범한 개발 이야기/PHP] - [PHP] PHP를 이용해 업비트(upbit) API 통신하기 #3 매수, 매도 하기

 

 지난 3월에 만들었던 업비트(upbit)에서 제공하는 API를 이용해서 실시간으로 통신하는 프로그램을 만들었습니다. 그 후 잠시 손을 놓고 있었는데 최근에 다시 확인해 볼 일이 있어서 프로그램을 열어보았고 좀 더 쉽게 사용할 수 있도록 변경을 해보았습니다.

 

필요 패키지

 업비트는 REST API 요청 시, 발급받은 access key와 secret key로 토큰을 생성하여 Authorization 헤더를 통해 전송합니다. 토큰은 JWT(https://jwt.io) 형식을 따릅니다. JWT 사이트에 가시면 다양한 언어와 버전, 그리고 설치하는 방법 등 들이 나와있으니 확인해보시면 됩니다. 저는 firebase/php-jwt 버전으로 설치했습니다. 설치는 composer를 이용했습니다.

composer require firebase/php-jwt

 

 두번째로는 HTTP client 라이브러리 인 guzzlehttp/guzzle  라이브러리를 설치했습니다. 기존에 curl로 만들었던 부분을 psr-7, psr-18에 맞는 인터페이스를 PHP에서 권장하는 방법으로 수정을 했습니다.  guzzlehttp/guzzle 은 쉽게 설치를 할 수 있고, 사용도 쉽게 할 수 있기 때문에 많은 곳에서 사용하고 있는 라이브러리입니다. 설치는 composer를 이용했습니다.

composer require guzzlehttp/guzzle

 

소스코드

소스는 기본적으로 캔들조회, 계좌조회, 주문, 주문 취소 등 가장 기본적인 기능만 추가해서 작성했습니다. 소스코드는 깃허브 (https://github.com/songsariya/upbit-php.git)에 등록되었습니다. 

 

아래는 소스코드 전체 입니다. 

<?php

use Exception;
use Firebase\JWT\JWT;

/**
 * Upbit Api 
 * Upbit 서버와 통신을 하는 메소드로 구성
 */
class UpbitApi
{

    const POST = "POST";
    const GET = "GET";
    const PUT = "PUT";
    const DELETE = "DELETE";

    const HTTP_CODE_200 = "200";

    const SIDE_BID = "bid"; // 매수
    const SIDE_ASK = "ask"; // 매도

    const ORD_TYPE_LIMIT = "limit"; // 지정가 주문
    const ORD_TYPE_PRICE = "price"; // 시장가 주문 (매수)
    const ORD_TYPE_MARKET = "market"; // 시장가 주문 (매도)


    private $accessKey;
    private $secretKey;
    private \GuzzleHttp\Client $client;

    /**
     * 생성자
     */
    public function __construct(string $accessKey, string $secretKey)
    {
        $this->accessKey = $accessKey;
        $this->secretKey = $secretKey;
        $this->client = new \GuzzleHttp\Client(['base_uri' => 'https://api.upbit.com/v1/']);
    }

    /**
     * 마켓 코드 조회
     *
     * @return array
     */
    public function getMarketList(): array
    {
        $uri = "market/all?isDetails=false";

        return $this->send(self::GET, $uri);
    }

    /**
     * 분 Candle 정보 조회
     *
     * @param int $unit 분 단위 1 3 5 15 10 30 60 240 분 캔들 조회
     * @param string $market 마켓 코드
     * @param string|null $to 마지막 캔들 시각 포맷 (yyyy-MM-dd'T'HH:mm:ss'Z') 또는 (yyyy-MM-dd HH:mm:ss), 빈 값 요청시 가장 최근 캔들
     * @param int|null $count 캔들 갯수(최대 200개)
     * @return array
     */
    public function getCandlesMinutes(int $unit, string $market, ?string $to = null, ?int $count = null): array
    {
        $uri = "candles/minutes/{$unit}?market={$market}";
        $uri .= $to ? "&to={$to}" : "";
        $uri .= $count ? "&count={$count}" : "";
        return $this->send(self::GET, $uri);
    }

    /**
     * 일 Candle 정보 조회
     *
     * @param string $market 마켓 코드
     * @param integer|null $count 캔들 갯수(최대 200개)
     * @param string|null $to 마지막 캔들 시각 포맷 (yyyy-MM-dd'T'HH:mm:ss'Z') 또는 (yyyy-MM-dd HH:mm:ss), 빈 값 요청시 가장 최근 캔들
     * @param string|null $convertingPriceUnit 종가 환산 화폐 단위 (생략 가능 KRW로 명시할 시 원화 환산 가격을 반환)
     * @return array
     */
    public function getCandlesDays(string $market, ?int $count = null, ?string $to = null, ?string $convertingPriceUnit): array
    {
        $uri = "candles/days?market={$market}";
        $uri .= $count ? "&count={$count}" : "";
        $uri .= $to ? "&to={$to}" : "";
        $uri .= $convertingPriceUnit ? "&convertingPriceUnit={$convertingPriceUnit}" : "";

        return $this->send(self::GET, $uri);
    }

    /**
     * 주 Candle 정보 조회
     *
     * @param string $market 마켓 코드
     * @param integer|null $count 캔들 갯수(최대 200개)
     * @param string|null $to 마지막 캔들 시각 포맷 (yyyy-MM-dd'T'HH:mm:ss'Z') 또는 (yyyy-MM-dd HH:mm:ss), 빈 값 요청시 가장 최근 캔들
     * @return array
     */
    public function getCandlesWeeks(string $market, ?int $count = null, ?string $to = null): array
    {
        $uri = "candles/weeks?market={$market}";
        $uri .= $count ? "&count={$count}" : "";
        $uri .= $to ? "&to={$to}" : "";

        return $this->send(self::GET, $uri);
    }

    /**
     * 월 Candle 정보 조회
     *
     * @param string $market 마켓 코드
     * @param integer|null $count 캔들 갯수(최대 200개)
     * @param string|null $to 마지막 캔들 시각 포맷 (yyyy-MM-dd'T'HH:mm:ss'Z') 또는 (yyyy-MM-dd HH:mm:ss), 빈 값 요청시 가장 최근 캔들
     * @return array
     */
    public function getCandlesMonths(string $market, ?int $count = null, ?string $to = null): array
    {
        $uri = "candles/months?market={$market}";
        $uri .= $count ? "&count={$count}" : "";
        $uri .= $to ? "&to={$to}" : "";

        return $this->send(self::GET, $uri);
    }

    /**
     * 계좌정보 조회
     *
     * @return array
     */
    public function getAccounts(): array
    {
        $uri = "accounts";
        return $this->send(self::GET, $uri);
    }

    /**
     * 주문
     *
     * @param string $market
     * @param string $side
     * @param string $ord_type
     * @param float|null $volume
     * @param float|null $price
     * @param string|null $identifier
     * @return array
     */
    public function order(string $market, string $side, string $ord_type, ?float $volume = null, ?float $price = null, ?string $identifier = null): array
    {
        $uri = "orders";

        // 검증
        if ($side == self::SIDE_BID && $ord_type == self::ORD_TYPE_MARKET) {
            throw new Exception("매수 주문 인 경우 ord_type 이 market이 될 수 없습니다.");
        } else if ($side == self::SIDE_ASK && $ord_type == self::ORD_TYPE_PRICE) {
            throw new Exception("매도 주문 인 경우 ord_type 이 price가 될 수 없습니다.");
        }

        if ($side == self::SIDE_BID && $ord_type == self::ORD_TYPE_PRICE && (empty($price) || $price == 0)) {
            throw new Exception("시장가 매수 주문 인 경우 price 의 값이 필요로 합니다.");
        } else if ($side == self::SIDE_ASK && $ord_type == self::ORD_TYPE_MARKET && (empty($volume) || $volume == 0)) {
            throw new Exception("시장가 매도 주문 인 경우 volume 의 값이 필요로 합니다.");
        }


        if ($identifier == null) {
            $identifier = "{$market}_{$side}_{$ord_type}_" . date("Ymdhis");
        }

        $queryParam = [
            "market" => $market,
            "side" => $side,
            "volume" => $volume ?? null,
            "price" => $price ?? null,
            "ord_type" => $ord_type,
            "identifier" => $identifier,
        ];
        return $this->send(self::POST, $uri, $queryParam);
    }

    /**
     * 주문 취소
     *
     * @param string|null $uuid
     * @param string|null $identifier
     * @return array
     */
    public function orderCancel(?string $uuid = null, ?string $identifier = null): array
    {
        $uri = "order";

        // 검증
        if ($uuid == null && $identifier == null) {
            throw new Exception("uuid 또는 identifier 둘 중에 하나는 필요로 합니다.");
        }

        $queryParam = [
            "uuid" => $uuid,
            "identifier" => $identifier,
        ];

        return $this->send(self::DELETE, $uri, $queryParam);
    }

    /**
     * 주문 가능 정보
     *
     * @param string $market
     * @return array
     */
    public function orderChance(string $market): array
    {
        $uri = "orders/chance";
        $queryParam = [
            "market" => $market,
        ];

        return $this->send(self::GET, $uri, $queryParam);
    }

    /**
     * 주문 단건 조회
     *
     * @param string|null $uuid
     * @param string|null $identifier
     * @return array
     */
    public function orderInfo(?string $uuid = null, ?string $identifier = null): array
    {
        $uri = "order";

        // 검증
        if ($uuid == null && $identifier == null) {
            throw new Exception("uuid 또는 identifier 둘 중에 하나는 필요로 합니다.");
        }

        $queryParam = [
            "uuid" => $uuid,
            "identifier" => $identifier,
        ];

        return $this->send(self::GET, $uri, $queryParam);
    }


    /**
     * 주문 내역 조회
     *
     * @param string|null $market
     * @param array|null $uuids
     * @param array|null $identifiers
     * @param string|null $state
     * @param array|null $states
     * @param integer|null $page
     * @param integer|null $limit
     * @param string|null $order_by
     * @return array
     */
    public function orderList(?string $market = null, ?array $uuids = null, ?array $identifiers = null, ?string $state = null, ?array $states = null, ?int $page = null, ?int $limit = null, ?string $order_by = null): array
    {
        $uri = "orders";

        $queryParam = [
            "market" => $market,
            "uuids" => $uuids,
            "identifiers" => $identifiers,
            "state" => $state,
            "states" => $states,
            "page" => $page,
            "limit" => $limit,
            "order_by" => $order_by,
        ];

        // 값이 없으면 키를 제거해준다.
        $queryParam = array_filter($queryParam);

        print_r($queryParam);

        if (empty($queryParam)) {
            $queryParam = null;
        }

        return $this->send(self::GET, $uri, $queryParam);
    }

    /**
     * UPBIT 토큰 만드는 함수
     *
     * @param array|null $queryParam
     * @return string
     */
    private function makeToken(?array $queryParam = null): string
    {

        $addPayload = [];
        if ($queryParam !== null) {

            $queryString = http_build_query($queryParam);

            $query_hash =  hash('sha512', $queryString, false);

            $addPayload = [
                "query_hash" => $query_hash,
                "query_hash_alg" => "SHA512",
            ];
        }

        $payload = [
            "access_key" => $this->accessKey,
            "nonce" => "SSR_" . uniqid(),
        ];

        $mergePayload = array_merge($payload, $addPayload);

        $token = JWT::encode($mergePayload, $this->secretKey);

        return $token;
    }


    /**
     * UPBIT API 서버와 통신
     *
     * @param string $method
     * @param string $uri
     * @param array|null $param
     * @return array
     */
    private function send(string $method = self::GET, string $uri, ?array $param = null): array
    {
        $token = $this->makeToken($param);

        $form_params = array();
        if ($param !== null) {
            $form_params = array('form_params' => $param);
        }

        // basic option
        $options = array(
            'headers' => [
                'Accept' => 'application/json',
                'Authorization' => "Bearer " . $token,
            ],
        );

        if (!empty($form_params)) {
            $options = array_merge($options, $form_params);
        }

        $response = $this->client->request($method, $uri, $options);

        if ($response->getStatusCode() != self::HTTP_CODE_200) {
            throw new Exception("Resopnse Code is not 200", 0);
        }

        return json_decode($response->getBody(), true);
    }
}

 

부족한 실력으로 했기 때문에 더 좋은, 깔끔한 코드로 변경하셔서 사용하시면 될 듯합니다.

728x90
Comments