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

Commit 5cf0aed

Browse files
authored
Complete trading APIs for CCXT (#85), closes #11
* 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
1 parent 0be414c commit 5cf0aed

14 files changed

Lines changed: 1229 additions & 129 deletions

api/exchange.go

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,7 @@ type FillHandler interface {
5858
// TradeFetcher is the common method between FillTrackable and exchange
5959
// temporarily extracted out from TradeAPI so SDEX has the flexibility to only implement this rather than exchange and FillTrackable
6060
type TradeFetcher interface {
61-
GetTradeHistory(maybeCursorStart interface{}, maybeCursorEnd interface{}) (*TradeHistoryResult, error)
61+
GetTradeHistory(pair model.TradingPair, maybeCursorStart interface{}, maybeCursorEnd interface{}) (*TradeHistoryResult, error)
6262
}
6363

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

8686
TradeFetcher
8787

88-
GetOpenOrders() (map[model.TradingPair][]model.OpenOrder, error)
88+
GetOpenOrders(pairs []*model.TradingPair) (map[model.TradingPair][]model.OpenOrder, error)
8989

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

92-
CancelOrder(txID *model.TransactionID) (model.CancelOrderResult, error)
92+
CancelOrder(txID *model.TransactionID, pair model.TradingPair) (model.CancelOrderResult, error)
9393
}
9494

9595
// PrepareDepositResult is the result of a PrepareDeposit call

glide.lock

Lines changed: 4 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

glide.yaml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,3 +21,5 @@ import:
2121
version: 7595ba02fbce171759c10d69d96e4cd898d1fa93
2222
- package: github.com/nikhilsaraf/go-tools
2323
version: 19004f22be08c82a22e679726ca22853c65919ae
24+
- package: github.com/mitchellh/mapstructure
25+
version: v1.1.2

model/orderbook.go

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -175,11 +175,15 @@ type OpenOrder struct {
175175

176176
// String is the stringer function
177177
func (o OpenOrder) String() string {
178-
return fmt.Sprintf("OpenOrder[order=%s, ID=%s, startTime=%d, expireTime=%d, volumeExecuted=%s]",
178+
expireTimeString := "<nil>"
179+
if o.ExpireTime != nil {
180+
expireTimeString = fmt.Sprintf("%d", o.ExpireTime.AsInt64())
181+
}
182+
return fmt.Sprintf("OpenOrder[order=%s, ID=%s, startTime=%d, expireTime=%s, volumeExecuted=%s]",
179183
o.Order.String(),
180184
o.ID,
181185
o.StartTime.AsInt64(),
182-
o.ExpireTime.AsInt64(),
186+
expireTimeString,
183187
o.VolumeExecuted.AsString(),
184188
)
185189
}

model/tradingPair.go

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,3 +73,16 @@ func TradingPairs2Strings(c *AssetConverter, delim string, pairs []TradingPair)
7373
}
7474
return m, nil
7575
}
76+
77+
// TradingPairs2Strings2 converts the trading pairs to an array of strings
78+
func TradingPairs2Strings2(c *AssetConverter, delim string, pairs []*TradingPair) (map[TradingPair]string, error) {
79+
m := map[TradingPair]string{}
80+
for _, p := range pairs {
81+
pairString, e := p.ToString(c, delim)
82+
if e != nil {
83+
return nil, e
84+
}
85+
m[*p] = pairString
86+
}
87+
return m, nil
88+
}

plugins/ccxtExchange.go

Lines changed: 183 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -2,38 +2,51 @@ package plugins
22

33
import (
44
"fmt"
5+
"log"
56

67
"github.com/interstellar/kelp/api"
78
"github.com/interstellar/kelp/model"
89
"github.com/interstellar/kelp/support/sdk"
910
"github.com/interstellar/kelp/support/utils"
1011
)
1112

13+
const ccxtBalancePrecision = 10
14+
1215
// ensure that ccxtExchange conforms to the Exchange interface
1316
var _ api.Exchange = ccxtExchange{}
1417

1518
// 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/)
1619
type ccxtExchange struct {
17-
assetConverter *model.AssetConverter
18-
delimiter string
19-
api *sdk.Ccxt
20-
precision int8
21-
simMode bool
20+
assetConverter *model.AssetConverter
21+
delimiter string
22+
orderConstraints map[model.TradingPair]model.OrderConstraints
23+
api *sdk.Ccxt
24+
simMode bool
2225
}
2326

2427
// makeCcxtExchange is a factory method to make an exchange using the CCXT interface
25-
func makeCcxtExchange(ccxtBaseURL string, exchangeName string, simMode bool) (api.Exchange, error) {
26-
c, e := sdk.MakeInitializedCcxtExchange(ccxtBaseURL, exchangeName)
28+
func makeCcxtExchange(
29+
ccxtBaseURL string,
30+
exchangeName string,
31+
orderConstraints map[model.TradingPair]model.OrderConstraints,
32+
apiKeys []api.ExchangeAPIKey,
33+
simMode bool,
34+
) (api.Exchange, error) {
35+
if len(apiKeys) == 0 {
36+
return nil, fmt.Errorf("need at least 1 ExchangeAPIKey, even if it is an empty key")
37+
}
38+
39+
c, e := sdk.MakeInitializedCcxtExchange(ccxtBaseURL, exchangeName, apiKeys[0])
2740
if e != nil {
2841
return nil, fmt.Errorf("error making a ccxt exchange: %s", e)
2942
}
3043

3144
return ccxtExchange{
32-
assetConverter: model.CcxtAssetConverter,
33-
delimiter: "/",
34-
api: c,
35-
precision: utils.SdexPrecision,
36-
simMode: simMode,
45+
assetConverter: model.CcxtAssetConverter,
46+
delimiter: "/",
47+
orderConstraints: orderConstraints,
48+
api: c,
49+
simMode: simMode,
3750
}, nil
3851
}
3952

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

6376
priceResult[p] = api.Ticker{
64-
AskPrice: model.NumberFromFloat(askPrice, c.precision),
65-
BidPrice: model.NumberFromFloat(bidPrice, c.precision),
77+
AskPrice: model.NumberFromFloat(askPrice, c.orderConstraints[p].PricePrecision),
78+
BidPrice: model.NumberFromFloat(bidPrice, c.orderConstraints[p].PricePrecision),
6679
}
6780
}
6881

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

7790
// GetOrderConstraints impl
7891
func (c ccxtExchange) GetOrderConstraints(pair *model.TradingPair) *model.OrderConstraints {
79-
// TODO implement
80-
return nil
92+
oc := c.orderConstraints[*pair]
93+
return &oc
8194
}
8295

8396
// GetAccountBalances impl
8497
func (c ccxtExchange) GetAccountBalances(assetList []model.Asset) (map[model.Asset]model.Number, error) {
85-
// TODO implement
86-
return nil, nil
98+
balanceResponse, e := c.api.FetchBalance()
99+
if e != nil {
100+
return nil, e
101+
}
102+
103+
m := map[model.Asset]model.Number{}
104+
for _, asset := range assetList {
105+
ccxtAssetString, e := c.GetAssetConverter().ToString(asset)
106+
if e != nil {
107+
return nil, e
108+
}
109+
110+
if ccxtBalance, ok := balanceResponse[ccxtAssetString]; ok {
111+
m[asset] = *model.NumberFromFloat(ccxtBalance.Total, ccxtBalancePrecision)
112+
} else {
113+
m[asset] = *model.NumberConstants.Zero
114+
}
115+
}
116+
return m, nil
87117
}
88118

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

114144
func (c ccxtExchange) readOrders(orders []sdk.CcxtOrder, pair *model.TradingPair, orderAction model.OrderAction) []model.Order {
145+
pricePrecision := c.orderConstraints[*pair].PricePrecision
146+
volumePrecision := c.orderConstraints[*pair].VolumePrecision
147+
115148
result := []model.Order{}
116149
for _, o := range orders {
117150
result = append(result, model.Order{
118151
Pair: pair,
119152
OrderAction: orderAction,
120153
OrderType: model.OrderTypeLimit,
121-
Price: model.NumberFromFloat(o.Price, c.precision),
122-
Volume: model.NumberFromFloat(o.Amount, c.precision),
154+
Price: model.NumberFromFloat(o.Price, pricePrecision),
155+
Volume: model.NumberFromFloat(o.Amount, volumePrecision),
123156
Timestamp: nil,
124157
})
125158
}
126159
return result
127160
}
128161

162+
// GetTradeHistory impl
163+
func (c ccxtExchange) GetTradeHistory(pair model.TradingPair, maybeCursorStart interface{}, maybeCursorEnd interface{}) (*api.TradeHistoryResult, error) {
164+
pairString, e := pair.ToString(c.assetConverter, c.delimiter)
165+
if e != nil {
166+
return nil, fmt.Errorf("error converting pair to string: %s", e)
167+
}
168+
169+
// TODO use cursor when fetching trade history
170+
tradesRaw, e := c.api.FetchMyTrades(pairString)
171+
if e != nil {
172+
return nil, fmt.Errorf("error while fetching trade history for trading pair '%s': %s", pairString, e)
173+
}
174+
175+
trades := []model.Trade{}
176+
for _, raw := range tradesRaw {
177+
t, e := c.readTrade(&pair, pairString, raw)
178+
if e != nil {
179+
return nil, fmt.Errorf("error while reading trade: %s", e)
180+
}
181+
trades = append(trades, *t)
182+
}
183+
184+
// TODO implement cursor logic
185+
return &api.TradeHistoryResult{
186+
Cursor: nil,
187+
Trades: trades,
188+
}, nil
189+
}
190+
129191
// GetTrades impl
130192
func (c ccxtExchange) GetTrades(pair *model.TradingPair, maybeCursor interface{}) (*api.TradesResult, error) {
131193
pairString, e := pair.ToString(c.assetConverter, c.delimiter)
@@ -160,11 +222,14 @@ func (c ccxtExchange) readTrade(pair *model.TradingPair, pairString string, rawT
160222
return nil, fmt.Errorf("expected '%s' for 'symbol' field, got: %s", pairString, rawTrade.Symbol)
161223
}
162224

225+
pricePrecision := c.orderConstraints[*pair].PricePrecision
226+
volumePrecision := c.orderConstraints[*pair].VolumePrecision
227+
163228
trade := model.Trade{
164229
Order: model.Order{
165230
Pair: pair,
166-
Price: model.NumberFromFloat(rawTrade.Price, c.precision),
167-
Volume: model.NumberFromFloat(rawTrade.Amount, c.precision),
231+
Price: model.NumberFromFloat(rawTrade.Price, pricePrecision),
232+
Volume: model.NumberFromFloat(rawTrade.Amount, volumePrecision),
168233
OrderType: model.OrderTypeLimit,
169234
Timestamp: model.MakeTimestamp(rawTrade.Timestamp),
170235
},
@@ -181,34 +246,116 @@ func (c ccxtExchange) readTrade(pair *model.TradingPair, pairString string, rawT
181246
}
182247

183248
if rawTrade.Cost != 0.0 {
184-
// use 2x the precision for cost since it's logically derived from amount and price
185-
trade.Cost = model.NumberFromFloat(rawTrade.Cost, c.precision*2)
249+
// use bigger precision for cost since it's logically derived from amount and price
250+
costPrecision := pricePrecision
251+
if volumePrecision > pricePrecision {
252+
costPrecision = volumePrecision
253+
}
254+
trade.Cost = model.NumberFromFloat(rawTrade.Cost, costPrecision)
186255
}
187256

188257
return &trade, nil
189258
}
190259

191-
// GetTradeHistory impl
192-
func (c ccxtExchange) GetTradeHistory(maybeCursorStart interface{}, maybeCursorEnd interface{}) (*api.TradeHistoryResult, error) {
193-
// TODO implement
194-
return nil, nil
260+
// GetOpenOrders impl
261+
func (c ccxtExchange) GetOpenOrders(pairs []*model.TradingPair) (map[model.TradingPair][]model.OpenOrder, error) {
262+
pairStrings := []string{}
263+
string2Pair := map[string]model.TradingPair{}
264+
for _, pair := range pairs {
265+
pairString, e := pair.ToString(c.assetConverter, c.delimiter)
266+
if e != nil {
267+
return nil, fmt.Errorf("error converting pairs to strings: %s", e)
268+
}
269+
pairStrings = append(pairStrings, pairString)
270+
string2Pair[pairString] = *pair
271+
}
272+
273+
openOrdersMap, e := c.api.FetchOpenOrders(pairStrings)
274+
if e != nil {
275+
return nil, fmt.Errorf("error while fetching open orders for trading pairs '%v': %s", pairStrings, e)
276+
}
277+
278+
result := map[model.TradingPair][]model.OpenOrder{}
279+
for asset, ccxtOrderList := range openOrdersMap {
280+
pair, ok := string2Pair[asset]
281+
if !ok {
282+
return nil, fmt.Errorf("traing symbol %s returned from FetchOpenOrders was not in the original list of trading pairs: %v", asset, pairStrings)
283+
}
284+
285+
openOrderList := []model.OpenOrder{}
286+
for _, o := range ccxtOrderList {
287+
openOrder, e := c.convertOpenOrderFromCcxt(&pair, o)
288+
if e != nil {
289+
return nil, fmt.Errorf("cannot convertOpenOrderFromCcxt: %s", e)
290+
}
291+
openOrderList = append(openOrderList, *openOrder)
292+
}
293+
result[pair] = openOrderList
294+
}
295+
return result, nil
195296
}
196297

197-
// GetOpenOrders impl
198-
func (c ccxtExchange) GetOpenOrders() (map[model.TradingPair][]model.OpenOrder, error) {
199-
// TODO implement
200-
return nil, nil
298+
func (c ccxtExchange) convertOpenOrderFromCcxt(pair *model.TradingPair, o sdk.CcxtOpenOrder) (*model.OpenOrder, error) {
299+
if o.Type != "limit" {
300+
return nil, fmt.Errorf("we currently only support limit order types")
301+
}
302+
303+
orderAction := model.OrderActionSell
304+
if o.Side == "buy" {
305+
orderAction = model.OrderActionBuy
306+
}
307+
ts := model.MakeTimestamp(o.Timestamp)
308+
309+
return &model.OpenOrder{
310+
Order: model.Order{
311+
Pair: pair,
312+
OrderAction: orderAction,
313+
OrderType: model.OrderTypeLimit,
314+
Price: model.NumberFromFloat(o.Price, c.orderConstraints[*pair].PricePrecision),
315+
Volume: model.NumberFromFloat(o.Amount, c.orderConstraints[*pair].VolumePrecision),
316+
Timestamp: ts,
317+
},
318+
ID: o.ID,
319+
StartTime: ts,
320+
ExpireTime: nil,
321+
VolumeExecuted: model.NumberFromFloat(o.Filled, c.orderConstraints[*pair].VolumePrecision),
322+
}, nil
201323
}
202324

203325
// AddOrder impl
204326
func (c ccxtExchange) AddOrder(order *model.Order) (*model.TransactionID, error) {
205-
// TODO implement
206-
return nil, nil
327+
pairString, e := order.Pair.ToString(c.assetConverter, c.delimiter)
328+
if e != nil {
329+
return nil, fmt.Errorf("error converting pair to string: %s", e)
330+
}
331+
332+
side := "sell"
333+
if order.OrderAction.IsBuy() {
334+
side = "buy"
335+
}
336+
337+
log.Printf("ccxt is submitting order: pair=%s, orderAction=%s, orderType=%s, volume=%s, price=%s\n",
338+
pairString, order.OrderAction.String(), order.OrderType.String(), order.Volume.AsString(), order.Price.AsString())
339+
ccxtOpenOrder, e := c.api.CreateLimitOrder(pairString, side, order.Volume.AsFloat(), order.Price.AsFloat())
340+
if e != nil {
341+
return nil, fmt.Errorf("error while creating limit order %s: %s", *order, e)
342+
}
343+
344+
return model.MakeTransactionID(ccxtOpenOrder.ID), nil
207345
}
208346

209347
// CancelOrder impl
210-
func (c ccxtExchange) CancelOrder(txID *model.TransactionID) (model.CancelOrderResult, error) {
211-
// TODO implement
348+
func (c ccxtExchange) CancelOrder(txID *model.TransactionID, pair model.TradingPair) (model.CancelOrderResult, error) {
349+
log.Printf("ccxt is canceling order: ID=%s, tradingPair: %s\n", txID.String(), pair.String())
350+
351+
resp, e := c.api.CancelOrder(txID.String(), pair.String())
352+
if e != nil {
353+
return model.CancelResultFailed, e
354+
}
355+
356+
if resp == nil {
357+
return model.CancelResultFailed, fmt.Errorf("response from CancelOrder was nil")
358+
}
212359
return model.CancelResultCancelSuccessful, nil
213360
}
214361

0 commit comments

Comments
 (0)