CCXT-03-市场模型

1. 市场数据结构

交易所是用来交易有价物品的场所。优势它们被冠以各种不同的 术语,例如工具、符号、交易对、货币、股票、商品、合同等, 但是指的都是一个东西 - 交易对、符号或金融工具。 在ccxt库中,每个交易所都提供了多个市场。不同交易所提供的 交易市场各有不同,因而也提供了跨交易所的套利机会。一个市场 通常指的是一对可交易的数字货币或法币。 在CCXT中,市场模型的数据结构如下:

{
    'id':     ' btcusd',  // string literal for referencing within an exchange
    'symbol':  'BTC/USD', // uppercase string literal of a pair of currencies
    'base':    'BTC',     // uppercase string, unified base currency code, 3 or more letters
    'quote':   'USD',     // uppercase string, unified quote currency code, 3 or more letters
    'baseId':  'btc',     // any string, exchange-specific base currency id
    'quoteId': 'usd',     // any string, exchange-specific quote currency id
    'active': true,       // boolean, market status
    'precision': {        // number of decimal digits "after the dot"
        'price': 8,       // integer or float for TICK_SIZE roundingMode, might be missing if not supplied by the exchange
        'amount': 8,      // integer, might be missing if not supplied by the exchange
        'cost': 8,        // integer, very few exchanges actually have it
    },
    'limits': {           // value limits when placing orders on this market
        'amount': {
            'min': 0.01,  // order amount should be > min
            'max': 1000,  // order amount should be < max
        },
        'price': { ... }, // same min/max limits for the price of the order
        'cost':  { ... }, // same limits for order cost = price * amount
    },
    'info':      { ... }, // the original unparsed market info from the exchange
}

每个市场都是一个关联数组(或称为字典),包含以下键:

  • id:市场ID,用来在交易所内区分不同市场的字符串或数字ID。
  • symbol:市场符号,用来表示交易对的大写的字符串代码。通常记作:基础货币/报价货币,例如: BTC/USD, LTC/CNY 或 ETH/EUR等等。在ccxt库中使用市场符号来引用不同的市场。
  • base:基础货币代码,表示基础法币或加密货币的统一的大写字符串代码,代码是标准化的,在 CCXT中以及CCXT统一API中,使用货币代码来引用不同的货币。
  • quote:报价货币代码,用来表示报价法币或数字货币的统一的大写字符串代码。
  • baseId:基础货币ID,是交易所特定的表示基础货币的ID,不是统一的代码,理论上可以是任何 字符串。
  • quoteId:报价货币ID,是交易所特定的表示报价货币的ID,也不是统一的代码,每个交易所自行维护。
  • active:是否激活,布尔值,表示这个市场是否可交易。通常如果一个市场未激活,那么所有相关的 行情、委托账本以及其他访问端结点都返回空响应、全零、无数据或过时数据。调用者需要检查市场 是否激活并且定期刷新市场缓存。
  • info:一个用于记录非共性市场属性的关联数组,包括手续费、费率、限制以及其他一般性市场信息。 内部的info数组对每个特定的市场都是不同的,其内容依赖于交易所。
  • precision:在委托单中金额相关字段(例如价格、数量、成本等)支持的最大小数位数。
  • limits:限值,价格、数量和成本等的最高和最低限值,其中:成本 = 价格 * 数量。

2. 数据精度和极限值

不要混淆了精度和限值!精度和最低限值无关。8位精度并不一定意味着最低限值为 0.00000001。反过来也是正确的:最小限值0.0001也不一定意味着精度为4。

示例1

(market['limits']['amount']['min'] == 0.05) && (market['precision']['amount'] == 4)

上面的代码要求任何委托单的数量必须同时满足以下条件:

  • 数量值应当 >= 0.05,例如:
+ good: 0.05, 0.051, 0.0501, 0.0502, ..., 0.0599, 0.06, 0.0601, ...
- bad: 0.04, 0.049, 0.0499
  • 精度最高4位小数,例如:
