CCXT-04-API

1. API方法与访问端结点

每个交易所对象都提供了一组API方法。API的每个方法被称为一个访问端结点,它指的是用于查询各种信息的HTTP URL。所有的访问端结点都返回JSON响应。 通常有一个访问端结点用于获取交易所的市场列表,一个访问端结点用于提取特定市场的交易委托账本,一个访问端结点用于提取交易历史,一组访问点用于下单或取消委托单、充值或提现等等。 基本上你在交易所里可以进行的操作都会有一个API提供出来供你调用。 因为不同交易所的方法集彼此不同,ccxt库实现了以下功能:

  • 为所有可能的URL和方法提供公开和私有API
  • 提供一个统一的API支持各交易所的共同的方法

端结点URL在每个交易所的api属性中预定义。你不需要重载这个属性,除非你要实现一个新的交易所API(至少你需要了解你要做什么)。

2. 隐含的API方法

大多数交易所特定的API方法都是隐含的,意思是这些方法没有在代码中显式地定义。ccxt库采用声明式的方法来定义隐含的交易所API方法。 API的每个方法通常都有自己的访问端结点。ccxt库为每个交易所都定义了所有的访问端结点,你可以通过.api属性访问。在创建交易所对象时,在defineRestApi() / define_rest_api()中将会为.api列表中的每个url创建一个隐含的魔术方法(即偏函数或闭包)。这个环节在所有交易所上都是统一的。生成的每个方法都可以驼峰写法和下划线写法来访问。 访问点定义指的是一个交易所暴露出来的所有的API的URL的完整的列表。这个列表将在交易所对象初始化时转化为可调用的方法。在API访问端结点列表中的每个URL都有一个对应的可调用方法。对于所有的交易所而言,这都是自动进行的,因此ccxt库支持数字货币交易所的所有可能的URL。 每个隐含的方法都有一个唯一的名字,这个名字是利用.api中的定义生成的。 例如,对于一个私有HTTPS PUT访问端结点https://api.exchange.com/order/{id}/cancel, 其对应的隐含方法名为.privatePutOrderIdCancel() / .private_put_order_id_cancel()。 对于一个公开的HTTPS GET访问端结点https://api.exchange.com/market/ticker/{pair}, 其对应的隐含方法名为.publicGetTickerPair() / .public_get_ticker_pair(),依次类推。 隐含方法接收传入的参数字典,将请求发送到交易所,然后返回交易所特定的未解析的JSON结果。要传入一个参数,将其添加到字典中与参数同名的键下即可。例如对于上面的例子,就是像.privatePutOrderIdCancel ({ id: '41987a2b-...' }).publicGetTickerPair ({ pair: 'BTC/USD' })ccxt推荐的与交易所交互的方式,并不是使用交易所特定的隐含方法,而是使用ccxt 提供的统一方法。只有当ccxt的统一api中没有提供相应的方法时,才应当使用隐含的方法作为后备方案。 要获得指定交易所实例的所有可用方法,包括隐含方法和统一方法,你可以使用如下的简单代码。 JavaScript代码示例:

console.log (new ccxt.kraken ())

Python代码示例:

print(dir(ccxt.hitbtc()))

PHP代码示例:

var_dump (new \ccxt\okcoinusd ());

3. 公开API与私有API

API的URL通常分为两类:市场数据方面的公开API,以及交易和账户相关的私有API。 这两组API的方法通常分别使用前缀publicprivate

3.1 公开API

公开API用来访问市场数据,不需要进行身份验证。大多数交易所为所有用户提供开放的市场数据(通常有一定的限流措施)。使用ccxt库,任何人 都可以直接访问市场数据,而无需在交易所进行注册,也无需设置api key和密码。 公开API包含如下内容:

  • 交易对
  • 价格流
  • 委托账本(L1、L2、L3…)
  • 交易历史(完成的订单、交易、执行)
  • 行情数据(现价、24小时价格)
  • 用于图表的OHLCV序列数据
  • 其他公开访问点

