自己紹介
40代のフリーランスで主にiOSアプリのコードを書いています。
今年からAndroidも興味を持ち、現在学習中です。
背景
最近、ニュースでも暗号化通貨とかブロックチェーンなどよく耳にしてますがどんなものなのか知りたくなったのがきっかけです。
そのものの技術も気になりますが、とりあえずbitFlyerで取引情報をAPIで公開しているのでswiftで書いてみました。
何年前にFXアプリを作った経験からすると取引の仕組みは似ている感じです。
API情報
bitFlyerから提供しているAPIは大きくHTTP Public API
とHTTP Private API
があります。
その中、HTTP Public API
のサンプルを作ってみました。
当たり前の話ですが、HTTP Public APIはキーによる認証が不要なので簡単に使えます。
しかし、回数の制限があるのでトキュメントを確認してみてください。
やってみましょう
データはHTTP ResufulAPI、リアルタイムAPIにより取得できます。
サンプルは両方書いています。
HTTP ResufulAPI
ネットワーク通信はAlamofire
を利用しています。
普段のRequestを発行する仕組みと変わらないですね。
import Foundation
import Alamofire
/*
API制限
HTTP API は、以下のとおり呼出回数を制限いたします。
Private API は 1 分間に約 200 回を上限とします。
IP アドレスごとに 1 分間に約 500 回を上限とします。
注文数量が 0.01 以下の注文を大量に発注するユーザーは、一時的に、発注できる注文数が 1 分間に約 10 回までに制限されることがあります。
システムに負荷をかける目的での発注を繰り返していると当社が判断した場合は、API の使用が制限されることがあります。ご了承ください。
*/
final class BFCoinAPI {
// ホスト名
private static let Host = "https://api.bitflyer.jp/v1"
// 共通ヘッダー
static let CommonHeaders:HTTPHeaders = [
"Authorization": "",
"Version": Bundle.main.infoDictionary!["CFBundleShortVersionString"]! as! String,
"Accept": "application/json"
]
//リクエスト処理の生成
private class func createRequest(url:String, parameters: Parameters? = nil) -> Alamofire.DataRequest {
return Alamofire.request("\(Host)\(url)",
method:.get,
parameters: parameters,
encoding: JSONEncoding.default,
headers: BFCoinAPI.CommonHeaders).validate()
}
//マーケットの一覧
static func requestMarkets() -> Void {
self.createRequest(url: "/markets", parameters: nil).responseJSON { response in
if let JSON = response.result.value {
print("Success with response")
print(JSON)
}else{
print("Error with response")
}
}
}
//板情報
static func requestBoard(_ productCode: String?) -> Void {
let parameters = (productCode == nil) ? nil : ["product_code":productCode as Any]
self.createRequest(url: "/board", parameters: parameters).responseJSON { response in
if let JSON = response.result.value {
print("Success with response")
print(JSON)
}else{
print("Error with response")
}
}
}
//Ticker
static func requestTicker(_ productCode: String?) -> Void {
let parameters = (productCode == nil) ? nil : ["product_code":productCode as Any]
self.createRequest(url: "/ticker", parameters: parameters).responseJSON { response in
if let JSON = response.result.value {
print("Success with response")
print(JSON)
}else{
print("Error with response")
}
}
}
...
リアルタイムAPI
リアルタイムデータ取得のためにPubNub
のサービスを使っていますが、個人的にもこのサービス初めてなのでチュートリアルを読みながらコードを書きました。
ほんとはリアルタイムのみ処理するマネージャークラスを作りたかったですが、AppDelegate
にしないとデータが受信できませんでした。(理由不明、だれか教えてください。)
結局、AppDelegate
, AppDeletate+RealtimeAPI
の2つのクラスに分けてコードを書いています。
AppDelegate.swift
import UIKit
import PubNub
@UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate {
var window: UIWindow?
var client: PubNub! //realtime api
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool {
// Override point for customization after application launch.
//MARK: Setup realtime Client
self.client = BFLCoinManager.sharedManager.realtimeClient
self.client.addListener(self)
return true
}
AppDeletate+RealtimeAPI.swift
import UIKit
import PubNub
extension AppDelegate : PNObjectEventListener {
// Handle new message from one of channels on which client has been subscribed.
func client(_ client: PubNub, didReceiveMessage message: PNMessageResult) {
// Handle new message stored in message.data.message
if message.data.channel != message.data.subscription {
// Message has been received on channel group stored in message.data.subscription.
}
else {
// Message has been received on channel stored in message.data.channel.
}
guard let dataMessage = message.data.message else {
print("Received no message data.")
return;
}
print("Received message: \(dataMessage) on channel \(message.data.channel) " + "at \(message.data.timetoken)")
//更新処理
BFLCoinManager.sharedManager.updateRealtime(message)
}
// New presence event handling.
func client(_ client: PubNub, didReceivePresenceEvent event: PNPresenceEventResult) {
// Handle presence event event.data.presenceEvent (one of: join, leave, timeout, state-change).
if event.data.channel != event.data.subscription {
// Presence event has been received on channel group stored in event.data.subscription.
}
else {
// Presence event has been received on channel stored in event.data.channel.
}
guard let uuid = event.data.presence.uuid else {
print("Received no uuid data.")
return;
}
guard let state = event.data.presence.state else {
print("Received no uuid state.")
return;
}
if event.data.presenceEvent != "state-change" {
print("\(uuid) \"\(event.data.presenceEvent)'ed\"\n" +
"at: \(event.data.presence.timetoken) on \(event.data.channel) " +
"(Occupancy: \(event.data.presence.occupancy))");
}
else {
print("\(uuid) changed state at: " +
"\(event.data.presence.timetoken) on \(event.data.channel) to:\n" +
"\(state)");
}
}
// Handle subscription status change.
func client(_ client: PubNub, didReceive status: PNStatus) {
if status.operation == .subscribeOperation {
// Check whether received information about successful subscription or restore.
if status.category == .PNConnectedCategory || status.category == .PNReconnectedCategory {
let subscribeStatus: PNSubscribeStatus = status as! PNSubscribeStatus
if subscribeStatus.category == .PNConnectedCategory {
// This is expected for a subscribe, this means there is no error or issue whatsoever.
// Select last object from list of channels and send message to it.
let targetChannel = client.channels().last!
client.publish("Hello from the PubNub Swift SDK", toChannel: targetChannel,
compressed: false, withCompletion: { (publishStatus) -> Void in
if !publishStatus.isError {
// Message successfully published to specified channel.
}
else {
/**
Handle message publish error. Check 'category' property to find out
possible reason because of which request did fail.
Review 'errorData' property (which has PNErrorData data type) of status
object to get additional information about issue.
Request can be resent using: publishStatus.retry()
*/
}
})
}
else {
/**
This usually occurs if subscribe temporarily fails but reconnects. This means there was
an error but there is no longer any issue.
*/
}
}
else if status.category == .PNUnexpectedDisconnectCategory {
/**
This is usually an issue with the internet connection, this is an error, handle
appropriately retry will be called automatically.
*/
}
// Looks like some kind of issues happened while client tried to subscribe or disconnected from
// network.
else {
let errorStatus: PNErrorStatus = status as! PNErrorStatus
if errorStatus.category == .PNAccessDeniedCategory {
/**
This means that PAM does allow this client to subscribe to this channel and channel group
configuration. This is another explicit error.
*/
}
else {
/**
More errors can be directly specified by creating explicit cases for other error categories
of `PNStatusCategory` such as: `PNDecryptionErrorCategory`,
`PNMalformedFilterExpressionCategory`, `PNMalformedResponseCategory`, `PNTimeoutCategory`
or `PNNetworkIssuesCategory`
*/
}
}
}
}
}
BFCoinRealtimeAPI.swift
import Foundation
import PubNub
enum PrefixChannel : String {
case market = "lightning_board_snapshot_"
case board = "lightning_board_"
case ticker = "lightning_ticker_"
case executions = "lightning_executions_"
}
final class BFCoinRealtimeAPI : NSObject {
internal var client: PubNub!
init(_ client: PubNub) {
super.init()
self.client = client
}
static func setupClient() -> PubNub {
let configuration = PNConfiguration(publishKey: "BFCoinMgr", subscribeKey: "sub-c-52a9ab50-291b-11e5-baaa-0619f8945a4f")
configuration.stripMobilePayload = false
return PubNub.clientWithConfiguration(configuration)
}
//MARK: Channel
func registChannelsAll(_ productCode: String) {
self.registMarketChannel(productCode)
self.registBoardChannel(productCode)
self.registTickerChannel(productCode)
self.registExecutionsChannel(productCode)
}
func releaseChannelsAll() {
self.client.unsubscribeFromAll()
}
func registMarketChannel(_ productCode: String) {
//let subscribeKey = "\(PrefixChannel.market.rawValue)\(productCode)"
let subscribeKey = "lightning_ticker_BTC_JPY"
self.client.subscribeToChannels([subscribeKey], withPresence: true)
}
func releaseMarketChannel(_ productCode: String) {
let subscribeKey = "\(PrefixChannel.market.rawValue)\(productCode)"
self.client.unsubscribeFromChannels([subscribeKey], withPresence: true)
}
func registBoardChannel(_ productCode: String) {
let subscribeKey = "\(PrefixChannel.board.rawValue)\(productCode)"
self.client.subscribeToChannels([subscribeKey], withPresence: true)
}
func releaseBoardChannel(_ productCode: String) {
let subscribeKey = "\(PrefixChannel.board.rawValue)\(productCode)"
self.client.unsubscribeFromChannels([subscribeKey], withPresence: true)
}
func registTickerChannel(_ productCode: String) {
let subscribeKey = "\(PrefixChannel.ticker.rawValue)\(productCode)"
self.client.subscribeToChannels([subscribeKey], withPresence: true)
}
func releaseTickerChannel(_ productCode: String) {
let subscribeKey = "\(PrefixChannel.ticker.rawValue)\(productCode)"
self.client.unsubscribeFromChannels([subscribeKey], withPresence: true)
}
...
まとめ
まだ作成中でありますが、サンプルコードはこちらです。
https://github.com/dolfalf/BFLCoinManager
何時間で簡単にデータを受信するまでできたのですが、要はAPIドキュメント読みながらコードを書けば簡単にできるレベルですね。
時間がある時にUIの方も少しコードを書いてみたいですね。