aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorIsmaël Bouya <ismael.bouya@normalesup.org>2018-02-03 21:31:29 +0100
committerIsmaël Bouya <ismael.bouya@normalesup.org>2018-02-03 21:31:29 +0100
commit350ed24de673dc125be9e2fdecb0f1abc7835b41 (patch)
treec567dd8ebc773da6329d5a48cd4b23cbf831888f
parentecba11139e357567c46f7ba2a0cf8dbd98266fe8 (diff)
downloadTrader-350ed24de673dc125be9e2fdecb0f1abc7835b41.tar.gz
Trader-350ed24de673dc125be9e2fdecb0f1abc7835b41.tar.zst
Trader-350ed24de673dc125be9e2fdecb0f1abc7835b41.zip
Work in progress to use shorts
-rw-r--r--market.py92
-rw-r--r--portfolio.py132
-rw-r--r--test.py135
3 files changed, 270 insertions, 89 deletions
diff --git a/market.py b/market.py
index 1601e2d..1e1e083 100644
--- a/market.py
+++ b/market.py
@@ -4,6 +4,7 @@ import decimal
4def exchange_sum(self, *args): 4def exchange_sum(self, *args):
5 return sum([arg for arg in args if isinstance(arg, (float, int, decimal.Decimal))]) 5 return sum([arg for arg in args if isinstance(arg, (float, int, decimal.Decimal))])
6ccxt.Exchange.sum = exchange_sum 6ccxt.Exchange.sum = exchange_sum
7
7def poloniex_fetch_balance(self, params={}): 8def poloniex_fetch_balance(self, params={}):
8 self.load_markets() 9 self.load_markets()
9 balances = self.privatePostReturnCompleteBalances(self.extend({ 10 balances = self.privatePostReturnCompleteBalances(self.extend({
@@ -25,6 +26,39 @@ def poloniex_fetch_balance(self, params={}):
25 return self.parse_balance(result) 26 return self.parse_balance(result)
26ccxt.poloniex.fetch_balance = poloniex_fetch_balance 27ccxt.poloniex.fetch_balance = poloniex_fetch_balance
27 28
29def poloniex_fetch_margin_balances(self):
30 positions = self.privatePostGetMarginPosition({"currencyPair": "all"})
31 parsed = {}
32 for symbol, position in positions.items():
33 if position["type"] == "none":
34 continue
35 base_currency, currency = symbol.split("_")
36 parsed[currency] = {
37 "amount": decimal.Decimal(position["amount"]),
38 "borrowedPrice": decimal.Decimal(position["basePrice"]),
39 "lendingFees": decimal.Decimal(position["lendingFees"]),
40 "pl": decimal.Decimal(position["pl"]),
41 "liquidationPrice": decimal.Decimal(position["liquidationPrice"]),
42 "type": position["type"],
43 "total": decimal.Decimal(position["total"]),
44 "base_currency": base_currency,
45 }
46 return parsed
47ccxt.poloniex.fetch_margin_balances = poloniex_fetch_margin_balances
48
49def poloniex_fetch_balance_with_margin(self, params={}):
50 exchange_balance = self.fetch_balance(params=params)
51 margin_balances = self.fetch_margin_balances()
52
53 for currency, balance in margin_balances.items():
54 assert exchange_balance[currency]["total"] == 0
55 assert balance["type"] == "short"
56 exchange_balance[currency]["total"] = balance["amount"]
57 exchange_balance[currency]["marginPosition"] = balance
58 return exchange_balance
59ccxt.poloniex.fetch_balance_with_margin = poloniex_fetch_balance_with_margin
60
61
28def poloniex_fetch_balance_per_type(self): 62def poloniex_fetch_balance_per_type(self):
29 balances = self.privatePostReturnAvailableAccountBalances() 63 balances = self.privatePostReturnAvailableAccountBalances()
30 result = {'info': balances} 64 result = {'info': balances}
@@ -92,6 +126,7 @@ def poloniex_create_margin_order(self, symbol, type, side, amount, price=None, l
92 id = order['id'] 126 id = order['id']
93 self.orders[id] = order 127 self.orders[id] = order
94 return self.extend({'info': response}, order) 128 return self.extend({'info': response}, order)
129ccxt.poloniex.create_margin_order = poloniex_create_margin_order
95 130
96def poloniex_create_order(self, symbol, type, side, amount, price=None, account="exchange", lending_rate=None, params={}): 131def poloniex_create_order(self, symbol, type, side, amount, price=None, account="exchange", lending_rate=None, params={}):
97 if account == "exchange": 132 if account == "exchange":
@@ -100,8 +135,65 @@ def poloniex_create_order(self, symbol, type, side, amount, price=None, account=
100 return self.create_margin_order(symbol, type, side, amount, price=price, lending_rate=lending_rate, params=params) 135 return self.create_margin_order(symbol, type, side, amount, price=price, lending_rate=lending_rate, params=params)
101 else: 136 else:
102 raise NotImplementedError 137 raise NotImplementedError
138
139def poloniex_order_precision(self, symbol):
140 return 8
141
103ccxt.poloniex.create_exchange_order = ccxt.poloniex.create_order 142ccxt.poloniex.create_exchange_order = ccxt.poloniex.create_order
104ccxt.poloniex.create_order = poloniex_create_order 143ccxt.poloniex.create_order = poloniex_create_order
144ccxt.poloniex.order_precision = poloniex_order_precision
145
146def poloniex_transfer_balance(self, currency, amount, from_account, to_account):
147 result = self.privatePostTransferBalance({
148 "currency": currency,
149 "amount": amount,
150 "fromAccount": from_account,
151 "toAccount": to_account,
152 "confirmed": 1})
153 return result["success"] == 1
154ccxt.poloniex.transfer_balance = poloniex_transfer_balance
155
156# portfolio.market.create_order("DASH/BTC", "limit", "sell", 0.1, price=0.06828800, account="margin")
157
158# portfolio.market.privatePostReturnTradableBalances()
159# Returns tradable balances in margin
160# 'BTC_DASH': {'BTC': '0.01266999', 'DASH': '0.08574839'},
161# Je peux emprunter jusqu’à 0.08574839 DASH ou 0.01266999 BTC (une position est
162# déjà ouverte)
163# 'BTC_CLAM': {'BTC': '0.00585143', 'CLAM': '7.79300395'},
164# Je peux emprunter 7.7 CLAM pour les vendre contre des BTC, ou emprunter
165# 0.00585143 BTC pour acheter des CLAM
166
167# portfolio.market.privatePostReturnMarginAccountSummary()
168# Returns current informations for margin
169# {'currentMargin': '1.49680968', -> marge (ne doit pas descendre sous 20% / 0.2)
170# = netValue / totalBorrowedValue
171# 'lendingFees': '0.00000000', -> fees totaux
172# 'netValue': '0.01008254', -> balance + plus-value
173# 'pl': '0.00008254', -> plus value latente (somme des positions)
174# 'totalBorrowedValue': '0.00673602', -> valeur en BTC empruntée
175# 'totalValue': '0.01000000'} -> valeur totale en compte
176
177
178# portfolio.market.privatePostGetMarginPosition({"currencyPair": "BTC_DASH"})
179# See DASH/BTC positions
180# {'amount': '-0.10000000', -> DASH empruntés
181# 'basePrice': '0.06818560', -> à ce prix là (0.06828800 demandé * (1-0.15%))
182# 'lendingFees': '0.00000000', -> ce que je dois à mon créditeur
183# 'liquidationPrice': '0.15107132', -> prix auquel ça sera liquidé (dépend de ce que j’ai déjà sur mon compte margin)
184# 'pl': '-0.00000371', -> plus-value latente si je rachète tout de suite (négatif = perdu)
185# 'total': '0.00681856', -> valeur totale empruntée en BTC
186# 'type': 'short'}
187
188
189# closeMarginPosition({"currencyPair": "BTC_DASH"}) : fermer la position au prix
190# du marché
191# Nécessaire à la fin
192# portfolio.market.create_order("DASH/BTC", "limit", "buy", 0.1, price=0.06726487, account="margin")
193
194# portfolio.market.fetch_balance_per_type()
195# Ne suffit pas pour calculer les positions: ne contient que les 0.01 envoyés
196# TODO: vérifier si fetch_balance marque ces 0.01 comme disponibles -> oui
105 197
106market = ccxt.poloniex({ 198market = ccxt.poloniex({
107 "apiKey": "XXXXXXXX-XXXXXXXX-XXXXXXXX-XXXXXXXX", 199 "apiKey": "XXXXXXXX-XXXXXXXX-XXXXXXXX-XXXXXXXX",
diff --git a/portfolio.py b/portfolio.py
index 3257bcf..d9d2d4d 100644
--- a/portfolio.py
+++ b/portfolio.py
@@ -1,6 +1,6 @@
1from ccxt import ExchangeError 1from ccxt import ExchangeError
2import time 2import time
3from decimal import Decimal as D 3from decimal import Decimal as D, ROUND_DOWN
4# Put your poloniex api key in market.py 4# Put your poloniex api key in market.py
5from market import market 5from market import market
6 6
@@ -10,7 +10,7 @@ class Portfolio:
10 data = None 10 data = None
11 11
12 @classmethod 12 @classmethod
13 def repartition_pertenthousand(cls, liquidity="medium"): 13 def repartition(cls, liquidity="medium"):
14 cls.parse_cryptoportfolio() 14 cls.parse_cryptoportfolio()
15 liquidities = cls.liquidities[liquidity] 15 liquidities = cls.liquidities[liquidity]
16 cls.last_date = sorted(liquidities.keys())[-1] 16 cls.last_date = sorted(liquidities.keys())[-1]
@@ -40,7 +40,7 @@ class Portfolio:
40 cls.get_cryptoportfolio() 40 cls.get_cryptoportfolio()
41 41
42 def filter_weights(weight_hash): 42 def filter_weights(weight_hash):
43 if weight_hash[1] == 0: 43 if weight_hash[1][0] == 0:
44 return False 44 return False
45 if weight_hash[0] == "_row": 45 if weight_hash[0] == "_row":
46 return False 46 return False
@@ -48,15 +48,13 @@ class Portfolio:
48 48
49 def clean_weights(i): 49 def clean_weights(i):
50 def clean_weights_(h): 50 def clean_weights_(h):
51 if isinstance(h[1][i], str): 51 if h[0].endswith("s"):
52 return [h[0], h[1][i]] 52 return [h[0][0:-1], (h[1][i], "short")]
53 else: 53 else:
54 return [h[0], int(h[1][i] * 10000)] 54 return [h[0], (h[1][i], "long")]
55 return clean_weights_ 55 return clean_weights_
56 56
57 def parse_weights(portfolio_hash): 57 def parse_weights(portfolio_hash):
58 # FIXME: we'll need shorts at some point
59 assert all(map(lambda x: x == "long", portfolio_hash["holding"]["direction"]))
60 weights_hash = portfolio_hash["weights"] 58 weights_hash = portfolio_hash["weights"]
61 weights = {} 59 weights = {}
62 for i in range(len(weights_hash["_row"])): 60 for i in range(len(weights_hash["_row"])):
@@ -105,6 +103,9 @@ class Amount:
105 else: 103 else:
106 raise Exception("This asset is not available in the chosen market") 104 raise Exception("This asset is not available in the chosen market")
107 105
106 def __round__(self, n=8):
107 return Amount(self.currency, self.value.quantize(D(1)/D(10**n), rounding=ROUND_DOWN))
108
108 def __abs__(self): 109 def __abs__(self):
109 return Amount(self.currency, abs(self.value)) 110 return Amount(self.currency, abs(self.value))
110 111
@@ -196,35 +197,50 @@ class Balance:
196 for key in hash_: 197 for key in hash_:
197 if key in ["info", "free", "used", "total"]: 198 if key in ["info", "free", "used", "total"]:
198 continue 199 continue
199 if hash_[key]["total"] > 0 or key in cls.known_balances: 200 if hash_[key]["total"] != 0 or key in cls.known_balances:
200 cls.known_balances[key] = cls.from_hash(key, hash_[key]) 201 cls.known_balances[key] = cls.from_hash(key, hash_[key])
201 202
202 @classmethod 203 @classmethod
203 def fetch_balances(cls, market): 204 def fetch_balances(cls, market):
204 cls._fill_balances(market.fetch_balance()) 205 cls._fill_balances(market.fetch_balance())
205 return cls.known_balances 206 return cls.known_balances
207 # FIXME:Separate balances per trade type and in position
208 # Need to check how balances in position are represented
209
206 210
207 @classmethod 211 @classmethod
208 def dispatch_assets(cls, amount, repartition=None): 212 def dispatch_assets(cls, amount, repartition=None):
209 if repartition is None: 213 if repartition is None:
210 repartition = Portfolio.repartition_pertenthousand() 214 repartition = Portfolio.repartition()
211 sum_pertenthousand = sum([v for k, v in repartition.items()]) 215 sum_ratio = sum([v[0] for k, v in repartition.items()])
212 amounts = {} 216 amounts = {}
213 for currency, ptt in repartition.items(): 217 for currency, (ptt, trade_type) in repartition.items():
214 amounts[currency] = ptt * amount / sum_pertenthousand 218 amounts[currency] = ptt * amount / sum_ratio
215 if currency not in cls.known_balances: 219 if currency not in cls.known_balances:
216 cls.known_balances[currency] = cls(currency, 0, 0, 0) 220 cls.known_balances[currency] = cls(currency, 0, 0, 0)
217 return amounts 221 return amounts
218 222
219 @classmethod 223 @classmethod
224 def dispatch_trade_types(cls, repartition=None):
225 if repartition is None:
226 repartition = Portfolio.repartition()
227 trade_types = {}
228 for currency, (ptt, trade_type) in repartition.items():
229 trade_types[currency] = trade_type
230 return trade_types
231 # FIXME: once we know the repartition and sold everything, we can move
232 # the necessary part to the margin account
233
234 @classmethod
220 def prepare_trades(cls, market, base_currency="BTC", compute_value="average"): 235 def prepare_trades(cls, market, base_currency="BTC", compute_value="average"):
221 cls.fetch_balances(market) 236 cls.fetch_balances(market)
222 values_in_base = cls.in_currency(base_currency, market, compute_value=compute_value) 237 values_in_base = cls.in_currency(base_currency, market, compute_value=compute_value)
223 total_base_value = sum(values_in_base.values()) 238 total_base_value = sum(values_in_base.values())
224 new_repartition = cls.dispatch_assets(total_base_value) 239 new_repartition = cls.dispatch_assets(total_base_value)
240 trade_types = cls.dispatch_trade_types()
225 # Recompute it in case we have new currencies 241 # Recompute it in case we have new currencies
226 values_in_base = cls.in_currency(base_currency, market, compute_value=compute_value) 242 values_in_base = cls.in_currency(base_currency, market, compute_value=compute_value)
227 Trade.compute_trades(values_in_base, new_repartition, market=market) 243 Trade.compute_trades(values_in_base, new_repartition, trade_types, market=market)
228 244
229 @classmethod 245 @classmethod
230 def update_trades(cls, market, base_currency="BTC", compute_value="average", only=None): 246 def update_trades(cls, market, base_currency="BTC", compute_value="average", only=None):
@@ -232,15 +248,17 @@ class Balance:
232 values_in_base = cls.in_currency(base_currency, market, compute_value=compute_value) 248 values_in_base = cls.in_currency(base_currency, market, compute_value=compute_value)
233 total_base_value = sum(values_in_base.values()) 249 total_base_value = sum(values_in_base.values())
234 new_repartition = cls.dispatch_assets(total_base_value) 250 new_repartition = cls.dispatch_assets(total_base_value)
235 Trade.compute_trades(values_in_base, new_repartition, only=only, market=market) 251 trade_types = cls.dispatch_trade_types()
252 Trade.compute_trades(values_in_base, new_repartition, trade_types, only=only, market=market)
236 253
237 @classmethod 254 @classmethod
238 def prepare_trades_to_sell_all(cls, market, base_currency="BTC", compute_value="average"): 255 def prepare_trades_to_sell_all(cls, market, base_currency="BTC", compute_value="average"):
239 cls.fetch_balances(market) 256 cls.fetch_balances(market)
240 values_in_base = cls.in_currency(base_currency, market, compute_value=compute_value) 257 values_in_base = cls.in_currency(base_currency, market, compute_value=compute_value)
241 total_base_value = sum(values_in_base.values()) 258 total_base_value = sum(values_in_base.values())
242 new_repartition = cls.dispatch_assets(total_base_value, repartition={ base_currency: 1 }) 259 new_repartition = cls.dispatch_assets(total_base_value, repartition={ base_currency: (1, "long") })
243 Trade.compute_trades(values_in_base, new_repartition, market=market) 260 trade_types = cls.dispatch_trade_types()
261 Trade.compute_trades(values_in_base, new_repartition, trade_types, market=market)
244 262
245 def __repr__(self): 263 def __repr__(self):
246 return "Balance({} [{}/{}/{}])".format(self.currency, str(self.free), str(self.used), str(self.total)) 264 return "Balance({} [{}/{}/{}])".format(self.currency, str(self.free), str(self.used), str(self.total))
@@ -253,16 +271,16 @@ class Computation:
253 "ask": lambda x, y: x["ask"], 271 "ask": lambda x, y: x["ask"],
254 } 272 }
255 273
256
257class Trade: 274class Trade:
258 trades = {} 275 trades = {}
259 276
260 def __init__(self, value_from, value_to, currency, market=None): 277 def __init__(self, value_from, value_to, currency, trade_type, market=None):
261 # We have value_from of currency, and want to finish with value_to of 278 # We have value_from of currency, and want to finish with value_to of
262 # that currency. value_* may not be in currency's terms 279 # that currency. value_* may not be in currency's terms
263 self.currency = currency 280 self.currency = currency
264 self.value_from = value_from 281 self.value_from = value_from
265 self.value_to = value_to 282 self.value_to = value_to
283 self.trade_type = trade_type
266 self.orders = [] 284 self.orders = []
267 self.market = market 285 self.market = market
268 assert self.value_from.currency == self.value_to.currency 286 assert self.value_from.currency == self.value_to.currency
@@ -313,7 +331,7 @@ class Trade:
313 return cls.get_ticker(c1, c2, market) 331 return cls.get_ticker(c1, c2, market)
314 332
315 @classmethod 333 @classmethod
316 def compute_trades(cls, values_in_base, new_repartition, only=None, market=None): 334 def compute_trades(cls, values_in_base, new_repartition, trade_types, only=None, market=None):
317 base_currency = sum(values_in_base.values()).currency 335 base_currency = sum(values_in_base.values()).currency
318 for currency in Balance.currencies(): 336 for currency in Balance.currencies():
319 if currency == base_currency: 337 if currency == base_currency:
@@ -322,6 +340,7 @@ class Trade:
322 values_in_base.get(currency, Amount(base_currency, 0)), 340 values_in_base.get(currency, Amount(base_currency, 0)),
323 new_repartition.get(currency, Amount(base_currency, 0)), 341 new_repartition.get(currency, Amount(base_currency, 0)),
324 currency, 342 currency,
343 trade_types.get(currency, "long"),
325 market=market 344 market=market
326 ) 345 )
327 if only is None or trade.action == only: 346 if only is None or trade.action == only:
@@ -347,25 +366,30 @@ class Trade:
347 return "sell" 366 return "sell"
348 367
349 def order_action(self, inverted): 368 def order_action(self, inverted):
350 if self.value_from < self.value_to: 369 # a xor b xor c
351 return "buy" if not inverted else "sell" 370 if (self.trade_type == "short") != ((self.value_from < self.value_to) != inverted):
371 return "buy"
352 else: 372 else:
353 return "sell" if not inverted else "buy" 373 return "sell"
354 374
355 def prepare_order(self, compute_value="default"): 375 def prepare_order(self, compute_value="default"):
356 if self.action is None: 376 if self.action is None:
357 return 377 return
358 ticker = self.value_from.ticker 378 ticker = Trade.get_ticker(self.currency, self.base_currency, self.market)
359 inverted = ticker["inverted"] 379 inverted = ticker["inverted"]
360 if inverted: 380 if inverted:
361 ticker = ticker["original"] 381 ticker = ticker["original"]
362 rate = Trade.compute_value(ticker, self.order_action(inverted), compute_value=compute_value) 382 rate = Trade.compute_value(ticker, self.order_action(inverted), compute_value=compute_value)
363 # 0.1 383 # 0.1
364 384
385 # FIXME: optimize if value_to == 0 or value_from == 0?)
386
365 delta_in_base = abs(self.value_from - self.value_to) 387 delta_in_base = abs(self.value_from - self.value_to)
366 # 9 BTC's worth of move (10 - 1 or 1 - 10 depending on case) 388 # 9 BTC's worth of move (10 - 1 or 1 - 10 depending on case)
367 389
368 if not inverted: 390 if not inverted:
391 currency = self.base_currency
392 # BTC
369 if self.action == "sell": 393 if self.action == "sell":
370 # I have 10 BTC worth of FOO, and I want to sell 9 BTC worth of it 394 # I have 10 BTC worth of FOO, and I want to sell 9 BTC worth of it
371 # At rate 1 Foo = 0.1 BTC 395 # At rate 1 Foo = 0.1 BTC
@@ -382,35 +406,26 @@ class Trade:
382 delta = delta_in_base.in_currency(self.currency, self.market, rate=1/rate) 406 delta = delta_in_base.in_currency(self.currency, self.market, rate=1/rate)
383 # I want to buy 9 / 0.1 FOO 407 # I want to buy 9 / 0.1 FOO
384 # Action: "buy" "90 FOO" at rate "0.1" "BTC" on "market" 408 # Action: "buy" "90 FOO" at rate "0.1" "BTC" on "market"
385
386 # FIXME: Need to round up to the correct amount of FOO in case
387 # we want to use all BTC
388 currency = self.base_currency
389 # BTC
390 else: 409 else:
391 if self.action == "sell":
392 # I have 10 BTC worth of FOO, and I want to sell 9 BTC worth of it
393 # At rate 1 Foo = 0.1 BTC
394 delta = delta_in_base
395 # Action: "buy" "9 BTC" at rate "1/0.1" "FOO" on market
396
397 # FIXME: Need to round up to the correct amount of FOO in case
398 # we want to sell all
399 else:
400 delta = delta_in_base
401 # I want to buy 9 / 0.1 FOO
402 # Action: "sell" "9 BTC" at rate "1/0.1" "FOO" on "market"
403
404 # FIXME: Need to round up to the correct amount of FOO in case
405 # we want to use all BTC
406
407 currency = self.currency 410 currency = self.currency
408 # FOO 411 # FOO
412 delta = delta_in_base
413 # sell:
414 # I have 10 BTC worth of FOO, and I want to sell 9 BTC worth of it
415 # At rate 1 Foo = 0.1 BTC
416 # Action: "buy" "9 BTC" at rate "1/0.1" "FOO" on market
417 # buy:
418 # I want to buy 9 / 0.1 FOO
419 # Action: "sell" "9 BTC" at rate "1/0.1" "FOO" on "market"
409 420
410 self.orders.append(Order(self.order_action(inverted), delta, rate, currency, self.market)) 421 self.orders.append(Order(self.order_action(inverted), delta, rate, currency, self.trade_type, self.market))
411 422
412 @classmethod 423 @classmethod
413 def compute_value(cls, ticker, action, compute_value="default"): 424 def compute_value(cls, ticker, action, compute_value="default"):
425 if action == "buy":
426 action = "ask"
427 if action == "sell":
428 action = "bid"
414 if isinstance(compute_value, str): 429 if isinstance(compute_value, str):
415 compute_value = Computation.computations[compute_value] 430 compute_value = Computation.computations[compute_value]
416 return compute_value(ticker, action) 431 return compute_value(ticker, action)
@@ -450,11 +465,12 @@ class Trade:
450 order.get_status() 465 order.get_status()
451 466
452 def __repr__(self): 467 def __repr__(self):
453 return "Trade({} -> {} in {}, {})".format( 468 return "Trade({} -> {} in {}, {} {})".format(
454 self.value_from, 469 self.value_from,
455 self.value_to, 470 self.value_to,
456 self.currency, 471 self.currency,
457 self.action) 472 self.action,
473 self.trade_type)
458 474
459 @classmethod 475 @classmethod
460 def print_all_with_order(cls): 476 def print_all_with_order(cls):
@@ -467,19 +483,20 @@ class Trade:
467 print("\t", order, sep="") 483 print("\t", order, sep="")
468 484
469class Order: 485class Order:
470 def __init__(self, action, amount, rate, base_currency, market, account="exchange"): 486 def __init__(self, action, amount, rate, base_currency, trade_type, market):
471 self.action = action 487 self.action = action
472 self.amount = amount 488 self.amount = amount
473 self.rate = rate 489 self.rate = rate
474 self.base_currency = base_currency 490 self.base_currency = base_currency
475 self.market = market 491 self.market = market
476 self.account = account 492 self.trade_type = trade_type
477 self.result = None 493 self.result = None
478 self.status = "pending" 494 self.status = "pending"
479 495
480 def __repr__(self): 496 def __repr__(self):
481 return "Order({} {} at {} {} [{}])".format( 497 return "Order({} {} {} at {} {} [{}])".format(
482 self.action, 498 self.action,
499 self.trade_type,
483 self.amount, 500 self.amount,
484 self.rate, 501 self.rate,
485 self.base_currency, 502 self.base_currency,
@@ -487,6 +504,13 @@ class Order:
487 ) 504 )
488 505
489 @property 506 @property
507 def account(self):
508 if self.trade_type == "long":
509 return "exchange"
510 else:
511 return "margin"
512
513 @property
490 def pending(self): 514 def pending(self):
491 return self.status == "pending" 515 return self.status == "pending"
492 516
@@ -496,13 +520,15 @@ class Order:
496 520
497 def run(self, debug=False): 521 def run(self, debug=False):
498 symbol = "{}/{}".format(self.amount.currency, self.base_currency) 522 symbol = "{}/{}".format(self.amount.currency, self.base_currency)
499 amount = self.amount.value 523 amount = round(self.amount, self.market.order_precision(symbol)).value
500 524
501 if debug: 525 if debug:
502 print("market.create_order('{}', 'limit', '{}', {}, price={}, account={})".format( 526 print("market.create_order('{}', 'limit', '{}', {}, price={}, account={})".format(
503 symbol, self.action, amount, self.rate, self.account)) 527 symbol, self.action, amount, self.rate, self.account))
504 else: 528 else:
505 try: 529 try:
530 if self.action == "sell" and self.trade_type == "short":
531 assert self.market.transfer_balance(self.base_currency, amount * self.rate, "exchange", "margin")
506 self.result = self.market.create_order(symbol, 'limit', self.action, amount, price=self.rate, account=self.account) 532 self.result = self.market.create_order(symbol, 'limit', self.action, amount, price=self.rate, account=self.account)
507 self.status = "open" 533 self.status = "open"
508 except Exception as e: 534 except Exception as e:
@@ -527,7 +553,7 @@ def print_orders(market, base_currency="BTC"):
527 Trade.prepare_orders(compute_value="average") 553 Trade.prepare_orders(compute_value="average")
528 for currency, balance in Balance.known_balances.items(): 554 for currency, balance in Balance.known_balances.items():
529 print(balance) 555 print(balance)
530 portfolio.Trade.print_all_with_order() 556 Trade.print_all_with_order()
531 557
532def make_orders(market, base_currency="BTC"): 558def make_orders(market, base_currency="BTC"):
533 Balance.prepare_trades(market, base_currency=base_currency) 559 Balance.prepare_trades(market, base_currency=base_currency)
diff --git a/test.py b/test.py
index 8a6ba50..8240eb4 100644
--- a/test.py
+++ b/test.py
@@ -174,7 +174,7 @@ class PortfolioTest(unittest.TestCase):
174 174
175 with open("test_portfolio.json") as example: 175 with open("test_portfolio.json") as example:
176 import json 176 import json
177 self.json_response = json.load(example) 177 self.json_response = json.load(example, parse_int=portfolio.D, parse_float=portfolio.D)
178 178
179 self.patcher = mock.patch.multiple(portfolio.Portfolio, data=None, liquidities={}) 179 self.patcher = mock.patch.multiple(portfolio.Portfolio, data=None, liquidities={})
180 self.patcher.start() 180 self.patcher.start()
@@ -220,29 +220,63 @@ class PortfolioTest(unittest.TestCase):
220 self.assertEqual(10, len(liquidities["medium"].keys())) 220 self.assertEqual(10, len(liquidities["medium"].keys()))
221 self.assertEqual(10, len(liquidities["high"].keys())) 221 self.assertEqual(10, len(liquidities["high"].keys()))
222 222
223 expected = {'BTC': 2857, 'DGB': 1015, 'DOGE': 1805, 'SC': 623, 'ZEC': 3701} 223 expected = {
224 'BTC': (D("0.2857"), "long"),
225 'DGB': (D("0.1015"), "long"),
226 'DOGE': (D("0.1805"), "long"),
227 'SC': (D("0.0623"), "long"),
228 'ZEC': (D("0.3701"), "long"),
229 }
224 self.assertDictEqual(expected, liquidities["high"]['2018-01-08']) 230 self.assertDictEqual(expected, liquidities["high"]['2018-01-08'])
225 231
226 expected = {'ETC': 1000, 'FCT': 1000, 'GAS': 1000, 'NAV': 1000, 'OMG': 1000, 'OMNI': 1000, 'PPC': 1000, 'RIC': 1000, 'VIA': 1000, 'XCP': 1000} 232 expected = {
233 'BTC': (D("1.1102e-16"), "long"),
234 'ETC': (D("0.1"), "long"),
235 'FCT': (D("0.1"), "long"),
236 'GAS': (D("0.1"), "long"),
237 'NAV': (D("0.1"), "long"),
238 'OMG': (D("0.1"), "long"),
239 'OMNI': (D("0.1"), "long"),
240 'PPC': (D("0.1"), "long"),
241 'RIC': (D("0.1"), "long"),
242 'VIA': (D("0.1"), "long"),
243 'XCP': (D("0.1"), "long"),
244 }
227 self.assertDictEqual(expected, liquidities["medium"]['2018-01-08']) 245 self.assertDictEqual(expected, liquidities["medium"]['2018-01-08'])
228 246
229 # It doesn't refetch the data when available 247 # It doesn't refetch the data when available
230 portfolio.Portfolio.parse_cryptoportfolio() 248 portfolio.Portfolio.parse_cryptoportfolio()
231 mock_get.assert_called_once_with() 249 mock_get.assert_called_once_with()
232 250
233 portfolio.Portfolio.data["portfolio_1"]["holding"]["direction"][3] = "short"
234 self.assertRaises(AssertionError, portfolio.Portfolio.parse_cryptoportfolio)
235
236 @mock.patch.object(portfolio.Portfolio, "get_cryptoportfolio") 251 @mock.patch.object(portfolio.Portfolio, "get_cryptoportfolio")
237 def test_repartition_pertenthousand(self, mock_get): 252 def test_repartition(self, mock_get):
238 mock_get.side_effect = self.fill_data 253 mock_get.side_effect = self.fill_data
239 254
240 expected_medium = {'USDT': 1000, 'ETC': 1000, 'FCT': 1000, 'OMG': 1000, 'STEEM': 1000, 'STRAT': 1000, 'XEM': 1000, 'XMR': 1000, 'XVC': 1000, 'ZRX': 1000} 255 expected_medium = {
241 expected_high = {'USDT': 1226, 'BTC': 1429, 'ETC': 1127, 'ETH': 1569, 'FCT': 3341, 'GAS': 1308} 256 'BTC': (D("1.1102e-16"), "long"),
257 'USDT': (D("0.1"), "long"),
258 'ETC': (D("0.1"), "long"),
259 'FCT': (D("0.1"), "long"),
260 'OMG': (D("0.1"), "long"),
261 'STEEM': (D("0.1"), "long"),
262 'STRAT': (D("0.1"), "long"),
263 'XEM': (D("0.1"), "long"),
264 'XMR': (D("0.1"), "long"),
265 'XVC': (D("0.1"), "long"),
266 'ZRX': (D("0.1"), "long"),
267 }
268 expected_high = {
269 'USDT': (D("0.1226"), "long"),
270 'BTC': (D("0.1429"), "long"),
271 'ETC': (D("0.1127"), "long"),
272 'ETH': (D("0.1569"), "long"),
273 'FCT': (D("0.3341"), "long"),
274 'GAS': (D("0.1308"), "long"),
275 }
242 276
243 self.assertEqual(expected_medium, portfolio.Portfolio.repartition_pertenthousand()) 277 self.assertEqual(expected_medium, portfolio.Portfolio.repartition())
244 self.assertEqual(expected_medium, portfolio.Portfolio.repartition_pertenthousand(liquidity="medium")) 278 self.assertEqual(expected_medium, portfolio.Portfolio.repartition(liquidity="medium"))
245 self.assertEqual(expected_high, portfolio.Portfolio.repartition_pertenthousand(liquidity="high")) 279 self.assertEqual(expected_high, portfolio.Portfolio.repartition(liquidity="high"))
246 280
247 def tearDown(self): 281 def tearDown(self):
248 self.patcher.stop() 282 self.patcher.stop()
@@ -339,7 +373,7 @@ class BalanceTest(unittest.TestCase):
339 self.assertEqual(0, portfolio.Balance.known_balances["ETC"].total) 373 self.assertEqual(0, portfolio.Balance.known_balances["ETC"].total)
340 self.assertListEqual(["USDT", "XVG", "ETC"], list(portfolio.Balance.currencies())) 374 self.assertListEqual(["USDT", "XVG", "ETC"], list(portfolio.Balance.currencies()))
341 375
342 @mock.patch.object(portfolio.Portfolio, "repartition_pertenthousand") 376 @mock.patch.object(portfolio.Portfolio, "repartition")
343 @mock.patch.object(portfolio.market, "fetch_balance") 377 @mock.patch.object(portfolio.market, "fetch_balance")
344 def test_dispatch_assets(self, fetch_balance, repartition): 378 def test_dispatch_assets(self, fetch_balance, repartition):
345 fetch_balance.return_value = self.fetch_balance 379 fetch_balance.return_value = self.fetch_balance
@@ -348,8 +382,8 @@ class BalanceTest(unittest.TestCase):
348 self.assertNotIn("XEM", portfolio.Balance.currencies()) 382 self.assertNotIn("XEM", portfolio.Balance.currencies())
349 383
350 repartition.return_value = { 384 repartition.return_value = {
351 "XEM": 7500, 385 "XEM": (D("0.75"), "long"),
352 "BTC": 2600, 386 "BTC": (D("0.26"), "long"),
353 } 387 }
354 388
355 amounts = portfolio.Balance.dispatch_assets(portfolio.Amount("BTC", "10.1")) 389 amounts = portfolio.Balance.dispatch_assets(portfolio.Amount("BTC", "10.1"))
@@ -357,13 +391,13 @@ class BalanceTest(unittest.TestCase):
357 self.assertEqual(D("2.6"), amounts["BTC"].value) 391 self.assertEqual(D("2.6"), amounts["BTC"].value)
358 self.assertEqual(D("7.5"), amounts["XEM"].value) 392 self.assertEqual(D("7.5"), amounts["XEM"].value)
359 393
360 @mock.patch.object(portfolio.Portfolio, "repartition_pertenthousand") 394 @mock.patch.object(portfolio.Portfolio, "repartition")
361 @mock.patch.object(portfolio.Trade, "get_ticker") 395 @mock.patch.object(portfolio.Trade, "get_ticker")
362 @mock.patch.object(portfolio.Trade, "compute_trades") 396 @mock.patch.object(portfolio.Trade, "compute_trades")
363 def test_prepare_trades(self, compute_trades, get_ticker, repartition): 397 def test_prepare_trades(self, compute_trades, get_ticker, repartition):
364 repartition.return_value = { 398 repartition.return_value = {
365 "XEM": 7500, 399 "XEM": (D("0.75"), "long"),
366 "BTC": 2500, 400 "BTC": (D("0.25"), "long"),
367 } 401 }
368 def _get_ticker(c1, c2, market): 402 def _get_ticker(c1, c2, market):
369 if c1 == "USDT" and c2 == "BTC": 403 if c1 == "USDT" and c2 == "BTC":
@@ -587,11 +621,12 @@ class AcceptanceTest(unittest.TestCase):
587 }, 621 },
588 } 622 }
589 repartition = { 623 repartition = {
590 "ETH": 2500, 624 "ETH": (D("0.25"), "long"),
591 "ETC": 2500, 625 "ETC": (D("0.25"), "long"),
592 "BTC": 4000, 626 "BTC": (D("0.4"), "long"),
593 "BTD": 500, 627 "BTD": (D("0.01"), "short"),
594 "USDT": 500, 628 "B2X": (D("0.04"), "long"),
629 "USDT": (D("0.05"), "long"),
595 } 630 }
596 631
597 def fetch_ticker(symbol): 632 def fetch_ticker(symbol):
@@ -619,6 +654,12 @@ class AcceptanceTest(unittest.TestCase):
619 "bid": D("0.0008"), 654 "bid": D("0.0008"),
620 "ask": D("0.0012") 655 "ask": D("0.0012")
621 } 656 }
657 if symbol == "B2X/BTC":
658 return {
659 "symbol": "B2X/BTC",
660 "bid": D("0.0008"),
661 "ask": D("0.0012")
662 }
622 if symbol == "USDT/BTC": 663 if symbol == "USDT/BTC":
623 raise portfolio.ExchangeError 664 raise portfolio.ExchangeError
624 if symbol == "BTC/USDT": 665 if symbol == "BTC/USDT":
@@ -632,7 +673,7 @@ class AcceptanceTest(unittest.TestCase):
632 market = mock.Mock() 673 market = mock.Mock()
633 market.fetch_balance.return_value = fetch_balance 674 market.fetch_balance.return_value = fetch_balance
634 market.fetch_ticker.side_effect = fetch_ticker 675 market.fetch_ticker.side_effect = fetch_ticker
635 with mock.patch.object(portfolio.Portfolio, "repartition_pertenthousand", return_value=repartition): 676 with mock.patch.object(portfolio.Portfolio, "repartition", return_value=repartition):
636 # Action 1 677 # Action 1
637 portfolio.Balance.prepare_trades(market) 678 portfolio.Balance.prepare_trades(market)
638 679
@@ -654,9 +695,13 @@ class AcceptanceTest(unittest.TestCase):
654 self.assertNotIn("BTC", trades) 695 self.assertNotIn("BTC", trades)
655 696
656 self.assertEqual(portfolio.Amount("BTC", D("0.00")), trades["BTD"].value_from) 697 self.assertEqual(portfolio.Amount("BTC", D("0.00")), trades["BTD"].value_from)
657 self.assertEqual(portfolio.Amount("BTC", D("0.01")), trades["BTD"].value_to) 698 self.assertEqual(portfolio.Amount("BTC", D("0.002")), trades["BTD"].value_to)
658 self.assertEqual("buy", trades["BTD"].action) 699 self.assertEqual("buy", trades["BTD"].action)
659 700
701 self.assertEqual(portfolio.Amount("BTC", D("0.00")), trades["B2X"].value_from)
702 self.assertEqual(portfolio.Amount("BTC", D("0.008")), trades["B2X"].value_to)
703 self.assertEqual("buy", trades["B2X"].action)
704
660 self.assertEqual(portfolio.Amount("BTC", D("0.00")), trades["USDT"].value_from) 705 self.assertEqual(portfolio.Amount("BTC", D("0.00")), trades["USDT"].value_from)
661 self.assertEqual(portfolio.Amount("BTC", D("0.01")), trades["USDT"].value_to) 706 self.assertEqual(portfolio.Amount("BTC", D("0.01")), trades["USDT"].value_to)
662 self.assertEqual("buy", trades["USDT"].action) 707 self.assertEqual("buy", trades["USDT"].action)
@@ -680,7 +725,7 @@ class AcceptanceTest(unittest.TestCase):
680 self.assertEqual("limit", type) 725 self.assertEqual("limit", type)
681 if symbol == "ETH/BTC": 726 if symbol == "ETH/BTC":
682 self.assertEqual("sell", action) 727 self.assertEqual("sell", action)
683 self.assertEqual(2, 3*amount) 728 self.assertEqual(D('0.66666666'), amount)
684 self.assertEqual(D("0.14014"), price) 729 self.assertEqual(D("0.14014"), price)
685 elif symbol == "XVG/BTC": 730 elif symbol == "XVG/BTC":
686 self.assertEqual("sell", action) 731 self.assertEqual("sell", action)
@@ -693,6 +738,7 @@ class AcceptanceTest(unittest.TestCase):
693 "id": symbol, 738 "id": symbol,
694 } 739 }
695 market.create_order.side_effect = create_order 740 market.create_order.side_effect = create_order
741 market.order_precision.return_value = 8
696 742
697 # Action 3 743 # Action 3
698 portfolio.Trade.run_orders() 744 portfolio.Trade.run_orders()
@@ -734,7 +780,7 @@ class AcceptanceTest(unittest.TestCase):
734 } 780 }
735 market.fetch_balance.return_value = fetch_balance 781 market.fetch_balance.return_value = fetch_balance
736 782
737 with mock.patch.object(portfolio.Portfolio, "repartition_pertenthousand", return_value=repartition): 783 with mock.patch.object(portfolio.Portfolio, "repartition", return_value=repartition):
738 # Action 5 784 # Action 5
739 portfolio.Balance.update_trades(market, only="buy", compute_value="average") 785 portfolio.Balance.update_trades(market, only="buy", compute_value="average")
740 786
@@ -757,9 +803,13 @@ class AcceptanceTest(unittest.TestCase):
757 self.assertNotIn("BTC", trades) 803 self.assertNotIn("BTC", trades)
758 804
759 self.assertEqual(portfolio.Amount("BTC", D("0.00")), trades["BTD"].value_from) 805 self.assertEqual(portfolio.Amount("BTC", D("0.00")), trades["BTD"].value_from)
760 self.assertEqual(portfolio.Amount("BTC", D("0.0097")), trades["BTD"].value_to) 806 self.assertEqual(portfolio.Amount("BTC", D("0.00194")), trades["BTD"].value_to)
761 self.assertEqual("buy", trades["BTD"].action) 807 self.assertEqual("buy", trades["BTD"].action)
762 808
809 self.assertEqual(portfolio.Amount("BTC", D("0.00")), trades["B2X"].value_from)
810 self.assertEqual(portfolio.Amount("BTC", D("0.00776")), trades["B2X"].value_to)
811 self.assertEqual("buy", trades["B2X"].action)
812
763 self.assertEqual(portfolio.Amount("BTC", D("0.00")), trades["USDT"].value_from) 813 self.assertEqual(portfolio.Amount("BTC", D("0.00")), trades["USDT"].value_from)
764 self.assertEqual(portfolio.Amount("BTC", D("0.0097")), trades["USDT"].value_to) 814 self.assertEqual(portfolio.Amount("BTC", D("0.0097")), trades["USDT"].value_to)
765 self.assertEqual("buy", trades["USDT"].action) 815 self.assertEqual("buy", trades["USDT"].action)
@@ -772,21 +822,34 @@ class AcceptanceTest(unittest.TestCase):
772 portfolio.Trade.prepare_orders(only="buy", compute_value=lambda x, y: x["ask"]) 822 portfolio.Trade.prepare_orders(only="buy", compute_value=lambda x, y: x["ask"])
773 823
774 all_orders = portfolio.Trade.all_orders(state="pending") 824 all_orders = portfolio.Trade.all_orders(state="pending")
775 self.assertEqual(3, len(all_orders)) 825 self.assertEqual(4, len(all_orders))
776 self.assertEqual(portfolio.Amount("ETC", D("38.5")/3), all_orders[0].amount) 826 self.assertEqual(portfolio.Amount("ETC", D("12.83333333")), round(all_orders[0].amount))
777 self.assertEqual(D("0.003"), all_orders[0].rate) 827 self.assertEqual(D("0.003"), all_orders[0].rate)
778 self.assertEqual("buy", all_orders[0].action) 828 self.assertEqual("buy", all_orders[0].action)
829 self.assertEqual("long", all_orders[0].trade_type)
779 830
780 self.assertEqual(portfolio.Amount("BTD", D("24.25")/3), all_orders[1].amount) 831 self.assertEqual(portfolio.Amount("BTD", D("1.61666666")), round(all_orders[1].amount))
781 self.assertEqual(D("0.0012"), all_orders[1].rate) 832 self.assertEqual(D("0.0012"), all_orders[1].rate)
782 self.assertEqual("buy", all_orders[1].action) 833 self.assertEqual("sell", all_orders[1].action)
834 self.assertEqual("short", all_orders[1].trade_type)
835
836 diff = portfolio.Amount("B2X", D("19.4")/3) - all_orders[2].amount
837 self.assertAlmostEqual(0, diff.value)
838 self.assertEqual(D("0.0012"), all_orders[2].rate)
839 self.assertEqual("buy", all_orders[2].action)
840 self.assertEqual("long", all_orders[2].trade_type)
841
842 self.assertEqual(portfolio.Amount("BTC", D("0.0097")), all_orders[3].amount)
843 self.assertEqual(D("16000"), all_orders[3].rate)
844 self.assertEqual("sell", all_orders[3].action)
845 self.assertEqual("long", all_orders[3].trade_type)
783 846
784 self.assertEqual(portfolio.Amount("BTC", D("0.0097")), all_orders[2].amount) 847 # Action 7
785 self.assertEqual(D("16000"), all_orders[2].rate) 848 # TODO
786 self.assertEqual("sell", all_orders[2].action) 849 # portfolio.Trade.run_orders()
787 850
788 with mock.patch.object(portfolio.time, "sleep") as sleep: 851 with mock.patch.object(portfolio.time, "sleep") as sleep:
789 # Action 7 852 # Action 8
790 portfolio.Trade.follow_orders(verbose=False) 853 portfolio.Trade.follow_orders(verbose=False)
791 854
792 sleep.assert_called_with(30) 855 sleep.assert_called_with(30)