3.2 私有API

要使用私有API进行交易,你需要从交易所获取API key。这通常意味着你需要在交易所注册并使用你的账户创建API key。大多数交易所需要个人信息或身份标识,一些身份验证也是必要的。 如果你希望交易,首先需要在交易所进行注册,ccxt库不会创建账户或者提供API key。有些交易所的API提供了在代码中注册账户的接口,但是大多数交易所都没有这样的接口。你必须在交易所的网站注册并创建API key。 私有API包含以下内容:

  • 管理个人账户信息
  • 查询账户余额
  • 委托市价单和限价单
  • 创建充值地址并进行账户充值
  • 请求提取法币和加密货币
  • 查询个人的敞口/完结委托单
  • 查询杠杆交易的位置
  • 获取账本历史
  • 在不同账户之间转账
  • 使用商户服务

有些交易所的相同服务采用了不同的名称。例如,公开API通常称为市场数据、 基础、市场、mapi、api、价格等等。所有这些指的都是一组用于访问公开可用数据的方法。私有API通常称为trading、trade、tapi、exchange、account等等。 有些交易所也暴露出商户API,可以让你创建发票并接收你的客户的数字货币和法币支付。 这一类API通常称为merchant、wallet、payment、ecapi(用于电子商务的API)。 要获取指定交易所实例的所有可用方法,你可以使用如下的简单代码:

console.log (new ccxt.kraken ())   // JavaScript
print (dir (ccxt.hitbtc ()))        # Python
var_dump (new \ccxt\okcoinusd ()); // PHP

4. 同步调用与异步调用

JavaScript版本的CCXT库中,所有的方法都是异步的,这些方法返回解析值为JSON对象的Promise。在CCXT中我们使用现代的async/await语法来操作Promise, 如果你不熟悉这种语法,可以参考MDN。 JavaScript示例代码:

(async () => {
    let pairs = await kraken.publicGetSymbolsDetails ()
    let marketIds = Object.keys (pairs['result'])
    let marketId = marketIds[0]
    let ticker = await kraken.publicGetTicker ({ pair: marketId })
    console.log (kraken.id, marketId, ticker)
}) ()

Python版本的ccxt库使用async/await语法支持Python3.5+的异步并发模式。 异步的Python版本使用aiohttp实现纯异步io。在异步模式下所有的属性和方法名还是一样的,只是大多数方法都有async关键字装饰。如果你希望使用异步模式,应当链接ccxt.async_support子包,如下例所示:

import asyncio
import ccxt.async_support as ccxt
async def print_poloniex_ethbtc_ticker():
    poloniex = ccxt.poloniex()
    print(await poloniex.fetch_ticker('ETH/BTC'))
asyncio.get_event_loop().run_until_complete(print_poloniex_ethbtc_ticker())

在PHP版本的ccxt库中,所有的API方法都是同步的。

5. 调用参数与返回值

所有的公开和私有API方法都返回交易所响应的原始的JSON对象,也就是说没有解析的原始响应结果。统一API返回公共格式的JSON对象,在所有交易所上都保持统一的结构。 ** 调用参数 所有可能的API访问端结点集合对于每个交易所都不一样。大多数方法接收单一的关联数组(或Python字典)表示的键-值参数。传参方法如下所示:

bitso.publicGetTicker ({ book: 'eth_mxn' })                 // JavaScript
ccxt.zaif().public_get_ticker_pair ({ 'pair': 'btc_jpy' })  # Python
$luno->public_get_ticker (array ('pair' => 'XBTIDR'));      // PHP

要查看每个交易的方法参数的完整清单,请参考交易所的API文档。

6. API方法命名规范

交易所方法名是由以下字符串拼接而成:

  • 类型:public或private
  • HTTP方法:GET、POST、PUT、DELETE
  • 访问端结点URL

示例如下:

