diff options
-rw-r--r-- | market.py | 92 | ||||
-rw-r--r-- | portfolio.py | 132 | ||||
-rw-r--r-- | test.py | 135 |
3 files changed, 270 insertions, 89 deletions
@@ -4,6 +4,7 @@ import decimal | |||
4 | def exchange_sum(self, *args): | 4 | def 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))]) |
6 | ccxt.Exchange.sum = exchange_sum | 6 | ccxt.Exchange.sum = exchange_sum |
7 | |||
7 | def poloniex_fetch_balance(self, params={}): | 8 | def 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) |
26 | ccxt.poloniex.fetch_balance = poloniex_fetch_balance | 27 | ccxt.poloniex.fetch_balance = poloniex_fetch_balance |
27 | 28 | ||
29 | def 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 | ||
47 | ccxt.poloniex.fetch_margin_balances = poloniex_fetch_margin_balances | ||
48 | |||
49 | def 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 | ||
59 | ccxt.poloniex.fetch_balance_with_margin = poloniex_fetch_balance_with_margin | ||
60 | |||
61 | |||
28 | def poloniex_fetch_balance_per_type(self): | 62 | def 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) |
129 | ccxt.poloniex.create_margin_order = poloniex_create_margin_order | ||
95 | 130 | ||
96 | def poloniex_create_order(self, symbol, type, side, amount, price=None, account="exchange", lending_rate=None, params={}): | 131 | def 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 | |||
139 | def poloniex_order_precision(self, symbol): | ||
140 | return 8 | ||
141 | |||
103 | ccxt.poloniex.create_exchange_order = ccxt.poloniex.create_order | 142 | ccxt.poloniex.create_exchange_order = ccxt.poloniex.create_order |
104 | ccxt.poloniex.create_order = poloniex_create_order | 143 | ccxt.poloniex.create_order = poloniex_create_order |
144 | ccxt.poloniex.order_precision = poloniex_order_precision | ||
145 | |||
146 | def 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 | ||
154 | ccxt.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 | ||
106 | market = ccxt.poloniex({ | 198 | market = 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 @@ | |||
1 | from ccxt import ExchangeError | 1 | from ccxt import ExchangeError |
2 | import time | 2 | import time |
3 | from decimal import Decimal as D | 3 | from 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 |
5 | from market import market | 5 | from 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 | |||
257 | class Trade: | 274 | class 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 | ||
469 | class Order: | 485 | class 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 | ||
532 | def make_orders(market, base_currency="BTC"): | 558 | def make_orders(market, base_currency="BTC"): |
533 | Balance.prepare_trades(market, base_currency=base_currency) | 559 | Balance.prepare_trades(market, base_currency=base_currency) |
@@ -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) |