diff options
-rw-r--r-- | portfolio.py | 79 | ||||
-rw-r--r-- | test.py | 501 |
2 files changed, 572 insertions, 8 deletions
diff --git a/portfolio.py b/portfolio.py index 69e3755..9c58676 100644 --- a/portfolio.py +++ b/portfolio.py | |||
@@ -1,9 +1,7 @@ | |||
1 | from datetime import datetime | 1 | from datetime import datetime |
2 | from decimal import Decimal as D, ROUND_DOWN | ||
3 | from ccxt import ExchangeError, InsufficientFunds, ExchangeNotAvailable, InvalidOrder, OrderNotCached, OrderNotFound | ||
4 | from retry import retry | 2 | from retry import retry |
5 | 3 | from decimal import Decimal as D, ROUND_DOWN | |
6 | # FIXME: correctly handle web call timeouts | 4 | from ccxt import ExchangeError, InsufficientFunds, ExchangeNotAvailable, InvalidOrder, OrderNotCached, OrderNotFound, RequestTimeout |
7 | 5 | ||
8 | class Computation: | 6 | class Computation: |
9 | computations = { | 7 | computations = { |
@@ -414,6 +412,9 @@ class Trade: | |||
414 | for mouvement in order.mouvements: | 412 | for mouvement in order.mouvements: |
415 | self.market.report.print_log("{}\t\t{}".format(ind, mouvement)) | 413 | self.market.report.print_log("{}\t\t{}".format(ind, mouvement)) |
416 | 414 | ||
415 | class RetryException(Exception): | ||
416 | pass | ||
417 | |||
417 | class Order: | 418 | class Order: |
418 | def __init__(self, action, amount, rate, base_currency, trade_type, market, | 419 | def __init__(self, action, amount, rate, base_currency, trade_type, market, |
419 | trade, close_if_possible=False): | 420 | trade, close_if_possible=False): |
@@ -430,6 +431,7 @@ class Order: | |||
430 | self.close_if_possible = close_if_possible | 431 | self.close_if_possible = close_if_possible |
431 | self.id = None | 432 | self.id = None |
432 | self.tries = 0 | 433 | self.tries = 0 |
434 | self.start_date = None | ||
433 | 435 | ||
434 | def as_json(self): | 436 | def as_json(self): |
435 | return { | 437 | return { |
@@ -475,18 +477,18 @@ class Order: | |||
475 | def finished(self): | 477 | def finished(self): |
476 | return self.status.startswith("closed") or self.status == "canceled" or self.status == "error" | 478 | return self.status.startswith("closed") or self.status == "canceled" or self.status == "error" |
477 | 479 | ||
478 | @retry(InsufficientFunds) | 480 | @retry((InsufficientFunds, RetryException)) |
479 | def run(self): | 481 | def run(self): |
480 | self.tries += 1 | 482 | self.tries += 1 |
481 | symbol = "{}/{}".format(self.amount.currency, self.base_currency) | 483 | symbol = "{}/{}".format(self.amount.currency, self.base_currency) |
482 | amount = round(self.amount, self.market.ccxt.order_precision(symbol)).value | 484 | amount = round(self.amount, self.market.ccxt.order_precision(symbol)).value |
483 | 485 | ||
486 | action = "market.ccxt.create_order('{}', 'limit', '{}', {}, price={}, account={})".format(symbol, self.action, amount, self.rate, self.account) | ||
484 | if self.market.debug: | 487 | if self.market.debug: |
485 | self.market.report.log_debug_action("market.ccxt.create_order('{}', 'limit', '{}', {}, price={}, account={})".format( | 488 | self.market.report.log_debug_action(action) |
486 | symbol, self.action, amount, self.rate, self.account)) | ||
487 | self.results.append({"debug": True, "id": -1}) | 489 | self.results.append({"debug": True, "id": -1}) |
488 | else: | 490 | else: |
489 | action = "market.ccxt.create_order('{}', 'limit', '{}', {}, price={}, account={})".format(symbol, self.action, amount, self.rate, self.account) | 491 | self.start_date = datetime.now() |
490 | try: | 492 | try: |
491 | self.results.append(self.market.ccxt.create_order(symbol, 'limit', self.action, amount, price=self.rate, account=self.account)) | 493 | self.results.append(self.market.ccxt.create_order(symbol, 'limit', self.action, amount, price=self.rate, account=self.account)) |
492 | except InvalidOrder: | 494 | except InvalidOrder: |
@@ -494,6 +496,19 @@ class Order: | |||
494 | self.status = "closed" | 496 | self.status = "closed" |
495 | self.mark_finished_order() | 497 | self.mark_finished_order() |
496 | return | 498 | return |
499 | except RequestTimeout as e: | ||
500 | if not self.retrieve_order(): | ||
501 | if self.tries < 5: | ||
502 | self.market.report.log_error(action, message="Retrying after timeout", exception=e) | ||
503 | # We make a specific call in case retrieve_order | ||
504 | # would raise itself | ||
505 | raise RetryException | ||
506 | else: | ||
507 | self.market.report.log_error(action, message="Giving up {} after timeouts".format(self), exception=e) | ||
508 | self.status = "error" | ||
509 | return | ||
510 | else: | ||
511 | self.market.report.log_error(action, message="Timeout, found the order") | ||
497 | except InsufficientFunds as e: | 512 | except InsufficientFunds as e: |
498 | if self.tries < 5: | 513 | if self.tries < 5: |
499 | self.market.report.log_error(action, message="Retrying with reduced amount", exception=e) | 514 | self.market.report.log_error(action, message="Retrying with reduced amount", exception=e) |
@@ -585,6 +600,54 @@ class Order: | |||
585 | self.market.report.log_error("cancel_order", message="Already cancelled order", exception=e) | 600 | self.market.report.log_error("cancel_order", message="Already cancelled order", exception=e) |
586 | self.fetch() | 601 | self.fetch() |
587 | 602 | ||
603 | def retrieve_order(self): | ||
604 | symbol = "{}/{}".format(self.amount.currency, self.base_currency) | ||
605 | amount = round(self.amount, self.market.ccxt.order_precision(symbol)).value | ||
606 | start_timestamp = self.start_date.timestamp() - 5 | ||
607 | |||
608 | similar_open_orders = self.market.ccxt.fetch_orders(symbol=symbol, since=start_timestamp) | ||
609 | for order in similar_open_orders: | ||
610 | if (order["info"]["margin"] == 1 and self.account == "exchange") or\ | ||
611 | (order["info"]["margin"] != 1 and self.account == "margin"): | ||
612 | i_m_tested = True # coverage bug ?! | ||
613 | continue | ||
614 | if order["info"]["side"] != self.action: | ||
615 | continue | ||
616 | amount_diff = round( | ||
617 | abs(D(order["info"]["startingAmount"]) - amount), | ||
618 | self.market.ccxt.order_precision(symbol)) | ||
619 | rate_diff = round( | ||
620 | abs(D(order["info"]["rate"]) - self.rate), | ||
621 | self.market.ccxt.order_precision(symbol)) | ||
622 | if amount_diff != 0 or rate_diff != 0: | ||
623 | continue | ||
624 | self.results.append({"id": order["id"]}) | ||
625 | return True | ||
626 | |||
627 | similar_trades = self.market.ccxt.fetch_my_trades(symbol=symbol, since=start_timestamp) | ||
628 | # FIXME: use set instead of sorted(list(...)) | ||
629 | for order_id in sorted(list(map(lambda x: x["order"], similar_trades))): | ||
630 | trades = list(filter(lambda x: x["order"] == order_id, similar_trades)) | ||
631 | if any(x["timestamp"] < start_timestamp for x in trades): | ||
632 | continue | ||
633 | if any(x["side"] != self.action for x in trades): | ||
634 | continue | ||
635 | if any(x["info"]["category"] == "exchange" and self.account == "margin" for x in trades) or\ | ||
636 | any(x["info"]["category"] == "marginTrade" and self.account == "exchange" for x in trades): | ||
637 | continue | ||
638 | trade_sum = sum(D(x["info"]["amount"]) for x in trades) | ||
639 | amount_diff = round(abs(trade_sum - amount), | ||
640 | self.market.ccxt.order_precision(symbol)) | ||
641 | if amount_diff != 0: | ||
642 | continue | ||
643 | if (self.action == "sell" and any(D(x["info"]["rate"]) < self.rate for x in trades)) or\ | ||
644 | (self.action == "buy" and any(D(x["info"]["rate"]) > self.rate for x in trades)): | ||
645 | continue | ||
646 | self.results.append({"id": order_id}) | ||
647 | return True | ||
648 | |||
649 | return False | ||
650 | |||
588 | class Mouvement: | 651 | class Mouvement: |
589 | def __init__(self, currency, base_currency, hash_): | 652 | def __init__(self, currency, base_currency, hash_): |
590 | self.currency = currency | 653 | self.currency = currency |
@@ -3213,6 +3213,507 @@ class OrderTest(WebMockTestCase): | |||
3213 | self.assertEqual(5, self.m.report.log_error.call_count) | 3213 | self.assertEqual(5, self.m.report.log_error.call_count) |
3214 | self.m.report.log_error.assert_called_with(mock.ANY, message="Giving up Order(buy long 0.00096060 ETH at 0.1 BTC [pending])", exception=mock.ANY) | 3214 | self.m.report.log_error.assert_called_with(mock.ANY, message="Giving up Order(buy long 0.00096060 ETH at 0.1 BTC [pending])", exception=mock.ANY) |
3215 | 3215 | ||
3216 | self.m.reset_mock() | ||
3217 | with self.subTest(request_timeout=True): | ||
3218 | order = portfolio.Order("buy", portfolio.Amount("ETH", "0.001"), | ||
3219 | D("0.1"), "BTC", "long", self.m, "trade") | ||
3220 | with self.subTest(retrieved=False), \ | ||
3221 | mock.patch.object(order, "retrieve_order") as retrieve: | ||
3222 | self.m.ccxt.create_order.side_effect = [ | ||
3223 | portfolio.RequestTimeout, | ||
3224 | portfolio.RequestTimeout, | ||
3225 | { "id": 123 }, | ||
3226 | ] | ||
3227 | retrieve.return_value = False | ||
3228 | order.run() | ||
3229 | self.m.ccxt.create_order.assert_has_calls([ | ||
3230 | mock.call('ETH/BTC', 'limit', 'buy', D('0.0010'), account='exchange', price=D('0.1')), | ||
3231 | mock.call('ETH/BTC', 'limit', 'buy', D('0.0010'), account='exchange', price=D('0.1')), | ||
3232 | mock.call('ETH/BTC', 'limit', 'buy', D('0.0010'), account='exchange', price=D('0.1')), | ||
3233 | ]) | ||
3234 | self.assertEqual(3, self.m.ccxt.create_order.call_count) | ||
3235 | self.assertEqual(3, order.tries) | ||
3236 | self.m.report.log_error.assert_called() | ||
3237 | self.assertEqual(2, self.m.report.log_error.call_count) | ||
3238 | self.m.report.log_error.assert_called_with(mock.ANY, message="Retrying after timeout", exception=mock.ANY) | ||
3239 | self.assertEqual(123, order.id) | ||
3240 | |||
3241 | self.m.reset_mock() | ||
3242 | order = portfolio.Order("buy", portfolio.Amount("ETH", "0.001"), | ||
3243 | D("0.1"), "BTC", "long", self.m, "trade") | ||
3244 | with self.subTest(retrieved=True), \ | ||
3245 | mock.patch.object(order, "retrieve_order") as retrieve: | ||
3246 | self.m.ccxt.create_order.side_effect = [ | ||
3247 | portfolio.RequestTimeout, | ||
3248 | ] | ||
3249 | def _retrieve(): | ||
3250 | order.results.append({"id": 123}) | ||
3251 | return True | ||
3252 | retrieve.side_effect = _retrieve | ||
3253 | order.run() | ||
3254 | self.m.ccxt.create_order.assert_has_calls([ | ||
3255 | mock.call('ETH/BTC', 'limit', 'buy', D('0.0010'), account='exchange', price=D('0.1')), | ||
3256 | ]) | ||
3257 | self.assertEqual(1, self.m.ccxt.create_order.call_count) | ||
3258 | self.assertEqual(1, order.tries) | ||
3259 | self.m.report.log_error.assert_called() | ||
3260 | self.assertEqual(1, self.m.report.log_error.call_count) | ||
3261 | self.m.report.log_error.assert_called_with(mock.ANY, message="Timeout, found the order") | ||
3262 | self.assertEqual(123, order.id) | ||
3263 | |||
3264 | self.m.reset_mock() | ||
3265 | order = portfolio.Order("buy", portfolio.Amount("ETH", "0.001"), | ||
3266 | D("0.1"), "BTC", "long", self.m, "trade") | ||
3267 | with self.subTest(retrieved=False), \ | ||
3268 | mock.patch.object(order, "retrieve_order") as retrieve: | ||
3269 | self.m.ccxt.create_order.side_effect = [ | ||
3270 | portfolio.RequestTimeout, | ||
3271 | portfolio.RequestTimeout, | ||
3272 | portfolio.RequestTimeout, | ||
3273 | portfolio.RequestTimeout, | ||
3274 | portfolio.RequestTimeout, | ||
3275 | ] | ||
3276 | retrieve.return_value = False | ||
3277 | order.run() | ||
3278 | self.m.ccxt.create_order.assert_has_calls([ | ||
3279 | mock.call('ETH/BTC', 'limit', 'buy', D('0.0010'), account='exchange', price=D('0.1')), | ||
3280 | mock.call('ETH/BTC', 'limit', 'buy', D('0.0010'), account='exchange', price=D('0.1')), | ||
3281 | mock.call('ETH/BTC', 'limit', 'buy', D('0.0010'), account='exchange', price=D('0.1')), | ||
3282 | mock.call('ETH/BTC', 'limit', 'buy', D('0.0010'), account='exchange', price=D('0.1')), | ||
3283 | mock.call('ETH/BTC', 'limit', 'buy', D('0.0010'), account='exchange', price=D('0.1')), | ||
3284 | ]) | ||
3285 | self.assertEqual(5, self.m.ccxt.create_order.call_count) | ||
3286 | self.assertEqual(5, order.tries) | ||
3287 | self.m.report.log_error.assert_called() | ||
3288 | self.assertEqual(5, self.m.report.log_error.call_count) | ||
3289 | self.m.report.log_error.assert_called_with(mock.ANY, message="Giving up Order(buy long 0.00100000 ETH at 0.1 BTC [pending]) after timeouts", exception=mock.ANY) | ||
3290 | self.assertEqual("error", order.status) | ||
3291 | |||
3292 | def test_retrieve_order(self): | ||
3293 | with self.subTest(similar_open_order=True): | ||
3294 | order = portfolio.Order("buy", portfolio.Amount("ETH", "0.001"), | ||
3295 | D("0.1"), "BTC", "long", self.m, "trade") | ||
3296 | order.start_date = datetime.datetime(2018, 3, 25, 15, 15, 55) | ||
3297 | |||
3298 | self.m.ccxt.order_precision.return_value = 8 | ||
3299 | self.m.ccxt.fetch_orders.return_value = [ | ||
3300 | { # Wrong amount | ||
3301 | 'amount': 0.002, 'cost': 0.1, | ||
3302 | 'datetime': '2018-03-25T15:15:51.000Z', | ||
3303 | 'fee': None, 'filled': 0.0, | ||
3304 | 'id': '1', | ||
3305 | 'info': { | ||
3306 | 'amount': '0.002', | ||
3307 | 'date': '2018-03-25 15:15:51', | ||
3308 | 'margin': 0, 'orderNumber': '1', | ||
3309 | 'price': '0.1', 'rate': '0.1', | ||
3310 | 'side': 'buy', 'startingAmount': '0.002', | ||
3311 | 'status': 'open', 'total': '0.0002', | ||
3312 | 'type': 'limit' | ||
3313 | }, | ||
3314 | 'price': 0.1, 'remaining': 0.002, 'side': 'buy', | ||
3315 | 'status': 'open', 'symbol': 'ETH/BTC', | ||
3316 | 'timestamp': 1521990951000, 'trades': None, | ||
3317 | 'type': 'limit' | ||
3318 | }, | ||
3319 | { # Margin | ||
3320 | 'amount': 0.001, 'cost': 0.1, | ||
3321 | 'datetime': '2018-03-25T15:15:51.000Z', | ||
3322 | 'fee': None, 'filled': 0.0, | ||
3323 | 'id': '2', | ||
3324 | 'info': { | ||
3325 | 'amount': '0.001', | ||
3326 | 'date': '2018-03-25 15:15:51', | ||
3327 | 'margin': 1, 'orderNumber': '2', | ||
3328 | 'price': '0.1', 'rate': '0.1', | ||
3329 | 'side': 'buy', 'startingAmount': '0.001', | ||
3330 | 'status': 'open', 'total': '0.0001', | ||
3331 | 'type': 'limit' | ||
3332 | }, | ||
3333 | 'price': 0.1, 'remaining': 0.001, 'side': 'buy', | ||
3334 | 'status': 'open', 'symbol': 'ETH/BTC', | ||
3335 | 'timestamp': 1521990951000, 'trades': None, | ||
3336 | 'type': 'limit' | ||
3337 | }, | ||
3338 | { # selling | ||
3339 | 'amount': 0.001, 'cost': 0.1, | ||
3340 | 'datetime': '2018-03-25T15:15:51.000Z', | ||
3341 | 'fee': None, 'filled': 0.0, | ||
3342 | 'id': '3', | ||
3343 | 'info': { | ||
3344 | 'amount': '0.001', | ||
3345 | 'date': '2018-03-25 15:15:51', | ||
3346 | 'margin': 0, 'orderNumber': '3', | ||
3347 | 'price': '0.1', 'rate': '0.1', | ||
3348 | 'side': 'sell', 'startingAmount': '0.001', | ||
3349 | 'status': 'open', 'total': '0.0001', | ||
3350 | 'type': 'limit' | ||
3351 | }, | ||
3352 | 'price': 0.1, 'remaining': 0.001, 'side': 'sell', | ||
3353 | 'status': 'open', 'symbol': 'ETH/BTC', | ||
3354 | 'timestamp': 1521990951000, 'trades': None, | ||
3355 | 'type': 'limit' | ||
3356 | }, | ||
3357 | { # Wrong rate | ||
3358 | 'amount': 0.001, 'cost': 0.15, | ||
3359 | 'datetime': '2018-03-25T15:15:51.000Z', | ||
3360 | 'fee': None, 'filled': 0.0, | ||
3361 | 'id': '4', | ||
3362 | 'info': { | ||
3363 | 'amount': '0.001', | ||
3364 | 'date': '2018-03-25 15:15:51', | ||
3365 | 'margin': 0, 'orderNumber': '4', | ||
3366 | 'price': '0.15', 'rate': '0.15', | ||
3367 | 'side': 'buy', 'startingAmount': '0.001', | ||
3368 | 'status': 'open', 'total': '0.0001', | ||
3369 | 'type': 'limit' | ||
3370 | }, | ||
3371 | 'price': 0.15, 'remaining': 0.001, 'side': 'buy', | ||
3372 | 'status': 'open', 'symbol': 'ETH/BTC', | ||
3373 | 'timestamp': 1521990951000, 'trades': None, | ||
3374 | 'type': 'limit' | ||
3375 | }, | ||
3376 | { # All good | ||
3377 | 'amount': 0.001, 'cost': 0.1, | ||
3378 | 'datetime': '2018-03-25T15:15:51.000Z', | ||
3379 | 'fee': None, 'filled': 0.0, | ||
3380 | 'id': '5', | ||
3381 | 'info': { | ||
3382 | 'amount': '0.001', | ||
3383 | 'date': '2018-03-25 15:15:51', | ||
3384 | 'margin': 0, 'orderNumber': '1', | ||
3385 | 'price': '0.1', 'rate': '0.1', | ||
3386 | 'side': 'buy', 'startingAmount': '0.001', | ||
3387 | 'status': 'open', 'total': '0.0001', | ||
3388 | 'type': 'limit' | ||
3389 | }, | ||
3390 | 'price': 0.1, 'remaining': 0.001, 'side': 'buy', | ||
3391 | 'status': 'open', 'symbol': 'ETH/BTC', | ||
3392 | 'timestamp': 1521990951000, 'trades': None, | ||
3393 | 'type': 'limit' | ||
3394 | } | ||
3395 | ] | ||
3396 | result = order.retrieve_order() | ||
3397 | self.assertTrue(result) | ||
3398 | self.assertEqual('5', order.results[0]["id"]) | ||
3399 | self.m.ccxt.fetch_my_trades.assert_not_called() | ||
3400 | self.m.ccxt.fetch_orders.assert_called_once_with(symbol="ETH/BTC", since=1521983750) | ||
3401 | |||
3402 | self.m.reset_mock() | ||
3403 | with self.subTest(similar_open_order=False, past_trades=True): | ||
3404 | order = portfolio.Order("buy", portfolio.Amount("ETH", "0.001"), | ||
3405 | D("0.1"), "BTC", "long", self.m, "trade") | ||
3406 | order.start_date = datetime.datetime(2018, 3, 25, 15, 15, 55) | ||
3407 | |||
3408 | self.m.ccxt.order_precision.return_value = 8 | ||
3409 | self.m.ccxt.fetch_orders.return_value = [] | ||
3410 | self.m.ccxt.fetch_my_trades.return_value = [ | ||
3411 | { # Wrong timestamp 1 | ||
3412 | 'amount': 0.0006, | ||
3413 | 'cost': 0.00006, | ||
3414 | 'datetime': '2018-03-25T15:15:14.000Z', | ||
3415 | 'id': '1-1', | ||
3416 | 'info': { | ||
3417 | 'amount': '0.0006', | ||
3418 | 'category': 'exchange', | ||
3419 | 'date': '2018-03-25 15:15:14', | ||
3420 | 'fee': '0.00150000', | ||
3421 | 'globalTradeID': 1, | ||
3422 | 'orderNumber': '1', | ||
3423 | 'rate': '0.1', | ||
3424 | 'total': '0.00006', | ||
3425 | 'tradeID': '1-1', | ||
3426 | 'type': 'buy' | ||
3427 | }, | ||
3428 | 'order': '1', | ||
3429 | 'price': 0.1, | ||
3430 | 'side': 'buy', | ||
3431 | 'symbol': 'ETH/BTC', | ||
3432 | 'timestamp': 1521983714, | ||
3433 | 'type': 'limit' | ||
3434 | }, | ||
3435 | { # Wrong timestamp 2 | ||
3436 | 'amount': 0.0004, | ||
3437 | 'cost': 0.00004, | ||
3438 | 'datetime': '2018-03-25T15:16:54.000Z', | ||
3439 | 'id': '1-2', | ||
3440 | 'info': { | ||
3441 | 'amount': '0.0004', | ||
3442 | 'category': 'exchange', | ||
3443 | 'date': '2018-03-25 15:16:54', | ||
3444 | 'fee': '0.00150000', | ||
3445 | 'globalTradeID': 2, | ||
3446 | 'orderNumber': '1', | ||
3447 | 'rate': '0.1', | ||
3448 | 'total': '0.00004', | ||
3449 | 'tradeID': '1-2', | ||
3450 | 'type': 'buy' | ||
3451 | }, | ||
3452 | 'order': '1', | ||
3453 | 'price': 0.1, | ||
3454 | 'side': 'buy', | ||
3455 | 'symbol': 'ETH/BTC', | ||
3456 | 'timestamp': 1521983814, | ||
3457 | 'type': 'limit' | ||
3458 | }, | ||
3459 | { # Wrong side 1 | ||
3460 | 'amount': 0.0006, | ||
3461 | 'cost': 0.00006, | ||
3462 | 'datetime': '2018-03-25T15:15:54.000Z', | ||
3463 | 'id': '2-1', | ||
3464 | 'info': { | ||
3465 | 'amount': '0.0006', | ||
3466 | 'category': 'exchange', | ||
3467 | 'date': '2018-03-25 15:15:54', | ||
3468 | 'fee': '0.00150000', | ||
3469 | 'globalTradeID': 1, | ||
3470 | 'orderNumber': '2', | ||
3471 | 'rate': '0.1', | ||
3472 | 'total': '0.00006', | ||
3473 | 'tradeID': '2-1', | ||
3474 | 'type': 'sell' | ||
3475 | }, | ||
3476 | 'order': '2', | ||
3477 | 'price': 0.1, | ||
3478 | 'side': 'sell', | ||
3479 | 'symbol': 'ETH/BTC', | ||
3480 | 'timestamp': 1521983754, | ||
3481 | 'type': 'limit' | ||
3482 | }, | ||
3483 | { # Wrong side 2 | ||
3484 | 'amount': 0.0004, | ||
3485 | 'cost': 0.00004, | ||
3486 | 'datetime': '2018-03-25T15:16:54.000Z', | ||
3487 | 'id': '2-2', | ||
3488 | 'info': { | ||
3489 | 'amount': '0.0004', | ||
3490 | 'category': 'exchange', | ||
3491 | 'date': '2018-03-25 15:16:54', | ||
3492 | 'fee': '0.00150000', | ||
3493 | 'globalTradeID': 2, | ||
3494 | 'orderNumber': '2', | ||
3495 | 'rate': '0.1', | ||
3496 | 'total': '0.00004', | ||
3497 | 'tradeID': '2-2', | ||
3498 | 'type': 'buy' | ||
3499 | }, | ||
3500 | 'order': '2', | ||
3501 | 'price': 0.1, | ||
3502 | 'side': 'buy', | ||
3503 | 'symbol': 'ETH/BTC', | ||
3504 | 'timestamp': 1521983814, | ||
3505 | 'type': 'limit' | ||
3506 | }, | ||
3507 | { # Margin trade 1 | ||
3508 | 'amount': 0.0006, | ||
3509 | 'cost': 0.00006, | ||
3510 | 'datetime': '2018-03-25T15:15:54.000Z', | ||
3511 | 'id': '3-1', | ||
3512 | 'info': { | ||
3513 | 'amount': '0.0006', | ||
3514 | 'category': 'marginTrade', | ||
3515 | 'date': '2018-03-25 15:15:54', | ||
3516 | 'fee': '0.00150000', | ||
3517 | 'globalTradeID': 1, | ||
3518 | 'orderNumber': '3', | ||
3519 | 'rate': '0.1', | ||
3520 | 'total': '0.00006', | ||
3521 | 'tradeID': '3-1', | ||
3522 | 'type': 'buy' | ||
3523 | }, | ||
3524 | 'order': '3', | ||
3525 | 'price': 0.1, | ||
3526 | 'side': 'buy', | ||
3527 | 'symbol': 'ETH/BTC', | ||
3528 | 'timestamp': 1521983754, | ||
3529 | 'type': 'limit' | ||
3530 | }, | ||
3531 | { # Margin trade 2 | ||
3532 | 'amount': 0.0004, | ||
3533 | 'cost': 0.00004, | ||
3534 | 'datetime': '2018-03-25T15:16:54.000Z', | ||
3535 | 'id': '3-2', | ||
3536 | 'info': { | ||
3537 | 'amount': '0.0004', | ||
3538 | 'category': 'marginTrade', | ||
3539 | 'date': '2018-03-25 15:16:54', | ||
3540 | 'fee': '0.00150000', | ||
3541 | 'globalTradeID': 2, | ||
3542 | 'orderNumber': '3', | ||
3543 | 'rate': '0.1', | ||
3544 | 'total': '0.00004', | ||
3545 | 'tradeID': '3-2', | ||
3546 | 'type': 'buy' | ||
3547 | }, | ||
3548 | 'order': '3', | ||
3549 | 'price': 0.1, | ||
3550 | 'side': 'buy', | ||
3551 | 'symbol': 'ETH/BTC', | ||
3552 | 'timestamp': 1521983814, | ||
3553 | 'type': 'limit' | ||
3554 | }, | ||
3555 | { # Wrong amount 1 | ||
3556 | 'amount': 0.0005, | ||
3557 | 'cost': 0.00005, | ||
3558 | 'datetime': '2018-03-25T15:15:54.000Z', | ||
3559 | 'id': '4-1', | ||
3560 | 'info': { | ||
3561 | 'amount': '0.0005', | ||
3562 | 'category': 'exchange', | ||
3563 | 'date': '2018-03-25 15:15:54', | ||
3564 | 'fee': '0.00150000', | ||
3565 | 'globalTradeID': 1, | ||
3566 | 'orderNumber': '4', | ||
3567 | 'rate': '0.1', | ||
3568 | 'total': '0.00005', | ||
3569 | 'tradeID': '4-1', | ||
3570 | 'type': 'buy' | ||
3571 | }, | ||
3572 | 'order': '4', | ||
3573 | 'price': 0.1, | ||
3574 | 'side': 'buy', | ||
3575 | 'symbol': 'ETH/BTC', | ||
3576 | 'timestamp': 1521983754, | ||
3577 | 'type': 'limit' | ||
3578 | }, | ||
3579 | { # Wrong amount 2 | ||
3580 | 'amount': 0.0004, | ||
3581 | 'cost': 0.00004, | ||
3582 | 'datetime': '2018-03-25T15:16:54.000Z', | ||
3583 | 'id': '4-2', | ||
3584 | 'info': { | ||
3585 | 'amount': '0.0004', | ||
3586 | 'category': 'exchange', | ||
3587 | 'date': '2018-03-25 15:16:54', | ||
3588 | 'fee': '0.00150000', | ||
3589 | 'globalTradeID': 2, | ||
3590 | 'orderNumber': '4', | ||
3591 | 'rate': '0.1', | ||
3592 | 'total': '0.00004', | ||
3593 | 'tradeID': '4-2', | ||
3594 | 'type': 'buy' | ||
3595 | }, | ||
3596 | 'order': '4', | ||
3597 | 'price': 0.1, | ||
3598 | 'side': 'buy', | ||
3599 | 'symbol': 'ETH/BTC', | ||
3600 | 'timestamp': 1521983814, | ||
3601 | 'type': 'limit' | ||
3602 | }, | ||
3603 | { # Wrong price 1 | ||
3604 | 'amount': 0.0006, | ||
3605 | 'cost': 0.000066, | ||
3606 | 'datetime': '2018-03-25T15:15:54.000Z', | ||
3607 | 'id': '5-1', | ||
3608 | 'info': { | ||
3609 | 'amount': '0.0006', | ||
3610 | 'category': 'exchange', | ||
3611 | 'date': '2018-03-25 15:15:54', | ||
3612 | 'fee': '0.00150000', | ||
3613 | 'globalTradeID': 1, | ||
3614 | 'orderNumber': '5', | ||
3615 | 'rate': '0.11', | ||
3616 | 'total': '0.000066', | ||
3617 | 'tradeID': '5-1', | ||
3618 | 'type': 'buy' | ||
3619 | }, | ||
3620 | 'order': '5', | ||
3621 | 'price': 0.11, | ||
3622 | 'side': 'buy', | ||
3623 | 'symbol': 'ETH/BTC', | ||
3624 | 'timestamp': 1521983754, | ||
3625 | 'type': 'limit' | ||
3626 | }, | ||
3627 | { # Wrong price 2 | ||
3628 | 'amount': 0.0004, | ||
3629 | 'cost': 0.00004, | ||
3630 | 'datetime': '2018-03-25T15:16:54.000Z', | ||
3631 | 'id': '5-2', | ||
3632 | 'info': { | ||
3633 | 'amount': '0.0004', | ||
3634 | 'category': 'exchange', | ||
3635 | 'date': '2018-03-25 15:16:54', | ||
3636 | 'fee': '0.00150000', | ||
3637 | 'globalTradeID': 2, | ||
3638 | 'orderNumber': '5', | ||
3639 | 'rate': '0.1', | ||
3640 | 'total': '0.00004', | ||
3641 | 'tradeID': '5-2', | ||
3642 | 'type': 'buy' | ||
3643 | }, | ||
3644 | 'order': '5', | ||
3645 | 'price': 0.1, | ||
3646 | 'side': 'buy', | ||
3647 | 'symbol': 'ETH/BTC', | ||
3648 | 'timestamp': 1521983814, | ||
3649 | 'type': 'limit' | ||
3650 | }, | ||
3651 | { # All good 1 | ||
3652 | 'amount': 0.0006, | ||
3653 | 'cost': 0.00006, | ||
3654 | 'datetime': '2018-03-25T15:15:54.000Z', | ||
3655 | 'id': '7-1', | ||
3656 | 'info': { | ||
3657 | 'amount': '0.0006', | ||
3658 | 'category': 'exchange', | ||
3659 | 'date': '2018-03-25 15:15:54', | ||
3660 | 'fee': '0.00150000', | ||
3661 | 'globalTradeID': 1, | ||
3662 | 'orderNumber': '7', | ||
3663 | 'rate': '0.1', | ||
3664 | 'total': '0.00006', | ||
3665 | 'tradeID': '7-1', | ||
3666 | 'type': 'buy' | ||
3667 | }, | ||
3668 | 'order': '7', | ||
3669 | 'price': 0.1, | ||
3670 | 'side': 'buy', | ||
3671 | 'symbol': 'ETH/BTC', | ||
3672 | 'timestamp': 1521983754, | ||
3673 | 'type': 'limit' | ||
3674 | }, | ||
3675 | { # All good 2 | ||
3676 | 'amount': 0.0004, | ||
3677 | 'cost': 0.000036, | ||
3678 | 'datetime': '2018-03-25T15:16:54.000Z', | ||
3679 | 'id': '7-2', | ||
3680 | 'info': { | ||
3681 | 'amount': '0.0004', | ||
3682 | 'category': 'exchange', | ||
3683 | 'date': '2018-03-25 15:16:54', | ||
3684 | 'fee': '0.00150000', | ||
3685 | 'globalTradeID': 2, | ||
3686 | 'orderNumber': '7', | ||
3687 | 'rate': '0.09', | ||
3688 | 'total': '0.000036', | ||
3689 | 'tradeID': '7-2', | ||
3690 | 'type': 'buy' | ||
3691 | }, | ||
3692 | 'order': '7', | ||
3693 | 'price': 0.09, | ||
3694 | 'side': 'buy', | ||
3695 | 'symbol': 'ETH/BTC', | ||
3696 | 'timestamp': 1521983814, | ||
3697 | 'type': 'limit' | ||
3698 | }, | ||
3699 | ] | ||
3700 | |||
3701 | result = order.retrieve_order() | ||
3702 | self.assertTrue(result) | ||
3703 | self.assertEqual('7', order.results[0]["id"]) | ||
3704 | self.m.ccxt.fetch_orders.assert_called_once_with(symbol="ETH/BTC", since=1521983750) | ||
3705 | |||
3706 | self.m.reset_mock() | ||
3707 | with self.subTest(similar_open_order=False, past_trades=False): | ||
3708 | order = portfolio.Order("buy", portfolio.Amount("ETH", "0.001"), | ||
3709 | D("0.1"), "BTC", "long", self.m, "trade") | ||
3710 | order.start_date = datetime.datetime(2018, 3, 25, 15, 15, 55) | ||
3711 | |||
3712 | self.m.ccxt.order_precision.return_value = 8 | ||
3713 | self.m.ccxt.fetch_orders.return_value = [] | ||
3714 | self.m.ccxt.fetch_my_trades.return_value = [] | ||
3715 | result = order.retrieve_order() | ||
3716 | self.assertFalse(result) | ||
3216 | 3717 | ||
3217 | @unittest.skipUnless("unit" in limits, "Unit skipped") | 3718 | @unittest.skipUnless("unit" in limits, "Unit skipped") |
3218 | class MouvementTest(WebMockTestCase): | 3719 | class MouvementTest(WebMockTestCase): |