方法名 API URL基地址 端结点URL
publicGetIdOrderbook https://bitbay.net/API/Public {id}/orderbook
publicGetPairs https://bitlish.com/api pairs
publicGetJsonMarketTicker https://www.bitmarket.net json/{market}/ticker
privateGetUserMargin https://bitmex.com user/margin
privatePostTrade https://btc-x.is/api trade
tapiCancelOrder https://yobit.net tapi/CancelOrder

ccxt库同时支持驼峰命名法(JavaScript常用)和下划线命名法(Python和PHP常用), 因此所有的方法在任何开发语言中都可以上述两种风格之一调用:

exchange.methodName ()  // 驼峰式伪代码
exchange.method_name () // 下划线式伪代码

要获取指定交易所实例的所有可用方法的完整列表,可以简单地调用如下代码:

console.log (new ccxt.kraken ())   // JavaScript
print (dir (ccxt.hitbtc ()))        # Python
var_dump (new \ccxt\okcoinusd ()); // PHP

7. 统一API

ccxt统一API是所有交易所中的公共方法的集合。目前统一API包含以下方法:

  • fetchMarkets(): 从交易所提取所有有效市场的清单,返回市场对象数组。有些交易所没有办法通过其在线API获取市场清单,CCXT采用硬编码的方式返回这些交易所的市场清单。
  • loadMarkets([reload]):返回对象形式的市场清单并在交易所实例上缓存,键为交易符号。如果之前已经载入过,则从缓存中返回结果,除非是强制使用了reload标志并设置为true
  • fetchOrderBook(symbol[, limit = undefined[, params = {}]]):获取指定市场交易符号的L2/L3委托账本
  • fetchStatus([, params = {}]):返回交易所状态信息,可能使用API或者硬编码实现
  • fetchL2OrderBook(symbol[, limit = undefined[, params]]):获取交易符号的2层(价格聚合)委托账本
  • fetchTrades(symbol[, since[, [limit, [params]]]]):获取指定交易符号的最近交易
  • fetchTicker(symbol):获取指定交易符号的最新行情数据
  • fetchBalance():获取余额数据
  • createOrder(symbol, type, side, amount[, price[, params]])
  • createLimitBuyOrder(symbol, amount, price[, params])
  • createLimitSellOrder(symbol, amount, price[, params])
  • createMarketBuyOrder(symbol, amount[, params])
  • createMarketSellOrder(symbol, amount[, params])
  • cancelOrder(id[, symbol[, params]])
  • fetchOrder(id[, symbol[, params]])
  • fetchOrders([symbol[, since[, limit[, params]]]])
  • fetchOpenOrders([symbol[, since, limit, params]]]])
  • fetchClosedOrders([symbol[, since[, limit[, params]]]])
  • fetchMyTrades([symbol[, since[, limit[, params]]]])

8. 改写统一API的参数

注意,统一API的大部分方法都可以接受一个可选的params参数,它是一个关联数组(字典,默认为空),包含了你希望改写的参数。params的内容是与特定交易所相关的,参考交易所的API文档了解其支持的字段和值。如果 你需要传入自定义设置或可选的参数,那么可以使用params字典。 JavaScript示例代码:

(async () => {
    const params = {
        'foo': 'bar',      // exchange-specific overrides in unified queries
        'Hello': 'World!', // see their docs for more details on parameter names
    }
    // the overrides go into the last argument to the unified call ↓ HERE
    const result = await exchange.fetchOrderBook (symbol, length, params)
}) ()

Python示例代码:

params = {
    'foo': 'bar',       # exchange-specific overrides in unified queries
    'Hello': 'World!',  # see their docs for more details on parameter names
}
# overrides go in the last argument to the unified call ↓ HERE
result = exchange.fetch_order_book(symbol, length, params)

PHP示例代码:

$params = array (
    'foo' => 'bar',       // exchange-specific overrides in unified queries
    'Hello' => 'World!',  // see their docs for more details on parameter names
}
// overrides go into the last argument to the unified call ↓ HERE
$result = $exchange->fetch_order_book ($symbol, $length, $params);

