中安拓也のブログ

プログラミングについて書くブログ

仮想通貨取引所のAPIを使ってみる #2 - 各種取引所のBTC価格を取得する

前回記事はこちら

国内仮想通貨取引所5種(bitFlyer、Coincheck、Zaif、bitbank、QUOINEX)のWeb APIにアクセスして、現在のビットコイン価格を表示するアプリケーションを作成しました。

バージョン情報

Angular + Reduxの環境を使います

  • Angular: 5.2.9

  • angular-redux/store: 7.1.1

  • redux: 3.7.2

  • angular/material: 5.2.4

  • Node: 8.1.4

実装

アプリケーション概要

angular-redux/storeを使ったAngular + Reduxのアーキテクチャを採用しました。アプリケーションの処理の大まかな流れですが、AppComponentから各種取引所APIにアクセスするServiceクラスを呼び出します。各種Serviceクラスは、APIのレスポンス結果を各種Actionクラスに渡し、ActionクラスからRecuderを中継してレスポンス結果(Reducerに整形される)がStoreに渡ります。そしてStoreに渡った整形されたAPIのレスポンス結果をAppComponentのViewテンプレート上で表示します。

f:id:l08084:20180331162342p:plain
本アプリケーションの構造

bitFlyer APIの呼び出し

仮想通貨取引所bitFlyerのAPIにアクセスして、現在のビットコイン価格を取得します。

Service

BitflyerServiceからbitFlyerのHTTP Public APIにアクセスして、Ticker情報を取得しています。

src/app/services/bitflyer.service.ts:

import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { Observable } from 'rxjs/Observable';
import { BitflyerTickerModel } from '../../state/bitflyer-ticker/bitflyer-ticker.model';
import { BitflyerTickerActions } from '../../state/bitflyer-ticker/bitflyer-ticker.action';

const URLS = {
  BASE: 'https://api.bitflyer.jp',
  TICKER: '/v1/getticker'
};

@Injectable()
export class BitflyerService {
  constructor(
    private http: HttpClient,
    private action: BitflyerTickerActions
  ) {}

  getTicker = (): void => {
    this.http
      .get(`${URLS.BASE}${URLS.TICKER}`)
      .map(response => response as BitflyerTickerModel)
      .subscribe(ticker => this.action.setTicker(ticker));
  };
}
Model

src/state/bitflyer-ticker/bitflyer-ticker.model.ts:

export interface BitflyerTickerModel {
  best_ask: number;
  best_ask_size: number;
  best_bid: number;
  best_bid_size: number;
  ltp: number;
  product_code: string;
  tick_id: number;
  timestamp: string;
  total_ask_depth: number;
  total_bid_depth: number;
  volume: number;
  volume_by_product: number;
}
Action

src/state/bitflyer-ticker/bitflyer-ticker.action.ts:

import { Injectable } from '@angular/core';
import { dispatch } from '@angular-redux/store';
import { FluxStandardAction } from 'flux-standard-action';
import { BitflyerTickerModel } from './bitflyer-ticker.model';

export type BitflyerTickerAction = FluxStandardAction<
  BitflyerTickerModel,
  void
>;

@Injectable()
export class BitflyerTickerActions {
  static BITFLYER_SET_TICKER = 'BITFLYER_SET_TICKER';

  @dispatch()
  setTicker = (ticker: BitflyerTickerModel): BitflyerTickerAction => ({
    type: BitflyerTickerActions.BITFLYER_SET_TICKER,
    payload: ticker,
    meta: undefined
  });
}
Reducer

src/state/bitflyer-ticker/bitflyer-ticker.reducer.ts:

import { Action } from 'redux';
import {
  BitflyerTickerActions,
  BitflyerTickerAction
} from '../bitflyer-ticker/bitflyer-ticker.action';
import { BitflyerTickerModel } from './bitflyer-ticker.model';

export function bitflyerTickerReducer(
  lastState: BitflyerTickerModel = null,
  action: Action
): BitflyerTickerModel {
  switch (action.type) {
    case BitflyerTickerActions.BITFLYER_SET_TICKER:
      return (action as BitflyerTickerAction).payload;
    default:
      return lastState;
  }
}

Coincheck APIの呼び出し

仮想通貨取引所CoincheckのAPIにアクセスして、現在のビットコイン価格を取得します。

