]> git.immae.eu Git - perso/Immae/Projets/Cryptomonnaies/Cryptoportfolio/Trader.git/blob - ccxt_wrapper.py
Merge branch 'dev'
[perso/Immae/Projets/Cryptomonnaies/Cryptoportfolio/Trader.git] / ccxt_wrapper.py
1 from ccxt import *
2 import decimal
3 import time
4 from retry.api import retry_call
5 import re
6 from requests.exceptions import RequestException
7 from ssl import SSLError
8
9 def _cw_exchange_sum(self, *args):
10 return sum([arg for arg in args if isinstance(arg, (float, int, decimal.Decimal))])
11 Exchange.sum = _cw_exchange_sum
12
13 class poloniexE(poloniex):
14 RETRIABLE_CALLS = [
15 re.compile(r"^return"),
16 re.compile(r"^cancel"),
17 re.compile(r"^closeMarginPosition$"),
18 re.compile(r"^getMarginPosition$"),
19 ]
20
21 def request(self, path, api='public', method='GET', params={}, headers=None, body=None):
22 """
23 Wrapped to allow retry of non-posting requests"
24 """
25
26 origin_request = super().request
27 kwargs = {
28 "api": api,
29 "method": method,
30 "params": params,
31 "headers": headers,
32 "body": body
33 }
34
35 retriable = any(re.match(call, path) for call in self.RETRIABLE_CALLS)
36 if api == "public" or method == "GET" or retriable:
37 return retry_call(origin_request, fargs=[path], fkwargs=kwargs,
38 tries=10, delay=1, exceptions=(RequestTimeout, InvalidNonce))
39 else:
40 return origin_request(path, **kwargs)
41
42 def __init__(self, *args, **kwargs):
43 super().__init__(*args, **kwargs)
44
45 # For requests logging
46 self.session.origin_request = self.session.request
47 self.session._parent = self
48
49 def request_wrap(self, *args, **kwargs):
50 try:
51 r = self.origin_request(*args, **kwargs)
52 self._parent._market.report.log_http_request(args[0],
53 args[1], kwargs["data"], kwargs["headers"], r)
54 return r
55 except (SSLError, RequestException) as e:
56 self._parent._market.report.log_http_request(args[0],
57 args[1], kwargs["data"], kwargs["headers"], e)
58 raise e
59
60 self.session.request = request_wrap.__get__(self.session,
61 self.session.__class__)
62
63 @staticmethod
64 def nanoseconds():
65 return int(time.time() * 1000000000)
66
67 def fetch_margin_balance(self):
68 """
69 portfolio.market.privatePostGetMarginPosition({"currencyPair": "BTC_DASH"})
70 See DASH/BTC positions
71 {'amount': '-0.10000000', -> DASH empruntés (à rendre)
72 'basePrice': '0.06818560', -> à ce prix là (0.06828800 demandé * (1-0.15%))
73 'lendingFees': '0.00000000', -> ce que je dois à mon créditeur en intérêts
74 'liquidationPrice': '0.15107132', -> prix auquel ça sera liquidé (dépend de ce que j’ai déjà sur mon compte margin)
75 'pl': '-0.00000371', -> plus-value latente si je rachète tout de suite (négatif = perdu)
76 'total': '0.00681856', -> valeur totale empruntée en BTC (au moment de l'échange)
77 = amount * basePrice à erreur d'arrondi près
78 'type': 'short'}
79 """
80 positions = self.privatePostGetMarginPosition({"currencyPair": "all"})
81 parsed = {}
82 for symbol, position in positions.items():
83 if position["type"] == "none":
84 continue
85 base_currency, currency = symbol.split("_")
86 parsed[currency] = {
87 "amount": decimal.Decimal(position["amount"]),
88 "borrowedPrice": decimal.Decimal(position["basePrice"]),
89 "lendingFees": decimal.Decimal(position["lendingFees"]),
90 "pl": decimal.Decimal(position["pl"]),
91 "liquidationPrice": decimal.Decimal(position["liquidationPrice"]),
92 "type": position["type"],
93 "total": decimal.Decimal(position["total"]),
94 "baseCurrency": base_currency,
95 }
96 return parsed
97
98 def fetch_balance_per_type(self):
99 balances = self.privatePostReturnAvailableAccountBalances()
100 result = {'info': balances}
101 for key, balance in balances.items():
102 result[key] = {}
103 for currency, amount in balance.items():
104 result.setdefault(currency, {})
105 result[currency][key] = decimal.Decimal(amount)
106 result[key][currency] = decimal.Decimal(amount)
107 return result
108
109 def fetch_all_balances(self):
110 exchange_balances = self.fetch_balance()
111 margin_balances = self.fetch_margin_balance()
112 balances_per_type = self.fetch_balance_per_type()
113
114 all_balances = {}
115 in_positions = {}
116 pending_pl = {}
117
118 for currency, exchange_balance in exchange_balances.items():
119 if currency in ["info", "free", "used", "total"]:
120 continue
121
122 margin_balance = margin_balances.get(currency, {})
123 balance_per_type = balances_per_type.get(currency, {})
124
125 all_balances[currency] = {
126 "total": exchange_balance["total"] + margin_balance.get("amount", 0),
127 "exchange_used": exchange_balance["used"],
128 "exchange_total": exchange_balance["total"] - balance_per_type.get("margin", 0),
129 "exchange_free": exchange_balance["free"] - balance_per_type.get("margin", 0),
130 # Disponible sur le compte margin
131 "margin_available": balance_per_type.get("margin", 0),
132 # Bloqué en position
133 "margin_in_position": 0,
134 # Emprunté
135 "margin_borrowed": -margin_balance.get("amount", 0),
136 # Total
137 "margin_total": balance_per_type.get("margin", 0) + margin_balance.get("amount", 0),
138 "margin_pending_gain": 0,
139 "margin_lending_fees": margin_balance.get("lendingFees", 0),
140 "margin_pending_base_gain": margin_balance.get("pl", 0),
141 "margin_position_type": margin_balance.get("type", None),
142 "margin_liquidation_price": margin_balance.get("liquidationPrice", 0),
143 "margin_borrowed_base_price": margin_balance.get("total", 0),
144 "margin_borrowed_base_currency": margin_balance.get("baseCurrency", None),
145 }
146 if len(margin_balance) > 0:
147 in_positions.setdefault(margin_balance["baseCurrency"], 0)
148 in_positions[margin_balance["baseCurrency"]] += margin_balance["total"]
149
150 pending_pl.setdefault(margin_balance["baseCurrency"], 0)
151 pending_pl[margin_balance["baseCurrency"]] += margin_balance["pl"]
152
153 # J’emprunte 0.12062983 que je revends à 0.06003598 BTC/DASH, soit 0.00724213 BTC.
154 # Sur ces 0.00724213 BTC je récupère 0.00724213*(1-0.0015) = 0.00723127 BTC
155 #
156 # -> ordertrades ne tient pas compte des fees
157 # amount = montant vendu (un seul mouvement)
158 # rate = à ce taux
159 # total = total en BTC (pour ce mouvement)
160 # -> marginposition:
161 # amount = ce que je dois rendre
162 # basePrice = prix de vente en tenant compte des fees
163 # (amount * basePrice = la quantité de BTC que j’ai effectivement
164 # reçue à erreur d’arrondi près, utiliser plutôt "total")
165 # total = la quantité de BTC que j’ai reçue
166 # pl = plus value actuelle si je rachetais tout de suite
167 # -> marginaccountsummary:
168 # currentMargin = La marge actuelle (= netValue/totalBorrowedValue)
169 # totalValue = BTC actuellement en margin (déposé)
170 # totalBorrowedValue = sum (amount * ticker[lowestAsk])
171 # pl = sum(pl)
172 # netValue = BTC actuellement en margin (déposé) + pl
173 # Exemple:
174 # In [38]: m.ccxt.private_post_returnordertrades({"orderNumber": "XXXXXXXXXXXX"})
175 # Out[38]:
176 # [{'amount': '0.11882982',
177 # 'currencyPair': 'BTC_DASH',
178 # 'date': '2018-02-26 22:48:35',
179 # 'fee': '0.00150000',
180 # 'globalTradeID': 348891380,
181 # 'rate': '0.06003598',
182 # 'total': '0.00713406',
183 # 'tradeID': 9634443,
184 # 'type': 'sell'},
185 # {'amount': '0.00180000',
186 # 'currencyPair': 'BTC_DASH',
187 # 'date': '2018-02-26 22:48:30',
188 # 'fee': '0.00150000',
189 # 'globalTradeID': 348891375,
190 # 'rate': '0.06003598',
191 # 'total': '0.00010806',
192 # 'tradeID': 9634442,
193 # 'type': 'sell'}]
194 #
195 # In [51]: m.ccxt.privatePostGetMarginPosition({"currencyPair": "BTC_DASH"})
196 # Out[51]:
197 # {'amount': '-0.12062982',
198 # 'basePrice': '0.05994587',
199 # 'lendingFees': '0.00000000',
200 # 'liquidationPrice': '0.15531479',
201 # 'pl': '0.00000122',
202 # 'total': '0.00723126',
203 # 'type': 'short'}
204 # In [52]: m.ccxt.privatePostGetMarginPosition({"currencyPair": "BTC_BTS"})
205 # Out[52]:
206 # {'amount': '-332.97159188',
207 # 'basePrice': '0.00002171',
208 # 'lendingFees': '0.00000000',
209 # 'liquidationPrice': '0.00005543',
210 # 'pl': '0.00029548',
211 # 'total': '0.00723127',
212 # 'type': 'short'}
213 #
214 # In [53]: m.ccxt.privatePostReturnMarginAccountSummary()
215 # Out[53]:
216 # {'currentMargin': '1.04341991',
217 # 'lendingFees': '0.00000000',
218 # 'netValue': '0.01478093',
219 # 'pl': '0.00029666',
220 # 'totalBorrowedValue': '0.01416585',
221 # 'totalValue': '0.01448427'}
222
223 for currency, in_position in in_positions.items():
224 all_balances[currency]["total"] += in_position
225 all_balances[currency]["margin_in_position"] += in_position
226 all_balances[currency]["margin_total"] += in_position
227
228 for currency, pl in pending_pl.items():
229 all_balances[currency]["margin_pending_gain"] += pl
230
231 return all_balances
232
233 def create_exchange_order(self, symbol, type, side, amount, price=None, params={}):
234 return super().create_order(symbol, type, side, amount, price=price, params=params)
235
236 def create_margin_order(self, symbol, type, side, amount, price=None, lending_rate=None, params={}):
237 if type == 'market':
238 raise ExchangeError(self.id + ' allows limit orders only')
239 self.load_markets()
240 method = 'privatePostMargin' + self.capitalize(side)
241 market = self.market(symbol)
242 price = float(price)
243 amount = float(amount)
244 if lending_rate is not None:
245 params = self.extend({"lendingRate": lending_rate}, params)
246 response = getattr(self, method)(self.extend({
247 'currencyPair': market['id'],
248 'rate': self.price_to_precision(symbol, price),
249 'amount': self.amount_to_precision(symbol, amount),
250 }, params))
251 timestamp = self.milliseconds()
252 order = self.parse_order(self.extend({
253 'timestamp': timestamp,
254 'status': 'open',
255 'type': type,
256 'side': side,
257 'price': price,
258 'amount': amount,
259 }, response), market)
260 id = order['id']
261 self.orders[id] = order
262 return self.extend({'info': response}, order)
263
264 def order_precision(self, symbol):
265 return 8
266
267 def transfer_balance(self, currency, amount, from_account, to_account):
268 result = self.privatePostTransferBalance({
269 "currency": currency,
270 "amount": amount,
271 "fromAccount": from_account,
272 "toAccount": to_account,
273 "confirmed": 1})
274 return result["success"] == 1
275
276 def close_margin_position(self, currency, base_currency):
277 """
278 closeMarginPosition({"currencyPair": "BTC_DASH"})
279 fermer la position au prix du marché
280 """
281 symbol = "{}_{}".format(base_currency, currency)
282 self.privatePostCloseMarginPosition({"currencyPair": symbol})
283
284 def tradable_balances(self):
285 """
286 portfolio.market.privatePostReturnTradableBalances()
287 Returns tradable balances in margin
288 'BTC_DASH': {'BTC': '0.01266999', 'DASH': '0.08574839'},
289 Je peux emprunter jusqu’à 0.08574839 DASH ou 0.01266999 BTC (une position est déjà ouverte)
290 'BTC_CLAM': {'BTC': '0.00585143', 'CLAM': '7.79300395'},
291 Je peux emprunter 7.7 CLAM pour les vendre contre des BTC, ou emprunter 0.00585143 BTC pour acheter des CLAM
292 """
293
294 tradable_balances = self.privatePostReturnTradableBalances()
295 for symbol, balances in tradable_balances.items():
296 for currency, balance in balances.items():
297 balances[currency] = decimal.Decimal(balance)
298 return tradable_balances
299
300 def margin_summary(self):
301 """
302 portfolio.market.privatePostReturnMarginAccountSummary()
303 Returns current informations for margin
304 {'currentMargin': '1.49680968', -> marge (ne doit pas descendre sous 20% / 0.2)
305 = netValue / totalBorrowedValue
306 'lendingFees': '0.00000000', -> fees totaux
307 'netValue': '0.01008254', -> balance + plus-value
308 'pl': '0.00008254', -> plus value latente (somme des positions)
309 'totalBorrowedValue': '0.00673602', -> valeur empruntée convertie en BTC.
310 (= sum(amount * ticker[lowerAsk]) pour amount dans marginposition)
311 'totalValue': '0.01000000'} -> balance (collateral déposé en margin)
312 """
313 summary = self.privatePostReturnMarginAccountSummary()
314
315 return {
316 "current_margin": decimal.Decimal(summary["currentMargin"]),
317 "lending_fees": decimal.Decimal(summary["lendingFees"]),
318 "gains": decimal.Decimal(summary["pl"]),
319 "total_borrowed": decimal.Decimal(summary["totalBorrowedValue"]),
320 "total": decimal.Decimal(summary["totalValue"]),
321 }
322
323 def nonce(self):
324 """
325 Wrapped to allow nonce with other libraries
326 """
327 return self.nanoseconds()
328
329 def fetch_balance(self, params={}):
330 """
331 Wrapped to get decimals
332 """
333 self.load_markets()
334 balances = self.privatePostReturnCompleteBalances(self.extend({
335 'account': 'all',
336 }, params))
337 result = {'info': balances}
338 currencies = list(balances.keys())
339 for c in range(0, len(currencies)):
340 id = currencies[c]
341 balance = balances[id]
342 currency = self.common_currency_code(id)
343 account = {
344 'free': decimal.Decimal(balance['available']),
345 'used': decimal.Decimal(balance['onOrders']),
346 'total': decimal.Decimal(0.0),
347 }
348 account['total'] = self.sum(account['free'], account['used'])
349 result[currency] = account
350 return self.parse_balance(result)
351
352 def parse_ticker(self, ticker, market=None):
353 """
354 Wrapped to get decimals
355 """
356 timestamp = self.milliseconds()
357 symbol = None
358 if market:
359 symbol = market['symbol']
360 return {
361 'symbol': symbol,
362 'timestamp': timestamp,
363 'datetime': self.iso8601(timestamp),
364 'high': decimal.Decimal(ticker['high24hr']),
365 'low': decimal.Decimal(ticker['low24hr']),
366 'bid': decimal.Decimal(ticker['highestBid']),
367 'ask': decimal.Decimal(ticker['lowestAsk']),
368 'vwap': None,
369 'open': None,
370 'close': None,
371 'first': None,
372 'last': decimal.Decimal(ticker['last']),
373 'change': decimal.Decimal(ticker['percentChange']),
374 'percentage': None,
375 'average': None,
376 'baseVolume': decimal.Decimal(ticker['quoteVolume']),
377 'quoteVolume': decimal.Decimal(ticker['baseVolume']),
378 'info': ticker,
379 }
380
381 def create_order(self, symbol, type, side, amount, price=None, account="exchange", lending_rate=None, params={}):
382 """
383 Wrapped to handle margin and exchange accounts
384 """
385 if account == "exchange":
386 return self.create_exchange_order(symbol, type, side, amount, price=price, params=params)
387 elif account == "margin":
388 return self.create_margin_order(symbol, type, side, amount, price=price, lending_rate=lending_rate, params=params)
389 else:
390 raise NotImplementedError
391
392