+ good: 0.05, 0.051, 0.052, ..., 0.0531, ..., 0.06, ... 0.0719, ...
- bad: 0.05001, 0.05000, 0.06001

** 示例2

(market['limits']['price']['min'] == 0.0019) && (market['precision']['price'] == 5)

这个例子中要求任何委托单的价格必须同时满足以下条件:

  • 价格应当 >= 0.019,例如:
+ good: 0.019, ... 0.0191, ... 0.01911, 0.01912, ...
- bad: 0.016, ..., 0.01699
  • 价格精度最高5位小数,例如:
+ good: 0.02, 0.021, 0.0212, 0.02123, 0.02124, 0.02125, ...
- bad: 0.017000, 0.017001, ...

** 示例3

(market['limits']['amount']['min'] == 50) && (market['precision']['amount'] == -1)

这个示例要求任何委托单的数量同时满足以下条件:

  • 数量应当 >= 50,例如:
+ good: 50, 60, 70, 80, 90, 100, ... 2000, ...
- bad: 1, 2, 3, ..., 9
  • 精度为负数表示应当为10的倍数,例如:
+ good: 50, ..., 110, ... 1230, ..., 1000000, ..., 1234560, ...
- bad: 9.5, ... 10.1, ..., 11, ... 200.71, ...

3. 委托单中的数值要求与格式化方法

ccxt的用户应当始终遵守精度和限值要求!委托单中的值应当满足以下条件:

  • 委托单amount > limits[‘min’][‘amount’]
  • 委托单amount < limits[‘max’][‘amount’]
  • 委托单price > limits[‘min’][‘price’]
  • 委托单price < limits[‘max’][‘price’]
  • 委托单cost (amount * price) > limits[‘min’][‘cost’]
  • 委托单cost (amount * price) < limits[‘max’][‘cost’]
  • amount的精度 <= precision[‘amount’]
  • price 的精度 <= precision[‘price’]

有些交易所的委托单可能不会包含上面提到的所有的值。

数值格式化方法 ** 每个交易所都有它自己的取整、计数和填充模式。

CCXT支持的取整模式有:

  • ROUND – 取整精度要求之后的小数位
  • TRUNCATE– 截断精度要求之后的小数位

数值精度计数模式可以使用exchange.precisionMode属性访问。

CCXT支持的计数模式包括:

  • DECIMAL_PLACES – 统计所有的数字,99%的交易所使用这种计数模式
  • SIGNIFICANT_DIGITS – 仅统计非零数字,有些交易所(bitfinex等)采用这种模式的计数
  • TICK_SIZE – 有些交易所只允许某个特定值的整数倍(bitmex使用这种模式)

CCXT支持的填充模式包括:

  • **NO_PADDING **– 无填充,大多数情况下的默认模式
  • **PAD_WITH_ZERO **– 使用0字符填充至精度要求

交易所基类包含了decimalToPrecision来帮助格式化数值为要求的精度, 它支持不同的取整、计数和填充模式。

JavaScript方法原型:

function decimalToPrecision (x, roundingMode, numPrecisionDigits, countingMode = DECIMAL_PLACES, paddingMode = NO_PADDING)

Python方法原型:

def decimal_to_precision(n, rounding_mode=ROUND, precision=None, counting_mode=DECIMAL_PLACES, padding_mode=NO_PADDING):

php方法原型:

function decimalToPrecision ($x, $roundingMode = ROUND, $numPrecisionDigits = null, $countingMode = DECIMAL_PLACES, $paddingMode = NO_PADDING)

可以访问以下示例代码查看如何使用decimalToPrecision方法来格式化字符串和浮点数:

4. 载入市场清单

大多数情况下,在可以访问其他API方法之前,你都需要先载入特定交易所的市场清单和交易符号。如果你忘记载入市场清单,ccxt库会在你第一次调用统一API前自动载入。ccxt会先后发送两个HTTP请求,第一个请求市场清单, 第二个请求其他数据。 要手工预先载入市场清单,可以调用交易所实例的loadMarkets ()load_markets () 方法,该方法返回一个描述市场集合的关联数组,键为交易符号。如果你希望 对业务逻辑有更多控制,那么推荐用这种方法手工载入市场清单。 JavaScript示例代码:

(async () => {
    let kraken = new ccxt.kraken ()
    let markets = await kraken.load_markets ()
    console.log (kraken.id, markets)
}) ()

Python示例代码:

okcoin = ccxt.okcoinusd ()
markets = okcoin.load_markets ()
print (okcoin.id, markets)

PHP示例代码:

$id = 'huobipro';
$exchange = '\\ccxt\\' . $id;
$huobipro = new $exchange ();
$markets = $huobipro->load_markets ();
var_dump ($huobipro->id, $markets);

5. 交易符号和市场ID

市场ID用于在REST请求-响应过程中引用交易所内的交易对。每个交易所都有不同的市场ID集,因此不可以跨交易所使用市场ID。例如,BTC/USD 交易对在不同的交易所中可能有不同的ID:btcusd、 BTCUSD、XBTUSD、btc/usd、 42 (数字ID)、 BTC/USD、 Btc/Usd、 tBTCUSD、 XXBTZUSD等。你不需要记住或使用市场ID,他们的作用是在交易所模型实现的内部用于HTTP的请求 -响应目的。 CCXT库将不通用的市场ID抽象为标准化的交易符号。交易符号不同于市场ID。 每个市场都采用一个对应的符号来引用,交易符号可以跨交易所使用,这使得交易符号更适用于跨交易所套利等其他很多应用。 交易符号通常是描述一对交易货币的大写字符串常量,以斜杠间隔两个货币代码。 货币代码是3~4位大写字母,例如 BTC, ETH, USD, GBP, CNY, LTC, JPY, DOGE, RUB, ZEC, XRP, XMR, 等等。有些交易所也有长一些的富有异国风情的货币名称。 在斜杠之前的货币被称为基础货币,之后的被称为报价货币。下面是一些符号的示例: BTC/USD, DOGE/LTC, ETH/EUR, DASH/XRP, BTC/CNY, ZEC/XMR, ETH/JPY。 有时用户可能会注意到像’XBTM18’ 或’.XRPUSDM20180101’ 或r “exotic/rare symbols” 之类的交易符号。交易符号并不是一定要有斜杠或者包含货币对的代码。符号字符串完全取决于市场类型(它是一个现货市场、期货市场、暗池市场或过期市场等等)。 CCXT不鼓励你解析交易符号字符串,你不应该依赖于交易符号的格式,CCXT推荐你使用市场属性来达成你的应用需求。 市场结构使用符号和ID为键。交易所基类也有内置的方法可以按符号访问市场对象。大多数API方法需要传入交易符号作为第一个参数。当查询当前价格或委托下单时,也常常需要你指定一个交易符号。 大多数时候,CCXT用户都是与市场交易符号打交道。如果你访问字典中不存在的 键,就会收获一个异常。 JavaScript示例代码:

(async () => {
    console.log (await exchange.loadMarkets ())
    let btcusd1 = exchange.markets['BTC/USD']     // get market structure by symbol
    let btcusd2 = exchange.market ('BTC/USD')     // same result in a slightly different way
    let btcusdId = exchange.marketId ('BTC/USD')  // get market id by symbol
    let symbols = exchange.symbols                // get an array of symbols
    let symbols2 = Object.keys (exchange.markets) // same as previous line
    console.log (exchange.id, symbols)            // print all symbols
    let currencies = exchange.currencies          // a list of currencies
    let bitfinex = new ccxt.bitfinex ()
    await bitfinex.loadMarkets ()
    bitfinex.markets['BTC/USD']                   // symbol → market (get market by symbol)
    bitfinex.markets_by_id['XRPBTC']              // id → market (get market by id)
    bitfinex.markets['BTC/USD']['id']             // symbol → id (get id by symbol)
    bitfinex.markets_by_id['XRPBTC']['symbol']    // id → symbol (get symbol by id)
})

Python示例代码:

print (exchange.load_markets ())
etheur1 = exchange.markets['ETH/EUR']      # get market structure by symbol
etheur2 = exchange.market ('ETH/EUR')      # same result in a slightly different way
etheurId = exchange.market_id ('BTC/USD')  # get market id by symbol
symbols = exchange.symbols                 # get a list of symbols
symbols2 = list (exchange.markets.keys ()) # same as previous line
print (exchange.id, symbols)               # print all symbols
currencies = exchange.currencies           # a list of currencies
kraken = ccxt.kraken ()
kraken.load_markets ()
kraken.markets['BTC/USD']                  # symbol → market (get market by symbol)
kraken.markets_by_id['XXRPZUSD']           # id → market (get market by id)
kraken.markets['BTC/USD']['id']            # symbol → id (get id by symbol)
kraken.markets_by_id['XXRPZUSD']['symbol'] # id → symbol (get symbol by id)

PHP示例代码:

$var_dump ($exchange->load_markets ());
$dashcny1 = $exchange->markets['DASH/CNY'];     // get market structure by symbol
$dashcny2 = $exchange->market ('DASH/CNY');     // same result in a slightly different way
$dashcnyId = $exchange->market_id ('DASH/CNY'); // get market id by symbol
$symbols = $exchange->symbols;                  // get an array of symbols
$symbols2 = array_keys ($exchange->markets);    // same as previous line
var_dump ($exchange->id, $symbols);             // print all symbols
$currencies = $exchange->currencies;            // a list of currencies
$okcoinusd = '\\ccxt\\okcoinusd';
$okcoinusd = new $okcoinusd ();
$okcoinusd->load_markets ();
$okcoinusd->markets['BTC/USD'];                 // symbol → market (get market by symbol)
$okcoinusd->markets_by_id['btc_usd'];           // id → market (get market by id)
$okcoinusd->markets['BTC/USD']['id'];           // symbol → id (get id by symbol)
$okcoinusd->markets_by_id['btc_usd']['symbol']; // id → symbol (get symbol by id)

6. 货币命名的一致性

不同的交易所在术语定义方面有一些模糊之处,对于新手交易者而言可能会产生歧义。有些交易所将市场称为交易对,而另一些交易所则将交易符号称为产品。对于CCXT开发库而言,每个交易所都包含一个或多个交易市场,每个交易市场有一个ID和一个符号,大多数符号都是由基础货币和报价货币对组成。

Exchanges → Markets → Symbols → Currencies

历史上对同一个交易对曾经使用过各种各样的符号名称。有些数字货币(例如Dash)甚至名字都改过不止一次。为了在多个交易所之间保持一致性,ccxt库使用以下已知的符号和货币的替代名称:

  • XBT → BTC:XBT比较新,但是BTC在交易所中更常见,而且听起来更像比特币
  • BCC → BCH:比特币现金分叉通常使用两个不同的名称:BCC和BCH。BCC有点 不明确,容易和BitConnect搞混。ccxt库会正确地将BCC换成BCH(有些交易所和聚合器会混淆这两个名字)。
  • DRK → DASH:DASH原来叫Darkcoin,然后改名为Dash
  • BCHABC → BCH:在2018年11月15日,比特币现金再次分叉,因此,现在有BCH (BCH ABC) 和BSV (BCH SV)。
  • BCHSV → BSV:这对应比台币现金的SV分叉,有些交易所称之为BSV,另一些交易所称之为BCHSV,ccxt使用前者。
  • DSH → DASH:The DSH (Dashcoin) 和DASH (Dash)不是一个东西。有些交易所不恰当地将DASH 标记为DSH,ccxt库对此进行了修正(DSH → DASH),但是只有一个交易所混淆了这两种货币, 绝大多数交易所都正确地区分了这两种货币。记住DASH/BTC和DSH/BTC不一样。
  • XRB → NANO:NANO是RaiBlocks的较新的代码,因此,CCXT统一API将在必要时 使用NANO替代较早的XRB。
  • USD → USDT:有些交易所,例如Bitfinex、HitBTC等在其列表中将其命名为USD,但是 那些市场实际上交易的是USDT。混淆来自于3个字母的限制或者是其他原因。在实际交易 的货币是USDT而非USD时,CCXT库会将USD替换为USDT。注意,有些交易所同时有 USD和USDT。例如,Kraken有一个USDT/USD交易对。