9. 统一API结果分页

大多数统一API的方法会返回单一对象或对象数组(交易、委托单等)。 然而,极少数交易所会一次返回全部委托单、全部交易或全部ohlcv烛线图数据。 更常见的是交易所API会限制返回一定数量的最新对象,你不能一次调用就获取从开始时间到当前时刻的全部对象。实际上,极少有交易所能容忍或允许这样的调用。 要获取历史委托单或交易,用户需要分页遍历数据。分页通常表示要循环提取部分数据。 在大多数情况下,用户需要使用某种类型的分页来获取期望的结果。如果用户没有使用分页,大多数方法将返回交易所的默认结果,这可能是从历史的开始时刻或者也可能是返回最近的一部分数据。默认行为是交易所相关的!分页通常会在以下方法中用到:

  • fetchTrades
  • fetchOHLCV
  • fetchOrders
  • fetchOpenOrders
  • fetchClosedOrders
  • fetchMyTrades
  • fetchTransactions
  • fetchDeposits
  • fetchWithdrawals

对于返回对象列表的方法,交易所可能提供一种或多种分页类型。CCXT默认统一了基于日期、基于毫秒时间戳的分页。 用于UTC日期和时间戳的方法集:

exchange.parse8601 ('2018-01-01T00:00:00Z') == 1514764800000 // integer, Z = UTC
exchange.iso8601 (1514764800000) == '2018-01-01T00:00:00Z'   // iso8601 string
exchange.seconds ()      // integer UTC timestamp in seconds
exchange.milliseconds () // integer UTC timestamp in milliseconds

9.1 基于日期的分页

这是目前CCXT统一API使用的分页类型。调用者提供一个毫秒时间戳作为since 参数的值,同时传入一个数值来限定返回结果的数量。要逐页遍历感兴趣的对象, 调用者运行如下的伪代码。 JavaScript:

if (exchange.has['fetchTrades']) {
    let since = exchange.milliseconds () - 86400000 // -1 day from now
    // alternatively, fetch from a certain starting datetime
    // let since = exchange.parse8601 ('2018-01-01T00:00:00Z')
    let allTrades = []
    while (since < exchange.milliseconds ()) {
        const symbol = undefined // change for your symbol
        const limit = 20 // change for your limit
        const trades = await exchange.fetchTrades (symbol, since, limit)
        if (trades.length) {
            since = trades[trades.length - 1]['timestamp']
            allTrades = allTrades.concat (trades)
        } else {
            break
        }
    }
}

Python:

if exchange.has['fetchOrders']:
    since = exchange.milliseconds () - 86400000  # -1 day from now
    # alternatively, fetch from a certain starting datetime
    # since = exchange.parse8601('2018-01-01T00:00:00Z')
    all_orders = []
    while since < exchange.milliseconds ():
        symbol = None  # change for your symbol
        limit = 20  # change for your limit
        orders = await exchange.fetch_orders(symbol, since, limit)
        if len(orders):
            since = orders[len(orders) - 1]['timestamp']
            all_orders += orders
        else:
            break

PHP:

if ($exchange->has['fetchMyTrades']) {
    $since = exchange->milliseconds () - 86400000; // -1 day from now
    // alternatively, fetch from a certain starting datetime
    // $since = $exchange->parse8601 ('2018-01-01T00:00:00Z');
    $all_trades = array ();
    while (since < exchange->milliseconds ()) {
        $symbol = null; // change for your symbol
        $limit = 20; // change for your limit
        $trades = $exchange->fetchMyTrades ($symbol, $since, $limit);
        if (count($trades)) {
            $since = $trades[count($trades) - 1]['timestamp'];
            $all_trades = array_merge ($all_trades, $trades);
        } else {
            break;
        }
    }
}

9.2 基于ID的分页