bitFlyerのAPI呼び出しと、URLとModel(レスポンス結果のプロパティ)以外ほぼ同じなので、Serviceクラスのコードだけ載せます。(以下取引所についても同様)

Service

src/app/services/coincheck.service.ts:

import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { CoincheckTickerModel } from '../../state/coincheck-ticker/coincheck-ticker.model';
import { Observable } from 'rxjs/Observable';
import { CoincheckTickerActions } from '../../state/coincheck-ticker/coincheck-ticker.action';

const URLS = {
  BASE: 'https://coincheck.com',
  TICKER: '/api/ticker'
};

@Injectable()
export class CoincheckService {
  constructor(
    private http: HttpClient,
    private action: CoincheckTickerActions
  ) {}

  getTicker = (): void => {
    this.http
      .get(`${URLS.BASE}${URLS.TICKER}`)
      .map(response => response as CoincheckTickerModel)
      .subscribe(ticker => this.action.setTicker(ticker));
  };
}

Zaif APIの呼び出し

仮想通貨取引所ZaifのAPIにアクセスして、現在のビットコイン価格を取得します。

Model、Action、Reducerは省略

Service

src/app/services/zaif.service.ts:

import { ZaifTickerActions } from '../../state/zaif-ticker/zaif-ticker.action';
import { HttpClient } from '@angular/common/http';
import { Injectable } from '@angular/core';
import { ZaifTickerModel } from '../../state/zaif-ticker/zaif-ticker.model';

const URLS = {
  BASE: 'https://api.zaif.jp/api/1',
  TICKER: '/ticker/btc_jpy'
};

@Injectable()
export class ZaifService {
  constructor(private http: HttpClient, private action: ZaifTickerActions) {}

  getTicker = (): void => {
    this.http
      .get(`${URLS.BASE}${URLS.TICKER}`)
      .map(response => response as ZaifTickerModel)
      .subscribe(ticker => this.action.setTicker(ticker));
  };
}

bitbank APIの呼び出し

仮想通貨取引所bitbankのAPIにアクセスして、現在のビットコイン価格を取得します。

Model、Action、Reducerは省略

Service

src/app/services/bitbank.service.ts:

import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { BitbankTickerResponse } from '../../state/bitbank-ticker/bitbank-ticker.model';
import { BitbankTickerActions } from '../../state/bitbank-ticker/bitbank-ticker.action';

const URLS = {
  BASE: 'https://public.bitbank.cc',
  TICKER: '/btc_jpy/ticker'
};

@Injectable()
export class BitbankService {
  constructor(private http: HttpClient, private action: BitbankTickerActions) {}

  getTicker = (): void => {
    this.http
      .get(`${URLS.BASE}${URLS.TICKER}`)
      .map(response => response as BitbankTickerResponse)
      .subscribe(response => this.action.setTicker(response.data));
  };
}

QUOINEX APIの呼び出し

仮想通貨取引所QUOINEXのAPIにアクセスして、現在のビットコイン価格を取得します。

Model、Action、Reducerは省略

Service

src/app/services/quoinex.service.ts:

import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { QuoinexTickerActions } from '../../state/quoinex-ticker/quoinex-ticker.action';
import { QuoinexTickerModel } from '../../state/quoinex-ticker/quoinex-ticker.model';

const URLS = {
  BASE: 'https://api.quoine.com',
  TICKER: '/products/5'
};

@Injectable()
export class QuoinexService {
  constructor(private http: HttpClient, private action: QuoinexTickerActions) {}

  getTicker = (): void => {
    this.http
      .get(`${URLS.BASE}${URLS.TICKER}`)
      .map(response => response as QuoinexTickerModel)
      .subscribe(ticker => this.action.setTicker(ticker));
  };
}

ExchangeListについて

各種仮想通貨取引所から取得したレスポンス結果を整形して、画面に表示するのに適した形にする役割を持ったクラス・ファンクション群になります。Serviceクラス、Modelクラス、Actionクラス、Reducerがあります。

Service

各種取引所のレスポンス結果から、ビットコイン価格のみを抽出し、それぞれに各取引所の名称をマッピングしています。また、取引所によって、ビットコイン価格がstring型だったりnumber型だったりするので、number型に統一しています。

