@@ -2,38 +2,51 @@ package plugins
22
33import (
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
1316var _ 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/)
1619type 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
7891func (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
8497func (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
114144func (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
130192func (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
204326func (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