调用者提供对象的from_id,以及一个用于限定返回结果数量的值。这是一些交易所的默认行为,然而,这一分页类型目前还不是统一的。要基于ID进行分页,调用者可以运行如下伪代码: JavaScript:

if (exchange.has['fetchTrades']) {
    let from_id = 'abc123' // all ids are strings
    let allTrades = []
    while (true) {
        const symbol = undefined // change for your symbol
        const since = undefined
        const limit = 20 // change for your limit
        const params = {
            'from_id': from_id, // exchange-specific non-unified parameter name
        }
        const trades = await exchange.fetchTrades (symbol, since, limit, params)
        if (trades.length) {
            from_id = trades[trades.length - 1]['id']
            allTrades.push (trades)
        } else {
            break
        }
    }
}

Python:

if exchange.has['fetchOrders']:
    from_id = 'abc123'  # all ids are strings
    all_orders = []
    while True:
        symbol = None  # change for your symbol
        since = None
        limit = 20  # change for your limit
        params = {
            'from_id': from_id,  # exchange-specific non-unified parameter name
        }
        orders = await exchange.fetch_orders(symbol, since, limit, params)
        if len(orders):
            from_id = orders[len(orders) - 1]['id']
            all_orders += orders
        else:
            break

PHP:

if ($exchange->has['fetchMyTrades']) {
    $from_id = 'abc123' // all ids are strings
    $all_trades = array ();
    while (true) {
        $symbol = null; // change for your symbol
        $since = null;
        $limit = 20; // change for your limit
        $params = array (
            'from_id' => $from_id, // exchange-specific non-unified parameter name
        );
        $trades = $exchange->fetchMyTrades ($symbol, $since, $limit, $params);
        if (count($trades)) {
            $from_id = $trades[count($trades) - 1]['id'];
            $all_trades = array_merge ($all_trades, $trades);
        } else {
            break;
        }
    }
}

9.3 基于页号的分页

调用者提供一个页号或者初始的游标值。交易所范围一页结果以及后续的游标值以便继续。 大多数实现这种类型分页的交易所,在响应内容或响应头中返回下一个游标。 可以访问这里 查看示例代码实现。 在每个迭代周期,调用者必须拿到下一个游标并将其传入下一次迭代的查询。 JavaScript:

if (exchange.has['fetchTrades']) {
    let page = 0  // exchange-specific type and value
    let allTrades = []
    while (true) {
        const symbol = undefined // change for your symbol
        const since = undefined
        const limit = 20 // change for your limit
        const params = {
            'page': page, // exchange-specific non-unified parameter name
        }
        const trades = await exchange.fetchTrades (symbol, since, limit, params)
        if (trades.length) {
            // not thread-safu and exchange-specific !
            page = exchange.last_json_response['cursor']
            allTrades.push (trades)
        } else {
            break
        }
    }
}

Python:

if exchange.has['fetchOrders']:
    cursor = 0  # exchange-specific type and value
    all_orders = []
    while True:
        symbol = None  # change for your symbol
        since = None
        limit = 20  # change for your limit
        params = {
            'cursor': cursor,  # exchange-specific non-unified parameter name
        }
        orders = await exchange.fetch_orders(symbol, since, limit, params)
        if len(orders):
            # not thread-safu and exchange-specific !
            cursor = exchange.last_http_headers['CB-AFTER']
            all_orders += orders
        else:
            break

PHP:

if ($exchange->has['fetchMyTrades']) {
    $start = '0' // exchange-specific type and value
    $all_trades = array ();
    while (true) {
        $symbol = null; // change for your symbol
        $since = null;
        $limit = 20; // change for your limit
        $params = array (
            'start' => $start, // exchange-specific non-unified parameter name
        );
        $trades = $exchange->fetchMyTrades ($symbol, $since, $limit, $params);
        if (count($trades)) {
            // not thread-safu and exchange-specific !
            $start = $exchange->last_json_response['next'];
            $all_trades = array_merge ($all_trades, $trades);
        } else {
            break;
        }
    }
}