src/app/services/exchange-list.service.ts:

import { Injectable } from '@angular/core';
import {
  ExchangeModel,
  EXCHANGE_NAME_LIST
} from '../../state/exchange-list/exchange-list.model';
import { ExchangeListActions } from '../../state/exchange-list/exchange-list.action';

@Injectable()
export class ExchangeListService {
  constructor(private action: ExchangeListActions) {}

  createList = (
    bitflyerLtp: number,
    coincheckLast: number,
    zaifLast: number,
    bitbankLast: string,
    quoinexLast: string
  ): void => {
    const exchangeList: ExchangeModel[] = [];
    exchangeList.push(
      { name: EXCHANGE_NAME_LIST[0], btcPrice: bitflyerLtp },
      { name: EXCHANGE_NAME_LIST[1], btcPrice: coincheckLast },
      { name: EXCHANGE_NAME_LIST[2], btcPrice: zaifLast },
      // converting a string to a number
      { name: EXCHANGE_NAME_LIST[3], btcPrice: +bitbankLast },
      { name: EXCHANGE_NAME_LIST[4], btcPrice: +quoinexLast }
    );
    this.action.setExcahgeList(exchangeList);
  };
}

Model、Action、Reducerは省略

rootReducer

各種取引所のReducerを結合しています。

src/state/root/reducer.ts:

import { IAppState } from './store';
import { combineReducers } from 'redux';
import { bitflyerTickerReducer } from '../bitflyer-ticker/bitflyer-ticker.reducer';
import { coincheckTickerReducer } from '../coincheck-ticker/coincheck-ticker.reducer';
import { zaifTickerReducer } from '../zaif-ticker/zaif-ticker.reducer';
import { bitbankTickerReducer } from '../bitbank-ticker/bitbank-ticker.reducer';
import { quoinexTickerReducer } from '../quoinex-ticker/quoinex-ticker.reducer';
import { exchangeListReducer } from '../exchange-list/exchange-list.reducer';

export const rootReducer = combineReducers<IAppState>({
  bitflyerTicker: bitflyerTickerReducer,
  coincheckTicker: coincheckTickerReducer,
  zaifTicker: zaifTickerReducer,
  bitbankTicker: bitbankTickerReducer,
  quoinexTicker: quoinexTickerReducer,
  exchangeList: exchangeListReducer
});

Store

Storeのソースコードは、👇です。

src/state/root/store.ts:

import { BitflyerTickerModel } from '../bitflyer-ticker/bitflyer-ticker.model';
import { CoincheckTickerModel } from '../coincheck-ticker/coincheck-ticker.model';
import { ZaifTickerModel } from '../zaif-ticker/zaif-ticker.model';
import { BitbankTickerModel } from '../bitbank-ticker/bitbank-ticker.model';
import { QuoinexTickerModel } from '../quoinex-ticker/quoinex-ticker.model';
import { ExchangeModel } from '../exchange-list/exchange-list.model';

export interface IAppState {
  bitflyerTicker: BitflyerTickerModel;
  coincheckTicker: CoincheckTickerModel;
  zaifTicker: ZaifTickerModel;
  bitbankTicker: BitbankTickerModel;
  quoinexTicker: QuoinexTickerModel;
  exchangeList: ExchangeModel[];
}

export const INITIAL_STATE: IAppState = {
  bitflyerTicker: undefined,
  coincheckTicker: undefined,
  zaifTicker: undefined,
  bitbankTicker: undefined,
  quoinexTicker: undefined,
  exchangeList: undefined
};

Storeを図にすると、👇になります(字が小さくて読めないと思いますが...)

f:id:l08084:20180331233541p:plain
Storeの図

AppComponent

AppComponentでは、各種サービスクラスの呼び出し、angular-redux/store@selectデコレーターによる最新のStoreの値の取得、Viewテンプレートによる結果の表示などを行なっています。

src/app/app.component.ts:

