Skip to content
This repository has been archived by the owner on Feb 1, 2024. It is now read-only.

Commit

Permalink
Complete trading APIs for CCXT (#85), closes #11
Browse files Browse the repository at this point in the history
* ccxt_trading: 1 - thread through API Key - testing to get API key to work

* ccxt_trading: 2 - instance creation logic

* ccxt_trading: 3 - FetchBalance() implementation for ccxt + test

* ccxt_trading: 4 - implement ccxtExchange#GetAccountBalances()

* ccxt_trading: 5 - fix GetOpenOrders API to supply the tradingPairs as input

* ccxt_trading: 6 - implement ccxt.FetchOpenOrders + test

* ccxt_trading: 7 - ccxtExchange#GetOpenOrders() + test

* ccxt_trading: 8 - ccxt.CreateLimitOrder() + test

* ccxt_trading: 9 - ccxtExchange.AddOrder() + test

* ccxt_trading: 10 - ccxt.CancelOrder() + test

* ccxt_trading: 11 - update CancelOrder Exchange API + ccxtExchange.CancelOrder() + test

* ccxt_trading: 12 - clean up ccxtExchange tests

* ccxt_trading: 13 - enable trading on binance

* ccxt_trading: 14 - use orderConstraints in ccxtExchange

* ccxt_trading: 15 - add support for fetching trade history
  • Loading branch information
nikhilsaraf authored Jan 9, 2019
1 parent 0be414c commit 5cf0aed
Show file tree
Hide file tree
Showing 14 changed files with 1,229 additions and 129 deletions.
6 changes: 3 additions & 3 deletions api/exchange.go
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ type FillHandler interface {
// TradeFetcher is the common method between FillTrackable and exchange
// temporarily extracted out from TradeAPI so SDEX has the flexibility to only implement this rather than exchange and FillTrackable
type TradeFetcher interface {
GetTradeHistory(maybeCursorStart interface{}, maybeCursorEnd interface{}) (*TradeHistoryResult, error)
GetTradeHistory(pair model.TradingPair, maybeCursorStart interface{}, maybeCursorEnd interface{}) (*TradeHistoryResult, error)
}

// FillTrackable enables any implementing exchange to support fill tracking
Expand All @@ -85,11 +85,11 @@ type TradeAPI interface {

TradeFetcher

GetOpenOrders() (map[model.TradingPair][]model.OpenOrder, error)
GetOpenOrders(pairs []*model.TradingPair) (map[model.TradingPair][]model.OpenOrder, error)

AddOrder(order *model.Order) (*model.TransactionID, error)

CancelOrder(txID *model.TransactionID) (model.CancelOrderResult, error)
CancelOrder(txID *model.TransactionID, pair model.TradingPair) (model.CancelOrderResult, error)
}

// PrepareDepositResult is the result of a PrepareDeposit call
Expand Down
6 changes: 4 additions & 2 deletions glide.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions glide.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -21,3 +21,5 @@ import:
version: 7595ba02fbce171759c10d69d96e4cd898d1fa93
- package: github.com/nikhilsaraf/go-tools
version: 19004f22be08c82a22e679726ca22853c65919ae
- package: github.com/mitchellh/mapstructure
version: v1.1.2
8 changes: 6 additions & 2 deletions model/orderbook.go
Original file line number Diff line number Diff line change
Expand Up @@ -175,11 +175,15 @@ type OpenOrder struct {

// String is the stringer function
func (o OpenOrder) String() string {
return fmt.Sprintf("OpenOrder[order=%s, ID=%s, startTime=%d, expireTime=%d, volumeExecuted=%s]",
expireTimeString := "<nil>"
if o.ExpireTime != nil {
expireTimeString = fmt.Sprintf("%d", o.ExpireTime.AsInt64())
}
return fmt.Sprintf("OpenOrder[order=%s, ID=%s, startTime=%d, expireTime=%s, volumeExecuted=%s]",
o.Order.String(),
o.ID,
o.StartTime.AsInt64(),
o.ExpireTime.AsInt64(),
expireTimeString,
o.VolumeExecuted.AsString(),
)
}
Expand Down
13 changes: 13 additions & 0 deletions model/tradingPair.go
Original file line number Diff line number Diff line change
Expand Up @@ -73,3 +73,16 @@ func TradingPairs2Strings(c *AssetConverter, delim string, pairs []TradingPair)
}
return m, nil
}

// TradingPairs2Strings2 converts the trading pairs to an array of strings
func TradingPairs2Strings2(c *AssetConverter, delim string, pairs []*TradingPair) (map[TradingPair]string, error) {
m := map[TradingPair]string{}
for _, p := range pairs {
pairString, e := p.ToString(c, delim)
if e != nil {
return nil, e
}
m[*p] = pairString
}
return m, nil
}
219 changes: 183 additions & 36 deletions plugins/ccxtExchange.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,38 +2,51 @@ package plugins

import (
"fmt"
"log"

"github.com/interstellar/kelp/api"
"github.com/interstellar/kelp/model"
"github.com/interstellar/kelp/support/sdk"
"github.com/interstellar/kelp/support/utils"
)

const ccxtBalancePrecision = 10

// ensure that ccxtExchange conforms to the Exchange interface
var _ api.Exchange = ccxtExchange{}

// ccxtExchange is the implementation for the CCXT REST library that supports many exchanges (https://github.com/franz-see/ccxt-rest, https://github.com/ccxt/ccxt/)
type ccxtExchange struct {
assetConverter *model.AssetConverter
delimiter string
api *sdk.Ccxt
precision int8
simMode bool
assetConverter *model.AssetConverter
delimiter string
orderConstraints map[model.TradingPair]model.OrderConstraints
api *sdk.Ccxt
simMode bool
}

// makeCcxtExchange is a factory method to make an exchange using the CCXT interface
func makeCcxtExchange(ccxtBaseURL string, exchangeName string, simMode bool) (api.Exchange, error) {
c, e := sdk.MakeInitializedCcxtExchange(ccxtBaseURL, exchangeName)
func makeCcxtExchange(
ccxtBaseURL string,
exchangeName string,
orderConstraints map[model.TradingPair]model.OrderConstraints,
apiKeys []api.ExchangeAPIKey,
simMode bool,
) (api.Exchange, error) {
if len(apiKeys) == 0 {
return nil, fmt.Errorf("need at least 1 ExchangeAPIKey, even if it is an empty key")
}

c, e := sdk.MakeInitializedCcxtExchange(ccxtBaseURL, exchangeName, apiKeys[0])
if e != nil {
return nil, fmt.Errorf("error making a ccxt exchange: %s", e)
}

return ccxtExchange{
assetConverter: model.CcxtAssetConverter,
delimiter: "/",
api: c,
precision: utils.SdexPrecision,
simMode: simMode,
assetConverter: model.CcxtAssetConverter,
delimiter: "/",
orderConstraints: orderConstraints,
api: c,
simMode: simMode,
}, nil
}

Expand Down Expand Up @@ -61,8 +74,8 @@ func (c ccxtExchange) GetTickerPrice(pairs []model.TradingPair) (map[model.Tradi
}

priceResult[p] = api.Ticker{
AskPrice: model.NumberFromFloat(askPrice, c.precision),
BidPrice: model.NumberFromFloat(bidPrice, c.precision),
AskPrice: model.NumberFromFloat(askPrice, c.orderConstraints[p].PricePrecision),
BidPrice: model.NumberFromFloat(bidPrice, c.orderConstraints[p].PricePrecision),
}
}

Expand All @@ -76,14 +89,31 @@ func (c ccxtExchange) GetAssetConverter() *model.AssetConverter {

// GetOrderConstraints impl
func (c ccxtExchange) GetOrderConstraints(pair *model.TradingPair) *model.OrderConstraints {
// TODO implement
return nil
oc := c.orderConstraints[*pair]
return &oc
}

// GetAccountBalances impl
func (c ccxtExchange) GetAccountBalances(assetList []model.Asset) (map[model.Asset]model.Number, error) {
// TODO implement
return nil, nil
balanceResponse, e := c.api.FetchBalance()
if e != nil {
return nil, e
}

m := map[model.Asset]model.Number{}
for _, asset := range assetList {
ccxtAssetString, e := c.GetAssetConverter().ToString(asset)
if e != nil {
return nil, e
}

if ccxtBalance, ok := balanceResponse[ccxtAssetString]; ok {
m[asset] = *model.NumberFromFloat(ccxtBalance.Total, ccxtBalancePrecision)
} else {
m[asset] = *model.NumberConstants.Zero
}
}
return m, nil
}

// GetOrderBook impl
Expand Down Expand Up @@ -112,20 +142,52 @@ func (c ccxtExchange) GetOrderBook(pair *model.TradingPair, maxCount int32) (*mo
}

func (c ccxtExchange) readOrders(orders []sdk.CcxtOrder, pair *model.TradingPair, orderAction model.OrderAction) []model.Order {
pricePrecision := c.orderConstraints[*pair].PricePrecision
volumePrecision := c.orderConstraints[*pair].VolumePrecision

result := []model.Order{}
for _, o := range orders {
result = append(result, model.Order{
Pair: pair,
OrderAction: orderAction,
OrderType: model.OrderTypeLimit,
Price: model.NumberFromFloat(o.Price, c.precision),
Volume: model.NumberFromFloat(o.Amount, c.precision),
Price: model.NumberFromFloat(o.Price, pricePrecision),
Volume: model.NumberFromFloat(o.Amount, volumePrecision),
Timestamp: nil,
})
}
return result
}

// GetTradeHistory impl
func (c ccxtExchange) GetTradeHistory(pair model.TradingPair, maybeCursorStart interface{}, maybeCursorEnd interface{}) (*api.TradeHistoryResult, error) {
pairString, e := pair.ToString(c.assetConverter, c.delimiter)
if e != nil {
return nil, fmt.Errorf("error converting pair to string: %s", e)
}

// TODO use cursor when fetching trade history
tradesRaw, e := c.api.FetchMyTrades(pairString)
if e != nil {
return nil, fmt.Errorf("error while fetching trade history for trading pair '%s': %s", pairString, e)
}

trades := []model.Trade{}
for _, raw := range tradesRaw {
t, e := c.readTrade(&pair, pairString, raw)
if e != nil {
return nil, fmt.Errorf("error while reading trade: %s", e)
}
trades = append(trades, *t)
}

// TODO implement cursor logic
return &api.TradeHistoryResult{
Cursor: nil,
Trades: trades,
}, nil
}

// GetTrades impl
func (c ccxtExchange) GetTrades(pair *model.TradingPair, maybeCursor interface{}) (*api.TradesResult, error) {
pairString, e := pair.ToString(c.assetConverter, c.delimiter)
Expand Down Expand Up @@ -160,11 +222,14 @@ func (c ccxtExchange) readTrade(pair *model.TradingPair, pairString string, rawT
return nil, fmt.Errorf("expected '%s' for 'symbol' field, got: %s", pairString, rawTrade.Symbol)
}

pricePrecision := c.orderConstraints[*pair].PricePrecision
volumePrecision := c.orderConstraints[*pair].VolumePrecision

trade := model.Trade{
Order: model.Order{
Pair: pair,
Price: model.NumberFromFloat(rawTrade.Price, c.precision),
Volume: model.NumberFromFloat(rawTrade.Amount, c.precision),
Price: model.NumberFromFloat(rawTrade.Price, pricePrecision),
Volume: model.NumberFromFloat(rawTrade.Amount, volumePrecision),
OrderType: model.OrderTypeLimit,
Timestamp: model.MakeTimestamp(rawTrade.Timestamp),
},
Expand All @@ -181,34 +246,116 @@ func (c ccxtExchange) readTrade(pair *model.TradingPair, pairString string, rawT
}

if rawTrade.Cost != 0.0 {
// use 2x the precision for cost since it's logically derived from amount and price
trade.Cost = model.NumberFromFloat(rawTrade.Cost, c.precision*2)
// use bigger precision for cost since it's logically derived from amount and price
costPrecision := pricePrecision
if volumePrecision > pricePrecision {
costPrecision = volumePrecision
}
trade.Cost = model.NumberFromFloat(rawTrade.Cost, costPrecision)
}

return &trade, nil
}

// GetTradeHistory impl
func (c ccxtExchange) GetTradeHistory(maybeCursorStart interface{}, maybeCursorEnd interface{}) (*api.TradeHistoryResult, error) {
// TODO implement
return nil, nil
// GetOpenOrders impl
func (c ccxtExchange) GetOpenOrders(pairs []*model.TradingPair) (map[model.TradingPair][]model.OpenOrder, error) {
pairStrings := []string{}
string2Pair := map[string]model.TradingPair{}
for _, pair := range pairs {
pairString, e := pair.ToString(c.assetConverter, c.delimiter)
if e != nil {
return nil, fmt.Errorf("error converting pairs to strings: %s", e)
}
pairStrings = append(pairStrings, pairString)
string2Pair[pairString] = *pair
}

openOrdersMap, e := c.api.FetchOpenOrders(pairStrings)
if e != nil {
return nil, fmt.Errorf("error while fetching open orders for trading pairs '%v': %s", pairStrings, e)
}

result := map[model.TradingPair][]model.OpenOrder{}
for asset, ccxtOrderList := range openOrdersMap {
pair, ok := string2Pair[asset]
if !ok {
return nil, fmt.Errorf("traing symbol %s returned from FetchOpenOrders was not in the original list of trading pairs: %v", asset, pairStrings)
}

openOrderList := []model.OpenOrder{}
for _, o := range ccxtOrderList {
openOrder, e := c.convertOpenOrderFromCcxt(&pair, o)
if e != nil {
return nil, fmt.Errorf("cannot convertOpenOrderFromCcxt: %s", e)
}
openOrderList = append(openOrderList, *openOrder)
}
result[pair] = openOrderList
}
return result, nil
}

// GetOpenOrders impl
func (c ccxtExchange) GetOpenOrders() (map[model.TradingPair][]model.OpenOrder, error) {
// TODO implement
return nil, nil
func (c ccxtExchange) convertOpenOrderFromCcxt(pair *model.TradingPair, o sdk.CcxtOpenOrder) (*model.OpenOrder, error) {
if o.Type != "limit" {
return nil, fmt.Errorf("we currently only support limit order types")
}

orderAction := model.OrderActionSell
if o.Side == "buy" {
orderAction = model.OrderActionBuy
}
ts := model.MakeTimestamp(o.Timestamp)

return &model.OpenOrder{
Order: model.Order{
Pair: pair,
OrderAction: orderAction,
OrderType: model.OrderTypeLimit,
Price: model.NumberFromFloat(o.Price, c.orderConstraints[*pair].PricePrecision),
Volume: model.NumberFromFloat(o.Amount, c.orderConstraints[*pair].VolumePrecision),
Timestamp: ts,
},
ID: o.ID,
StartTime: ts,
ExpireTime: nil,
VolumeExecuted: model.NumberFromFloat(o.Filled, c.orderConstraints[*pair].VolumePrecision),
}, nil
}

// AddOrder impl
func (c ccxtExchange) AddOrder(order *model.Order) (*model.TransactionID, error) {
// TODO implement
return nil, nil
pairString, e := order.Pair.ToString(c.assetConverter, c.delimiter)
if e != nil {
return nil, fmt.Errorf("error converting pair to string: %s", e)
}

side := "sell"
if order.OrderAction.IsBuy() {
side = "buy"
}

log.Printf("ccxt is submitting order: pair=%s, orderAction=%s, orderType=%s, volume=%s, price=%s\n",
pairString, order.OrderAction.String(), order.OrderType.String(), order.Volume.AsString(), order.Price.AsString())
ccxtOpenOrder, e := c.api.CreateLimitOrder(pairString, side, order.Volume.AsFloat(), order.Price.AsFloat())
if e != nil {
return nil, fmt.Errorf("error while creating limit order %s: %s", *order, e)
}

return model.MakeTransactionID(ccxtOpenOrder.ID), nil
}

// CancelOrder impl
func (c ccxtExchange) CancelOrder(txID *model.TransactionID) (model.CancelOrderResult, error) {
// TODO implement
func (c ccxtExchange) CancelOrder(txID *model.TransactionID, pair model.TradingPair) (model.CancelOrderResult, error) {
log.Printf("ccxt is canceling order: ID=%s, tradingPair: %s\n", txID.String(), pair.String())

resp, e := c.api.CancelOrder(txID.String(), pair.String())
if e != nil {
return model.CancelResultFailed, e
}

if resp == nil {
return model.CancelResultFailed, fmt.Errorf("response from CancelOrder was nil")
}
return model.CancelResultCancelSuccessful, nil
}

Expand Down
Loading

0 comments on commit 5cf0aed

Please sign in to comment.