7. 货币命名冲突的解决流程

每个交易所都使用一个关联数组用于数字货币代码的替换,可以通过exchange.commonCurrencies 属性访问这个关联数组。有时用户可能会注意到混合大小写或者包含空格的奇怪的货币符号,之所以使用这些名称是为了解决不同交易所使用一样的符号表示不同的货币而引起的冲突: 首先,我们采集不同交易所关于有疑问的货币代码的所有可用信息。交易所通常有其上市 货币的描述清单,可能在API中,也可能在文档里、知识库里或网站的其他地方。 当我们识别出每个货币代码所表示的数字货币后,我们查看其在CoinMarketCap上的主页。 具有最大市值的货币可以保留自己的货币代码。例如,HOT通常表示Holo或Hydro Protocol。 这种情况下Holo得以继续持有其代码HOT,Hydro Protocol将以其名称作为代码,也就是Hydro Protocol。 因此,可能会有这样的交易对:HOT/USD (表示Holo) 和 Hydro Protocol/USD,这表示不同的市场。 如果一个货币的市值未知,或者不足以决定胜出者,我们也考虑交易量以及其他因素。 当决定了胜出的货币之后,所有其他竞争货币的代码都会重新进行映射,并使用exchange.commonCurrencies 来进行替换。 不幸的是这还是一个进展中的工作,因为每天都在上市新的货币,也是不是会出现 新的交易所。因此,总之这是一个在快速变化的环境中的没有尽头的自我纠错过程, 我们也感谢你能报告你发现的冲突和不匹配之处。

8. 市场缓冲强制重载

loadMarkets () / load_markets ()是一个有副作用的方法,它会在exchange示例上保存市场数组。对每个交易所实例你只需要调用一次。所有后续对此方法的调用都会返回本地保存的市场数组。 当载入交易市场后,你可以随时使用markets属性访问市场信息,这个属性包含了一个以符号为键的市场关联数组。如果你需要强制重载市场列表,只需要在调用时设置参数reloadtrue即可。 JavaScript示例代码:

(async () => {
    let kraken = new ccxt.kraken ({ verbose: true }) // log HTTP requests
    await kraken.load_markets () // request markets
    console.log (kraken.id, kraken.markets)    // output a full list of all loaded markets
    console.log (Object.keys (kraken.markets)) // output a short list of market symbols
    console.log (kraken.markets['BTC/USD'])    // output single market details
    await kraken.load_markets () // return a locally cached version, no reload
    let reloadedMarkets = await kraken.load_markets (true) // force HTTP reload = true
    console.log (reloadedMarkets['ETH/BTC'])
}) ()

Python示例代码:

poloniex = ccxt.poloniex({'verbose': True}) # log HTTP requests
poloniex.load_markets() # request markets
print(poloniex.id, poloniex.markets)   # output a full list of all loaded markets
print(list(poloniex.markets.keys())) # output a short list of market symbols
print(poloniex.markets['BTC/ETH'])     # output single market details
poloniex.load_markets() # return a locally cached version, no reload
reloadedMarkets = poloniex.load_markets(True) # force HTTP reload = True
print(reloadedMarkets['ETH/ZEC'])

PHP示例代码:

$bitfinex = new \ccxt\bitfinex (array ('verbose' => true)); // log HTTP requests
$bitfinex.load_markets (); // request markets
var_dump ($bitfinex->id, $bitfinex->markets); // output a full list of all loaded markets
var_dump (array_keys ($bitfinex->markets));   // output a short list of market symbols
var_dump ($bitfinex->markets['XRP/USD']);     // output single market details
$bitfinex->load_markets (); // return a locally cached version, no reload
$reloadedMarkets = $bitfinex->load_markets (true); // force HTTP reload = true
var_dump ($bitfinex->markets['XRP/BTC']);