import { Component, OnInit, OnDestroy } from '@angular/core';
import { HttpClient, HttpHeaders } from '@angular/common/http';
import { Observable } from 'rxjs/Observable';
import { BitflyerService } from './services/bitflyer.service';
import { CoincheckService } from './services/coincheck.service';
import { select } from '@angular-redux/store';
import { BitflyerTickerModel } from '../state/bitflyer-ticker/bitflyer-ticker.model';
import { CoincheckTickerModel } from '../state/coincheck-ticker/coincheck-ticker.model';
import { ZaifService } from './services/zaif.service';
import { BitbankService } from './services/bitbank.service';
import { QuoinexService } from './services/quoinex.service';
import { ExchangeListService } from './services/exchange-list.service';
import { Subscription } from 'rxjs/Subscription';
import { ExchangeModel } from '../state/exchange-list/exchange-list.model';

@Component({
  selector: 'app-root',
  templateUrl: './app.component.html',
  styleUrls: ['./app.component.scss']
})
export class AppComponent implements OnInit, OnDestroy {
  displayedColumns = ['name', 'btcPrice'];
  dataSource: ExchangeModel[];
  subscription: Subscription;

  @select(['bitflyerTicker', 'ltp'])
  readonly bitflyerLtp$: Observable<number>;
  @select(['coincheckTicker', 'last'])
  readonly coincheckLast$: Observable<number>;
  @select(['zaifTicker', 'last'])
  readonly zaifLast$: Observable<number>;
  @select(['bitbankTicker', 'last'])
  readonly bitbankLast$: Observable<string>;
  @select(['quoinexTicker', 'last_traded_price'])
  readonly quoinexLast$: Observable<string>;
  @select('exchangeList') readonly exchangeList$: Observable<ExchangeModel[]>;

  constructor(
    private bitflyerService: BitflyerService,
    private coincheckService: CoincheckService,
    private exchangeListService: ExchangeListService,
    private zaifService: ZaifService,
    private bitbankService: BitbankService,
    private quoinexService: QuoinexService
  ) {}

  ngOnInit() {
    this.bitflyerService.getTicker();
    this.coincheckService.getTicker();
    this.zaifService.getTicker();
    this.bitbankService.getTicker();
    this.quoinexService.getTicker();

    this.subscription = Observable.combineLatest(
      this.bitflyerLtp$,
      this.coincheckLast$,
      this.zaifLast$,
      this.bitbankLast$,
      this.quoinexLast$,
      (bitflyerLtp, coincheckLast, zaifLast, bitbankLast, quoinexLast) => {
        if (
          bitflyerLtp &&
          coincheckLast &&
          zaifLast &&
          bitbankLast &&
          quoinexLast
        ) {
          this.exchangeListService.createList(
            bitflyerLtp,
            coincheckLast,
            zaifLast,
            bitbankLast,
            quoinexLast
          );
        }
      }
    ).subscribe();

    this.exchangeList$
      .filter(value => !!value)
      .filter(value => value.length === 5)
      .subscribe(value => (this.dataSource = value));
  }

  ngOnDestroy() {
    this.subscription.unsubscribe();
  }
}

Angular MaterialのTableを使って、データを表示しています。Angular Material Tableについて、こちらの記事を参照してください。また、numberパイプに'0.0-0'オプションを渡すことによって、小数点を丸めています。

src/app/app.component.html:

<div class="example-container mat-elevation-z8">
  <mat-table #table [dataSource]="dataSource">

    <!-- Exchange Name Column -->
    <ng-container matColumnDef="name">
      <mat-header-cell *matHeaderCellDef> 取引所 </mat-header-cell>
      <mat-cell *matCellDef="let element"> {{element.name}} </mat-cell>
    </ng-container>

    <!-- BTC Price Column -->
    <ng-container matColumnDef="btcPrice">
      <mat-header-cell *matHeaderCellDef> BTC価格 </mat-header-cell>
      <mat-cell *matCellDef="let element"> {{element.btcPrice | number: '0.0-0'}}円 </mat-cell>
    </ng-container>

    <mat-header-row *matHeaderRowDef="displayedColumns"></mat-header-row>
    <mat-row *matRowDef="let row; columns: displayedColumns;"></mat-row>
  </mat-table>
</div>

動作確認

ソースコードを動作すると、テーブル上に取引所の名前と現在のビットコインの価格が表示されます。もう少し改良すれば、アービトラージとかに使えるかも.....?

f:id:l08084:20180330120230p:plain
動作確認イメージ

なお、動作させる前に、ブラウザのCORSに対するセキュリティを無効にする必要があります。CORSの無効化についての説明は、こちらの記事をどうぞ。


次回の記事はこちら