]> git.immae.eu Git - perso/Immae/Projets/Cryptomonnaies/Cryptoportfolio/Trader.git/blobdiff - test.py
Merge branch 'dev'
[perso/Immae/Projets/Cryptomonnaies/Cryptoportfolio/Trader.git] / test.py
diff --git a/test.py b/test.py
index c0e6a8aa68aa30c7a06dc42f39164de1d7a6ca63..854e27b1089bf6f985778242a550eca9340a0c25 100644 (file)
--- a/test.py
+++ b/test.py
@@ -7,6 +7,7 @@ from unittest import mock
 import requests
 import requests_mock
 from io import StringIO
+import threading
 import portfolio, market, main, store
 
 limits = ["acceptance", "unit"]
@@ -22,8 +23,12 @@ for test_type in limits:
 class WebMockTestCase(unittest.TestCase):
     import time
 
+    def market_args(self, debug=False, quiet=False, report_path=None, **kwargs):
+        return main.configargparse.Namespace(report_path=report_path,
+                debug=debug, quiet=quiet, **kwargs)
+
     def setUp(self):
-        super(WebMockTestCase, self).setUp()
+        super().setUp()
         self.wm = requests_mock.Mocker()
         self.wm.start()
 
@@ -33,10 +38,14 @@ class WebMockTestCase(unittest.TestCase):
 
         self.patchers = [
                 mock.patch.multiple(market.Portfolio,
-                    last_date=None,
-                    data=None,
-                    liquidities={},
-                    report=mock.Mock()),
+                    data=store.LockedVar(None),
+                    liquidities=store.LockedVar({}),
+                    last_date=store.LockedVar(None),
+                    report=mock.Mock(),
+                    worker=None,
+                    worker_notify=None,
+                    worker_started=False,
+                    callback=None),
                 mock.patch.multiple(portfolio.Computation,
                     computations=portfolio.Computation.computations),
                 ]
@@ -47,12 +56,12 @@ class WebMockTestCase(unittest.TestCase):
         for patcher in self.patchers:
             patcher.stop()
         self.wm.stop()
-        super(WebMockTestCase, self).tearDown()
+        super().tearDown()
 
 @unittest.skipUnless("unit" in limits, "Unit skipped")
 class poloniexETest(unittest.TestCase):
     def setUp(self):
-        super(poloniexETest, self).setUp()
+        super().setUp()
         self.wm = requests_mock.Mocker()
         self.wm.start()
 
@@ -60,7 +69,35 @@ class poloniexETest(unittest.TestCase):
 
     def tearDown(self):
         self.wm.stop()
-        super(poloniexETest, self).tearDown()
+        super().tearDown()
+
+    def test__init(self):
+        with self.subTest("Nominal case"), \
+                mock.patch("market.ccxt.poloniexE.session") as session:
+            session.request.return_value = "response"
+            ccxt = market.ccxt.poloniexE()
+            ccxt._market = mock.Mock
+            ccxt._market.report = mock.Mock()
+
+            ccxt.session.request("GET", "URL", data="data",
+                    headers="headers")
+            ccxt._market.report.log_http_request.assert_called_with('GET', 'URL', 'data',
+                    'headers', 'response')
+
+        with self.subTest("Raising"),\
+                mock.patch("market.ccxt.poloniexE.session") as session:
+            session.request.side_effect = market.ccxt.RequestException("Boo")
+
+            ccxt = market.ccxt.poloniexE()
+            ccxt._market = mock.Mock
+            ccxt._market.report = mock.Mock()
+
+            with self.assertRaises(market.ccxt.RequestException, msg="Boo") as cm:
+                ccxt.session.request("GET", "URL", data="data",
+                        headers="headers")
+            ccxt._market.report.log_http_request.assert_called_with('GET', 'URL', 'data',
+                    'headers', cm.exception)
+
 
     def test_nanoseconds(self):
         with mock.patch.object(market.ccxt.time, "time") as time:
@@ -72,6 +109,58 @@ class poloniexETest(unittest.TestCase):
             time.return_value = 123456.7890123456
             self.assertEqual(123456789012345, self.s.nonce())
 
+    def test_request(self):
+        with mock.patch.object(market.ccxt.poloniex, "request") as request,\
+                mock.patch("market.ccxt.retry_call") as retry_call:
+            with self.subTest(wrapped=True):
+                with self.subTest(desc="public"):
+                    self.s.request("foo")
+                    retry_call.assert_called_with(request,
+                            delay=1, tries=10, fargs=["foo"],
+                            fkwargs={'api': 'public', 'method': 'GET', 'params': {}, 'headers': None, 'body': None},
+                            exceptions=(market.ccxt.RequestTimeout, market.ccxt.InvalidNonce))
+                    request.assert_not_called()
+
+                with self.subTest(desc="private GET"):
+                    self.s.request("foo", api="private")
+                    retry_call.assert_called_with(request,
+                            delay=1, tries=10, fargs=["foo"],
+                            fkwargs={'api': 'private', 'method': 'GET', 'params': {}, 'headers': None, 'body': None},
+                            exceptions=(market.ccxt.RequestTimeout, market.ccxt.InvalidNonce))
+                    request.assert_not_called()
+
+                with self.subTest(desc="private POST regexp"):
+                    self.s.request("returnFoo", api="private", method="POST")
+                    retry_call.assert_called_with(request,
+                            delay=1, tries=10, fargs=["returnFoo"],
+                            fkwargs={'api': 'private', 'method': 'POST', 'params': {}, 'headers': None, 'body': None},
+                            exceptions=(market.ccxt.RequestTimeout, market.ccxt.InvalidNonce))
+                    request.assert_not_called()
+
+                with self.subTest(desc="private POST non-regexp"):
+                    self.s.request("getMarginPosition", api="private", method="POST")
+                    retry_call.assert_called_with(request,
+                            delay=1, tries=10, fargs=["getMarginPosition"],
+                            fkwargs={'api': 'private', 'method': 'POST', 'params': {}, 'headers': None, 'body': None},
+                            exceptions=(market.ccxt.RequestTimeout, market.ccxt.InvalidNonce))
+                    request.assert_not_called()
+            retry_call.reset_mock()
+            request.reset_mock()
+            with self.subTest(wrapped=False):
+                with self.subTest(desc="private POST non-matching regexp"):
+                    self.s.request("marginBuy", api="private", method="POST")
+                    request.assert_called_with("marginBuy",
+                            api="private", method="POST", params={},
+                            headers=None, body=None)
+                    retry_call.assert_not_called()
+
+                with self.subTest(desc="private POST non-matching non-regexp"):
+                    self.s.request("closeMarginPositionOther", api="private", method="POST")
+                    request.assert_called_with("closeMarginPositionOther",
+                            api="private", method="POST", params={},
+                            headers=None, body=None)
+                    retry_call.assert_not_called()
+
     def test_order_precision(self):
         self.assertEqual(8, self.s.order_precision("FOO"))
 
@@ -441,10 +530,103 @@ class poloniexETest(unittest.TestCase):
 
             create_order.assert_called_once_with("symbol", "type", "side", "amount", price="price", params="params")
 
+@unittest.skipUnless("unit" in limits, "Unit skipped")
+class NoopLockTest(unittest.TestCase):
+    def test_with(self):
+        noop_lock = store.NoopLock()
+        with noop_lock:
+            self.assertTrue(True)
+
+@unittest.skipUnless("unit" in limits, "Unit skipped")
+class LockedVar(unittest.TestCase):
+
+    def test_values(self):
+        locked_var = store.LockedVar("Foo")
+        self.assertIsInstance(locked_var.lock, store.NoopLock)
+        self.assertEqual("Foo", locked_var.val)
+
+    def test_get(self):
+        with self.subTest(desc="Normal case"):
+            locked_var = store.LockedVar("Foo")
+            self.assertEqual("Foo", locked_var.get())
+        with self.subTest(desc="Dict"):
+            locked_var = store.LockedVar({"foo": "bar"})
+            self.assertEqual({"foo": "bar"}, locked_var.get())
+            self.assertEqual("bar", locked_var.get("foo"))
+            self.assertIsNone(locked_var.get("other"))
+
+    def test_set(self):
+        locked_var = store.LockedVar("Foo")
+        locked_var.set("Bar")
+        self.assertEqual("Bar", locked_var.get())
+
+    def test__getattr(self):
+        dummy = type('Dummy', (object,), {})()
+        dummy.attribute = "Hey"
+
+        locked_var = store.LockedVar(dummy)
+        self.assertEqual("Hey", locked_var.attribute)
+        with self.assertRaises(AttributeError):
+            locked_var.other
+
+    def test_start_lock(self):
+        locked_var = store.LockedVar("Foo")
+        locked_var.start_lock()
+        self.assertEqual("lock", locked_var.lock.__class__.__name__)
+
+        thread1 = threading.Thread(target=locked_var.set, args=["Bar1"])
+        thread2 = threading.Thread(target=locked_var.set, args=["Bar2"])
+        thread3 = threading.Thread(target=locked_var.set, args=["Bar3"])
+
+        with locked_var.lock:
+            thread1.start()
+            thread2.start()
+            thread3.start()
+
+            self.assertEqual("Foo", locked_var.val)
+        thread1.join()
+        thread2.join()
+        thread3.join()
+        self.assertEqual("Bar", locked_var.get()[0:3])
+
+    def test_wait_for_notification(self):
+        with self.assertRaises(RuntimeError):
+            store.Portfolio.wait_for_notification()
+
+        with mock.patch.object(store.Portfolio, "get_cryptoportfolio") as get,\
+                mock.patch.object(store.Portfolio, "report") as report,\
+                mock.patch.object(store.time, "sleep") as sleep:
+            store.Portfolio.start_worker(poll=3)
+
+            store.Portfolio.worker_notify.set()
+
+            store.Portfolio.callback.wait()
+
+            report.print_log.assert_called_once_with("Fetching cryptoportfolio")
+            get.assert_called_once_with(refetch=True)
+            sleep.assert_called_once_with(3)
+            self.assertFalse(store.Portfolio.worker_notify.is_set())
+            self.assertTrue(store.Portfolio.worker.is_alive())
+
+            store.Portfolio.callback.clear()
+            store.Portfolio.worker_started = False
+            store.Portfolio.worker_notify.set()
+            store.Portfolio.callback.wait()
+
+            self.assertFalse(store.Portfolio.worker.is_alive())
+
+    def test_notify_and_wait(self):
+        with mock.patch.object(store.Portfolio, "callback") as callback,\
+                mock.patch.object(store.Portfolio, "worker_notify") as worker_notify:
+            store.Portfolio.notify_and_wait()
+            callback.clear.assert_called_once_with()
+            worker_notify.set.assert_called_once_with()
+            callback.wait.assert_called_once_with()
+
 @unittest.skipUnless("unit" in limits, "Unit skipped")
 class PortfolioTest(WebMockTestCase):
     def setUp(self):
-        super(PortfolioTest, self).setUp()
+        super().setUp()
 
         with open("test_samples/test_portfolio.json") as example:
             self.json_response = example.read()
@@ -453,86 +635,131 @@ class PortfolioTest(WebMockTestCase):
 
     @mock.patch.object(market.Portfolio, "parse_cryptoportfolio")
     def test_get_cryptoportfolio(self, parse_cryptoportfolio):
-        self.wm.get(market.Portfolio.URL, [
-            {"text":'{ "foo": "bar" }', "status_code": 200},
-            {"text": "System Error", "status_code": 500},
-            {"exc": requests.exceptions.ConnectTimeout},
-            ])
-        market.Portfolio.get_cryptoportfolio()
-        self.assertIn("foo", market.Portfolio.data)
-        self.assertEqual("bar", market.Portfolio.data["foo"])
-        self.assertTrue(self.wm.called)
-        self.assertEqual(1, self.wm.call_count)
-        market.Portfolio.report.log_error.assert_not_called()
-        market.Portfolio.report.log_http_request.assert_called_once()
-        parse_cryptoportfolio.assert_called_once_with()
-        market.Portfolio.report.log_http_request.reset_mock()
-        parse_cryptoportfolio.reset_mock()
-        market.Portfolio.data = None
-
-        market.Portfolio.get_cryptoportfolio()
-        self.assertIsNone(market.Portfolio.data)
-        self.assertEqual(2, self.wm.call_count)
-        parse_cryptoportfolio.assert_not_called()
-        market.Portfolio.report.log_error.assert_not_called()
-        market.Portfolio.report.log_http_request.assert_called_once()
-        market.Portfolio.report.log_http_request.reset_mock()
-        parse_cryptoportfolio.reset_mock()
-
-        market.Portfolio.data = "Foo"
-        market.Portfolio.get_cryptoportfolio()
-        self.assertEqual(2, self.wm.call_count)
-        parse_cryptoportfolio.assert_not_called()
-
-        market.Portfolio.get_cryptoportfolio(refetch=True)
-        self.assertEqual("Foo", market.Portfolio.data)
-        self.assertEqual(3, self.wm.call_count)
-        market.Portfolio.report.log_error.assert_called_once_with("get_cryptoportfolio",
-                exception=mock.ANY)
-        market.Portfolio.report.log_http_request.assert_not_called()
+        with self.subTest(parallel=False):
+            self.wm.get(market.Portfolio.URL, [
+                {"text":'{ "foo": "bar" }', "status_code": 200},
+                {"text": "System Error", "status_code": 500},
+                {"exc": requests.exceptions.ConnectTimeout},
+                ])
+            market.Portfolio.get_cryptoportfolio()
+            self.assertIn("foo", market.Portfolio.data.get())
+            self.assertEqual("bar", market.Portfolio.data.get()["foo"])
+            self.assertTrue(self.wm.called)
+            self.assertEqual(1, self.wm.call_count)
+            market.Portfolio.report.log_error.assert_not_called()
+            market.Portfolio.report.log_http_request.assert_called_once()
+            parse_cryptoportfolio.assert_called_once_with()
+            market.Portfolio.report.log_http_request.reset_mock()
+            parse_cryptoportfolio.reset_mock()
+            market.Portfolio.data = store.LockedVar(None)
+
+            market.Portfolio.get_cryptoportfolio()
+            self.assertIsNone(market.Portfolio.data.get())
+            self.assertEqual(2, self.wm.call_count)
+            parse_cryptoportfolio.assert_not_called()
+            market.Portfolio.report.log_error.assert_not_called()
+            market.Portfolio.report.log_http_request.assert_called_once()
+            market.Portfolio.report.log_http_request.reset_mock()
+            parse_cryptoportfolio.reset_mock()
+
+            market.Portfolio.data = store.LockedVar("Foo")
+            market.Portfolio.get_cryptoportfolio()
+            self.assertEqual(2, self.wm.call_count)
+            parse_cryptoportfolio.assert_not_called()
+
+            market.Portfolio.get_cryptoportfolio(refetch=True)
+            self.assertEqual("Foo", market.Portfolio.data.get())
+            self.assertEqual(3, self.wm.call_count)
+            market.Portfolio.report.log_error.assert_called_once_with("get_cryptoportfolio",
+                    exception=mock.ANY)
+            market.Portfolio.report.log_http_request.assert_not_called()
+        with self.subTest(parallel=True):
+            with mock.patch.object(market.Portfolio, "is_worker_thread") as is_worker,\
+                    mock.patch.object(market.Portfolio, "notify_and_wait") as notify:
+                with self.subTest(worker=True):
+                    market.Portfolio.data = store.LockedVar(None)
+                    market.Portfolio.worker = mock.Mock()
+                    is_worker.return_value = True
+                    self.wm.get(market.Portfolio.URL, [
+                        {"text":'{ "foo": "bar" }', "status_code": 200},
+                        ])
+                    market.Portfolio.get_cryptoportfolio()
+                    self.assertIn("foo", market.Portfolio.data.get())
+                parse_cryptoportfolio.reset_mock()
+                with self.subTest(worker=False):
+                    market.Portfolio.data = store.LockedVar(None)
+                    market.Portfolio.worker = mock.Mock()
+                    is_worker.return_value = False
+                    market.Portfolio.get_cryptoportfolio()
+                    notify.assert_called_once_with()
+                    parse_cryptoportfolio.assert_not_called()
 
     def test_parse_cryptoportfolio(self):
-        market.Portfolio.data = store.json.loads(self.json_response, parse_int=D,
-                parse_float=D)
-        market.Portfolio.parse_cryptoportfolio()
-
-        self.assertListEqual(
-                ["medium", "high"],
-                list(market.Portfolio.liquidities.keys()))
-
-        liquidities = market.Portfolio.liquidities
-        self.assertEqual(10, len(liquidities["medium"].keys()))
-        self.assertEqual(10, len(liquidities["high"].keys()))
-
-        expected = {
-                'BTC':  (D("0.2857"), "long"),
-                'DGB':  (D("0.1015"), "long"),
-                'DOGE': (D("0.1805"), "long"),
-                'SC':   (D("0.0623"), "long"),
-                'ZEC':  (D("0.3701"), "long"),
-                }
-        date = portfolio.datetime(2018, 1, 8)
-        self.assertDictEqual(expected, liquidities["high"][date])
-
-        expected = {
-                'BTC':  (D("1.1102e-16"), "long"),
-                'ETC':  (D("0.1"), "long"),
-                'FCT':  (D("0.1"), "long"),
-                'GAS':  (D("0.1"), "long"),
-                'NAV':  (D("0.1"), "long"),
-                'OMG':  (D("0.1"), "long"),
-                'OMNI': (D("0.1"), "long"),
-                'PPC':  (D("0.1"), "long"),
-                'RIC':  (D("0.1"), "long"),
-                'VIA':  (D("0.1"), "long"),
-                'XCP':  (D("0.1"), "long"),
-                }
-        self.assertDictEqual(expected, liquidities["medium"][date])
-        self.assertEqual(portfolio.datetime(2018, 1, 15), market.Portfolio.last_date)
+        with self.subTest(description="Normal case"):
+            market.Portfolio.data = store.LockedVar(store.json.loads(
+                self.json_response, parse_int=D, parse_float=D))
+            market.Portfolio.parse_cryptoportfolio()
+
+            self.assertListEqual(
+                    ["medium", "high"],
+                    list(market.Portfolio.liquidities.get().keys()))
+
+            liquidities = market.Portfolio.liquidities.get()
+            self.assertEqual(10, len(liquidities["medium"].keys()))
+            self.assertEqual(10, len(liquidities["high"].keys()))
+
+            expected = {
+                    'BTC':  (D("0.2857"), "long"),
+                    'DGB':  (D("0.1015"), "long"),
+                    'DOGE': (D("0.1805"), "long"),
+                    'SC':   (D("0.0623"), "long"),
+                    'ZEC':  (D("0.3701"), "long"),
+                    }
+            date = portfolio.datetime(2018, 1, 8)
+            self.assertDictEqual(expected, liquidities["high"][date])
+
+            expected = {
+                    'BTC':  (D("1.1102e-16"), "long"),
+                    'ETC':  (D("0.1"), "long"),
+                    'FCT':  (D("0.1"), "long"),
+                    'GAS':  (D("0.1"), "long"),
+                    'NAV':  (D("0.1"), "long"),
+                    'OMG':  (D("0.1"), "long"),
+                    'OMNI': (D("0.1"), "long"),
+                    'PPC':  (D("0.1"), "long"),
+                    'RIC':  (D("0.1"), "long"),
+                    'VIA':  (D("0.1"), "long"),
+                    'XCP':  (D("0.1"), "long"),
+                    }
+            self.assertDictEqual(expected, liquidities["medium"][date])
+            self.assertEqual(portfolio.datetime(2018, 1, 15), market.Portfolio.last_date.get())
+
+        with self.subTest(description="Missing weight"):
+            data = store.json.loads(self.json_response, parse_int=D, parse_float=D)
+            del(data["portfolio_2"]["weights"])
+            market.Portfolio.data = store.LockedVar(data)
+
+            market.Portfolio.parse_cryptoportfolio()
+            self.assertListEqual(
+                    ["medium", "high"],
+                    list(market.Portfolio.liquidities.get().keys()))
+            self.assertEqual({}, market.Portfolio.liquidities.get("medium"))
+
+        with self.subTest(description="All missing weights"):
+            data = store.json.loads(self.json_response, parse_int=D, parse_float=D)
+            del(data["portfolio_1"]["weights"])
+            del(data["portfolio_2"]["weights"])
+            market.Portfolio.data = store.LockedVar(data)
+
+            market.Portfolio.parse_cryptoportfolio()
+            self.assertEqual({}, market.Portfolio.liquidities.get("medium"))
+            self.assertEqual({}, market.Portfolio.liquidities.get("high"))
+            self.assertEqual(datetime.datetime(1,1,1), market.Portfolio.last_date.get())
+
 
     @mock.patch.object(market.Portfolio, "get_cryptoportfolio")
     def test_repartition(self, get_cryptoportfolio):
-        market.Portfolio.liquidities = {
+        market.Portfolio.liquidities = store.LockedVar({
                 "medium": {
                     "2018-03-01": "medium_2018-03-01",
                     "2018-03-08": "medium_2018-03-08",
@@ -541,8 +768,8 @@ class PortfolioTest(WebMockTestCase):
                     "2018-03-01": "high_2018-03-01",
                     "2018-03-08": "high_2018-03-08",
                     }
-                }
-        market.Portfolio.last_date = "2018-03-08"
+                })
+        market.Portfolio.last_date = store.LockedVar("2018-03-08")
 
         self.assertEqual("medium_2018-03-08", market.Portfolio.repartition())
         get_cryptoportfolio.assert_called_once_with()
@@ -559,9 +786,9 @@ class PortfolioTest(WebMockTestCase):
             else:
                 self.assertFalse(refetch)
             self.call_count += 1
-            market.Portfolio.last_date = store.datetime.now()\
+            market.Portfolio.last_date = store.LockedVar(store.datetime.now()\
                 - store.timedelta(10)\
-                + store.timedelta(self.call_count)
+                + store.timedelta(self.call_count))
         get_cryptoportfolio.side_effect = _get
 
         market.Portfolio.wait_for_recent()
@@ -572,7 +799,7 @@ class PortfolioTest(WebMockTestCase):
 
         sleep.reset_mock()
         get_cryptoportfolio.reset_mock()
-        market.Portfolio.last_date = None
+        market.Portfolio.last_date = store.LockedVar(None)
         self.call_count = 0
         market.Portfolio.wait_for_recent(delta=15)
         sleep.assert_not_called()
@@ -580,13 +807,45 @@ class PortfolioTest(WebMockTestCase):
 
         sleep.reset_mock()
         get_cryptoportfolio.reset_mock()
-        market.Portfolio.last_date = None
+        market.Portfolio.last_date = store.LockedVar(None)
         self.call_count = 0
         market.Portfolio.wait_for_recent(delta=1)
         sleep.assert_called_with(30)
         self.assertEqual(9, sleep.call_count)
         self.assertEqual(10, get_cryptoportfolio.call_count)
 
+    def test_is_worker_thread(self):
+        with self.subTest(worker=None):
+            self.assertFalse(store.Portfolio.is_worker_thread())
+
+        with self.subTest(worker="not self"),\
+                mock.patch("threading.current_thread") as current_thread:
+            current = mock.Mock()
+            current_thread.return_value = current
+            store.Portfolio.worker = mock.Mock()
+            self.assertFalse(store.Portfolio.is_worker_thread())
+
+        with self.subTest(worker="self"),\
+                mock.patch("threading.current_thread") as current_thread:
+            current = mock.Mock()
+            current_thread.return_value = current
+            store.Portfolio.worker = current
+            self.assertTrue(store.Portfolio.is_worker_thread())
+
+    def test_start_worker(self):
+        with mock.patch.object(store.Portfolio, "wait_for_notification") as notification:
+            store.Portfolio.start_worker()
+            notification.assert_called_once_with(poll=30)
+
+            self.assertEqual("lock", store.Portfolio.last_date.lock.__class__.__name__)
+            self.assertEqual("lock", store.Portfolio.liquidities.lock.__class__.__name__)
+            store.Portfolio.report.start_lock.assert_called_once_with()
+
+            self.assertIsNotNone(store.Portfolio.worker)
+            self.assertIsNotNone(store.Portfolio.worker_notify)
+            self.assertIsNotNone(store.Portfolio.callback)
+            self.assertTrue(store.Portfolio.worker_started)
+
 @unittest.skipUnless("unit" in limits, "Unit skipped")
 class AmountTest(WebMockTestCase):
     def test_values(self):
@@ -912,12 +1171,12 @@ class BalanceTest(WebMockTestCase):
 @unittest.skipUnless("unit" in limits, "Unit skipped")
 class MarketTest(WebMockTestCase):
     def setUp(self):
-        super(MarketTest, self).setUp()
+        super().setUp()
 
         self.ccxt = mock.Mock(spec=market.ccxt.poloniexE)
 
     def test_values(self):
-        m = market.Market(self.ccxt)
+        m = market.Market(self.ccxt, self.market_args())
 
         self.assertEqual(self.ccxt, m.ccxt)
         self.assertFalse(m.debug)
@@ -929,28 +1188,33 @@ class MarketTest(WebMockTestCase):
         self.assertEqual(m, m.balances.market)
         self.assertEqual(m, m.ccxt._market)
 
-        m = market.Market(self.ccxt, debug=True)
+        m = market.Market(self.ccxt, self.market_args(debug=True))
         self.assertTrue(m.debug)
 
-        m = market.Market(self.ccxt, debug=False)
+        m = market.Market(self.ccxt, self.market_args(debug=False))
         self.assertFalse(m.debug)
 
+        with mock.patch("market.ReportStore") as report_store:
+            with self.subTest(quiet=False):
+                m = market.Market(self.ccxt, self.market_args(quiet=False))
+                report_store.assert_called_with(m, verbose_print=True)
+                report_store().log_market.assert_called_once()
+            report_store.reset_mock()
+            with self.subTest(quiet=True):
+                m = market.Market(self.ccxt, self.market_args(quiet=True))
+                report_store.assert_called_with(m, verbose_print=False)
+                report_store().log_market.assert_called_once()
+
     @mock.patch("market.ccxt")
     def test_from_config(self, ccxt):
         with mock.patch("market.ReportStore"):
             ccxt.poloniexE.return_value = self.ccxt
-            self.ccxt.session.request.return_value = "response"
 
-            m = market.Market.from_config({"key": "key", "secred": "secret"})
+            m = market.Market.from_config({"key": "key", "secred": "secret"}, self.market_args())
 
             self.assertEqual(self.ccxt, m.ccxt)
 
-            self.ccxt.session.request("GET", "URL", data="data",
-                    headers="headers")
-            m.report.log_http_request.assert_called_with('GET', 'URL', 'data',
-                    'headers', 'response')
-
-        m = market.Market.from_config({"key": "key", "secred": "secret"}, debug=True)
+        m = market.Market.from_config({"key": "key", "secred": "secret"}, self.market_args(debug=True))
         self.assertEqual(True, m.debug)
 
     def test_get_tickers(self):
@@ -959,7 +1223,7 @@ class MarketTest(WebMockTestCase):
                 market.NotSupported
                 ]
 
-        m = market.Market(self.ccxt)
+        m = market.Market(self.ccxt, self.market_args())
         self.assertEqual("tickers", m.get_tickers())
         self.assertEqual("tickers", m.get_tickers())
         self.ccxt.fetch_tickers.assert_called_once()
@@ -972,7 +1236,7 @@ class MarketTest(WebMockTestCase):
                     "ETH/ETC": { "bid": 1, "ask": 3 },
                     "XVG/ETH": { "bid": 10, "ask": 40 },
                     }
-            m = market.Market(self.ccxt)
+            m = market.Market(self.ccxt, self.market_args())
 
             ticker = m.get_ticker("ETH", "ETC")
             self.assertEqual(1, ticker["bid"])
@@ -1000,7 +1264,7 @@ class MarketTest(WebMockTestCase):
                     market.ExchangeError("foo"),
                     ]
 
-            m = market.Market(self.ccxt)
+            m = market.Market(self.ccxt, self.market_args())
 
             ticker = m.get_ticker("ETH", "ETC")
             self.ccxt.fetch_ticker.assert_called_with("ETH/ETC")
@@ -1020,7 +1284,7 @@ class MarketTest(WebMockTestCase):
             self.assertIsNone(ticker)
 
     def test_fetch_fees(self):
-        m = market.Market(self.ccxt)
+        m = market.Market(self.ccxt, self.market_args())
         self.ccxt.fetch_fees.return_value = "Foo"
         self.assertEqual("Foo", m.fetch_fees())
         self.ccxt.fetch_fees.assert_called_once()
@@ -1047,7 +1311,7 @@ class MarketTest(WebMockTestCase):
         get_ticker.side_effect = _get_ticker
 
         with mock.patch("market.ReportStore"):
-            m = market.Market(self.ccxt)
+            m = market.Market(self.ccxt, self.market_args())
             self.ccxt.fetch_all_balances.return_value = {
                     "USDT": {
                         "exchange_free": D("10000.0"),
@@ -1087,7 +1351,7 @@ class MarketTest(WebMockTestCase):
                 (False, 12), (True, 12)]:
             with self.subTest(sleep=sleep, debug=debug), \
                     mock.patch("market.ReportStore"):
-                m = market.Market(self.ccxt, debug=debug)
+                m = market.Market(self.ccxt, self.market_args(debug=debug))
 
                 order_mock1 = mock.Mock()
                 order_mock2 = mock.Mock()
@@ -1159,12 +1423,44 @@ class MarketTest(WebMockTestCase):
                 else:
                     time_mock.assert_called_with(sleep)
 
+        with self.subTest("disappearing order"), \
+                mock.patch("market.ReportStore"):
+            all_orders.reset_mock()
+            m = market.Market(self.ccxt, self.market_args())
+
+            order_mock1 = mock.Mock()
+            order_mock2 = mock.Mock()
+            all_orders.side_effect = [
+                    [order_mock1, order_mock2],
+                    [order_mock1, order_mock2],
+
+                    [order_mock1, order_mock2],
+                    [order_mock1, order_mock2],
+
+                    []
+                    ]
+
+            order_mock1.get_status.side_effect = ["open", "closed"]
+            order_mock2.get_status.side_effect = ["open", "error_disappeared"]
+
+            order_mock1.trade = mock.Mock()
+            trade_mock = mock.Mock()
+            order_mock2.trade = trade_mock
+
+            trade_mock.tick_actions_recreate.return_value = "tick1"
+
+            m.follow_orders()
+
+            trade_mock.tick_actions_recreate.assert_called_once_with(2)
+            trade_mock.prepare_order.assert_called_once_with(compute_value="tick1")
+            m.report.log_error.assert_called_once_with("follow_orders", message=mock.ANY)
+
     @mock.patch.object(market.BalanceStore, "fetch_balances")
     def test_move_balance(self, fetch_balances):
         for debug in [True, False]:
             with self.subTest(debug=debug),\
                     mock.patch("market.ReportStore"):
-                m = market.Market(self.ccxt, debug=debug)
+                m = market.Market(self.ccxt, self.market_args(debug=debug))
 
                 value_from = portfolio.Amount("BTC", "1.0")
                 value_from.linked_to = portfolio.Amount("ETH", "10.0")
@@ -1200,51 +1496,287 @@ class MarketTest(WebMockTestCase):
                     self.ccxt.transfer_balance.assert_any_call("USDT", 100, "exchange", "margin")
                     self.ccxt.transfer_balance.assert_any_call("ETC", 5, "margin", "exchange")
 
-    def test_store_report(self):
+        m.report.reset_mock()
+        fetch_balances.reset_mock()
+        with self.subTest(retry=True):
+            with mock.patch("market.ReportStore"):
+                m = market.Market(self.ccxt, self.market_args())
 
-        file_open = mock.mock_open()
-        m = market.Market(self.ccxt, user_id=1)
-        with self.subTest(file=None),\
-                mock.patch.object(m, "report") as report,\
-                mock.patch("market.open", file_open):
-            m.store_report()
-            report.merge.assert_called_with(store.Portfolio.report)
-            file_open.assert_not_called()
+                value_from = portfolio.Amount("BTC", "0.0")
+                value_from.linked_to = portfolio.Amount("ETH", "0.0")
+                value_to = portfolio.Amount("BTC", "-3.0")
+                trade = portfolio.Trade(value_from, value_to, "ETH", m)
 
-        report.reset_mock()
+                m.trades.all = [trade]
+                balance = portfolio.Balance("BTC", { "margin_in_position": "0", "margin_available": "0" })
+                m.balances.all = {"BTC": balance}
+
+                m.ccxt.transfer_balance.side_effect = [
+                        market.ccxt.RequestTimeout,
+                        market.ccxt.InvalidNonce,
+                        True
+                        ]
+                m.move_balances()
+                self.ccxt.transfer_balance.assert_has_calls([
+                    mock.call("BTC", 3, "exchange", "margin"),
+                    mock.call("BTC", 3, "exchange", "margin"),
+                    mock.call("BTC", 3, "exchange", "margin")
+                    ])
+                self.assertEqual(3, fetch_balances.call_count)
+                m.report.log_error.assert_called_with(mock.ANY, message="Retrying", exception=mock.ANY)
+                self.assertEqual(3, m.report.log_move_balances.call_count)
+
+        self.ccxt.transfer_balance.reset_mock()
+        m.report.reset_mock()
+        fetch_balances.reset_mock()
+        with self.subTest(retry=True, too_much=True):
+            with mock.patch("market.ReportStore"):
+                m = market.Market(self.ccxt, self.market_args())
+
+                value_from = portfolio.Amount("BTC", "0.0")
+                value_from.linked_to = portfolio.Amount("ETH", "0.0")
+                value_to = portfolio.Amount("BTC", "-3.0")
+                trade = portfolio.Trade(value_from, value_to, "ETH", m)
+
+                m.trades.all = [trade]
+                balance = portfolio.Balance("BTC", { "margin_in_position": "0", "margin_available": "0" })
+                m.balances.all = {"BTC": balance}
+
+                m.ccxt.transfer_balance.side_effect = [
+                        market.ccxt.RequestTimeout,
+                        market.ccxt.RequestTimeout,
+                        market.ccxt.RequestTimeout,
+                        market.ccxt.RequestTimeout,
+                        market.ccxt.RequestTimeout,
+                        ]
+                with self.assertRaises(market.ccxt.RequestTimeout):
+                    m.move_balances()
+
+        self.ccxt.transfer_balance.reset_mock()
+        m.report.reset_mock()
+        fetch_balances.reset_mock()
+        with self.subTest(retry=True, partial_result=True):
+            with mock.patch("market.ReportStore"):
+                m = market.Market(self.ccxt, self.market_args())
+
+                value_from = portfolio.Amount("BTC", "1.0")
+                value_from.linked_to = portfolio.Amount("ETH", "10.0")
+                value_to = portfolio.Amount("BTC", "10.0")
+                trade1 = portfolio.Trade(value_from, value_to, "ETH", m)
+
+                value_from = portfolio.Amount("BTC", "0.0")
+                value_from.linked_to = portfolio.Amount("ETH", "0.0")
+                value_to = portfolio.Amount("BTC", "-3.0")
+                trade2 = portfolio.Trade(value_from, value_to, "ETH", m)
+
+                value_from = portfolio.Amount("USDT", "0.0")
+                value_from.linked_to = portfolio.Amount("XVG", "0.0")
+                value_to = portfolio.Amount("USDT", "-50.0")
+                trade3 = portfolio.Trade(value_from, value_to, "XVG", m)
+
+                m.trades.all = [trade1, trade2, trade3]
+                balance1 = portfolio.Balance("BTC", { "margin_in_position": "0", "margin_available": "0" })
+                balance2 = portfolio.Balance("USDT", { "margin_in_position": "100", "margin_available": "50" })
+                balance3 = portfolio.Balance("ETC", { "margin_in_position": "10", "margin_available": "15" })
+                m.balances.all = {"BTC": balance1, "USDT": balance2, "ETC": balance3}
+
+                call_counts = { "BTC": 0, "USDT": 0, "ETC": 0 }
+                def _transfer_balance(currency, amount, from_, to_):
+                    call_counts[currency] += 1
+                    if currency == "BTC":
+                        m.balances.all["BTC"] = portfolio.Balance("BTC", { "margin_in_position": "0", "margin_available": "3" })
+                    if currency == "USDT":
+                        if call_counts["USDT"] == 1:
+                            raise market.ccxt.RequestTimeout
+                        else:
+                            m.balances.all["USDT"] = portfolio.Balance("USDT", { "margin_in_position": "100", "margin_available": "150" })
+                    if currency == "ETC":
+                            m.balances.all["ETC"] = portfolio.Balance("ETC", { "margin_in_position": "10", "margin_available": "10" })
+
+
+                m.ccxt.transfer_balance.side_effect = _transfer_balance
+
+                m.move_balances()
+                self.ccxt.transfer_balance.assert_has_calls([
+                    mock.call("BTC", 3, "exchange", "margin"),
+                    mock.call('USDT', 100, 'exchange', 'margin'),
+                    mock.call('USDT', 100, 'exchange', 'margin'),
+                    mock.call("ETC", 5, "margin", "exchange")
+                    ])
+                self.assertEqual(2, fetch_balances.call_count)
+                m.report.log_error.assert_called_with(mock.ANY, message="Retrying", exception=mock.ANY)
+                self.assertEqual(2, m.report.log_move_balances.call_count)
+                m.report.log_move_balances.asser_has_calls([
+                    mock.call(
+                        {
+                            'BTC': portfolio.Amount("BTC", "3"),
+                            'USDT': portfolio.Amount("USDT", "150"),
+                            'ETC': portfolio.Amount("ETC", "10"),
+                            },
+                        {
+                            'BTC': portfolio.Amount("BTC", "3"),
+                            'USDT': portfolio.Amount("USDT", "100"),
+                            }),
+                    mock.call(
+                        {
+                            'BTC': portfolio.Amount("BTC", "3"),
+                            'USDT': portfolio.Amount("USDT", "150"),
+                            'ETC': portfolio.Amount("ETC", "10"),
+                            },
+                        {
+                            'BTC': portfolio.Amount("BTC", "0"),
+                            'USDT': portfolio.Amount("USDT", "100"),
+                            'ETC': portfolio.Amount("ETC", "-5"),
+                            }),
+                    ])
+
+
+    def test_store_file_report(self):
         file_open = mock.mock_open()
-        m = market.Market(self.ccxt, report_path="present", user_id=1)
+        m = market.Market(self.ccxt,
+                self.market_args(report_path="present"), user_id=1)
         with self.subTest(file="present"),\
                 mock.patch("market.open", file_open),\
                 mock.patch.object(m, "report") as report,\
                 mock.patch.object(market, "datetime") as time_mock:
 
-            time_mock.now.return_value = datetime.datetime(2018, 2, 25)
+            report.print_logs = [[time_mock.now(), "Foo"], [time_mock.now(), "Bar"]]
             report.to_json.return_value = "json_content"
 
-            m.store_report()
+            m.store_file_report(datetime.datetime(2018, 2, 25))
 
             file_open.assert_any_call("present/2018-02-25T00:00:00_1.json", "w")
-            file_open().write.assert_called_once_with("json_content")
+            file_open.assert_any_call("present/2018-02-25T00:00:00_1.log", "w")
+            file_open().write.assert_any_call("json_content")
+            file_open().write.assert_any_call("Foo\nBar")
             m.report.to_json.assert_called_once_with()
-            report.merge.assert_called_with(store.Portfolio.report)
-
-        report.reset_mock()
 
-        m = market.Market(self.ccxt, report_path="error", user_id=1)
+        m = market.Market(self.ccxt, self.market_args(report_path="error"), user_id=1)
         with self.subTest(file="error"),\
                 mock.patch("market.open") as file_open,\
                 mock.patch.object(m, "report") as report,\
                 mock.patch('sys.stdout', new_callable=StringIO) as stdout_mock:
             file_open.side_effect = FileNotFoundError
 
+            m.store_file_report(datetime.datetime(2018, 2, 25))
+
+            self.assertRegex(stdout_mock.getvalue(), "impossible to store report file: FileNotFoundError;")
+
+    @mock.patch.object(market, "psycopg2")
+    def test_store_database_report(self, psycopg2):
+        connect_mock = mock.Mock()
+        cursor_mock = mock.MagicMock()
+
+        connect_mock.cursor.return_value = cursor_mock
+        psycopg2.connect.return_value = connect_mock
+        m = market.Market(self.ccxt, self.market_args(),
+                pg_config={"config": "pg_config"}, user_id=1)
+        cursor_mock.fetchone.return_value = [42]
+
+        with self.subTest(error=False),\
+                mock.patch.object(m, "report") as report:
+            report.to_json_array.return_value = [
+                    ("date1", "type1", "payload1"),
+                    ("date2", "type2", "payload2"),
+                    ]
+            m.store_database_report(datetime.datetime(2018, 3, 24))
+            connect_mock.assert_has_calls([
+                mock.call.cursor(),
+                mock.call.cursor().execute('INSERT INTO reports("date", "market_config_id", "debug") VALUES (%s, %s, %s) RETURNING id;', (datetime.datetime(2018, 3, 24), None, False)),
+                mock.call.cursor().fetchone(),
+                mock.call.cursor().execute('INSERT INTO report_lines("date", "report_id", "type", "payload") VALUES (%s, %s, %s, %s);', ('date1', 42, 'type1', 'payload1')),
+                mock.call.cursor().execute('INSERT INTO report_lines("date", "report_id", "type", "payload") VALUES (%s, %s, %s, %s);', ('date2', 42, 'type2', 'payload2')),
+                mock.call.commit(),
+                mock.call.cursor().close(),
+                mock.call.close()
+                ])
+
+        connect_mock.reset_mock()
+        with self.subTest(error=True),\
+                mock.patch('sys.stdout', new_callable=StringIO) as stdout_mock:
+            psycopg2.connect.side_effect = Exception("Bouh")
+            m.store_database_report(datetime.datetime(2018, 3, 24))
+            self.assertEqual(stdout_mock.getvalue(), "impossible to store report to database: Exception; Bouh\n")
+
+    def test_store_report(self):
+        m = market.Market(self.ccxt, self.market_args(report_db=False), user_id=1)
+        with self.subTest(file=None, pg_config=None),\
+                mock.patch.object(m, "report") as report,\
+                mock.patch.object(m, "store_database_report") as db_report,\
+                mock.patch.object(m, "store_file_report") as file_report:
+            m.store_report()
+            report.merge.assert_called_with(store.Portfolio.report)
+
+            file_report.assert_not_called()
+            db_report.assert_not_called()
+
+        report.reset_mock()
+        m = market.Market(self.ccxt, self.market_args(report_db=False, report_path="present"), user_id=1)
+        with self.subTest(file="present", pg_config=None),\
+                mock.patch.object(m, "report") as report,\
+                mock.patch.object(m, "store_file_report") as file_report,\
+                mock.patch.object(m, "store_database_report") as db_report,\
+                mock.patch.object(market, "datetime") as time_mock:
+
+            time_mock.now.return_value = datetime.datetime(2018, 2, 25)
+
             m.store_report()
 
             report.merge.assert_called_with(store.Portfolio.report)
-            self.assertRegex(stdout_mock.getvalue(), "impossible to store report file: FileNotFoundError;")
+            file_report.assert_called_once_with(datetime.datetime(2018, 2, 25))
+            db_report.assert_not_called()
+
+        report.reset_mock()
+        m = market.Market(self.ccxt, self.market_args(report_db=True, report_path="present"), user_id=1)
+        with self.subTest(file="present", pg_config=None, report_db=True),\
+                mock.patch.object(m, "report") as report,\
+                mock.patch.object(m, "store_file_report") as file_report,\
+                mock.patch.object(m, "store_database_report") as db_report,\
+                mock.patch.object(market, "datetime") as time_mock:
+
+            time_mock.now.return_value = datetime.datetime(2018, 2, 25)
+
+            m.store_report()
+
+            report.merge.assert_called_with(store.Portfolio.report)
+            file_report.assert_called_once_with(datetime.datetime(2018, 2, 25))
+            db_report.assert_not_called()
+
+        report.reset_mock()
+        m = market.Market(self.ccxt, self.market_args(report_db=True), pg_config="present", user_id=1)
+        with self.subTest(file=None, pg_config="present"),\
+                mock.patch.object(m, "report") as report,\
+                mock.patch.object(m, "store_file_report") as file_report,\
+                mock.patch.object(m, "store_database_report") as db_report,\
+                mock.patch.object(market, "datetime") as time_mock:
+
+            time_mock.now.return_value = datetime.datetime(2018, 2, 25)
+
+            m.store_report()
+
+            report.merge.assert_called_with(store.Portfolio.report)
+            file_report.assert_not_called()
+            db_report.assert_called_once_with(datetime.datetime(2018, 2, 25))
+
+        report.reset_mock()
+        m = market.Market(self.ccxt, self.market_args(report_db=True, report_path="present"),
+                pg_config="pg_config", user_id=1)
+        with self.subTest(file="present", pg_config="present"),\
+                mock.patch.object(m, "report") as report,\
+                mock.patch.object(m, "store_file_report") as file_report,\
+                mock.patch.object(m, "store_database_report") as db_report,\
+                mock.patch.object(market, "datetime") as time_mock:
+
+            time_mock.now.return_value = datetime.datetime(2018, 2, 25)
+
+            m.store_report()
+
+            report.merge.assert_called_with(store.Portfolio.report)
+            file_report.assert_called_once_with(datetime.datetime(2018, 2, 25))
+            db_report.assert_called_once_with(datetime.datetime(2018, 2, 25))
 
     def test_print_orders(self):
-        m = market.Market(self.ccxt)
+        m = market.Market(self.ccxt, self.market_args())
         with mock.patch.object(m.report, "log_stage") as log_stage,\
                 mock.patch.object(m.balances, "fetch_balances") as fetch_balances,\
                 mock.patch.object(m, "prepare_trades") as prepare_trades,\
@@ -1258,7 +1790,7 @@ class MarketTest(WebMockTestCase):
             prepare_orders.assert_called_with(compute_value="average")
 
     def test_print_balances(self):
-        m = market.Market(self.ccxt)
+        m = market.Market(self.ccxt, self.market_args())
 
         with mock.patch.object(m.balances, "in_currency") as in_currency,\
                 mock.patch.object(m.report, "log_stage") as log_stage,\
@@ -1283,7 +1815,7 @@ class MarketTest(WebMockTestCase):
     @mock.patch("market.ReportStore.log_error")
     @mock.patch("market.Market.store_report")
     def test_process(self, store_report, log_error, process):
-        m = market.Market(self.ccxt)
+        m = market.Market(self.ccxt, self.market_args())
         with self.subTest(before=False, after=False):
             m.process(None)
 
@@ -1593,7 +2125,7 @@ class TradeStoreTest(WebMockTestCase):
 @unittest.skipUnless("unit" in limits, "Unit skipped")
 class BalanceStoreTest(WebMockTestCase):
     def setUp(self):
-        super(BalanceStoreTest, self).setUp()
+        super().setUp()
 
         self.fetch_balance = {
                 "ETC": {
@@ -1832,16 +2364,20 @@ class TradeTest(WebMockTestCase):
         value_to = portfolio.Amount("BTC", "1.0")
         trade = portfolio.Trade(value_from, value_to, "ETH", self.m)
 
-        self.assertEqual("buy", trade.order_action(False))
-        self.assertEqual("sell", trade.order_action(True))
+        trade.inverted = False
+        self.assertEqual("buy", trade.order_action())
+        trade.inverted = True
+        self.assertEqual("sell", trade.order_action())
 
         value_from = portfolio.Amount("BTC", "0")
         value_from.linked_to = portfolio.Amount("ETH", "0")
         value_to = portfolio.Amount("BTC", "-1.0")
         trade = portfolio.Trade(value_from, value_to, "ETH", self.m)
 
-        self.assertEqual("sell", trade.order_action(False))
-        self.assertEqual("buy", trade.order_action(True))
+        trade.inverted = False
+        self.assertEqual("sell", trade.order_action())
+        trade.inverted = True
+        self.assertEqual("buy", trade.order_action())
 
     def test_trade_type(self):
         value_from = portfolio.Amount("BTC", "0.5")
@@ -1859,26 +2395,59 @@ class TradeTest(WebMockTestCase):
         self.assertEqual("short", trade.trade_type)
 
     def test_is_fullfiled(self):
-        value_from = portfolio.Amount("BTC", "0.5")
-        value_from.linked_to = portfolio.Amount("ETH", "10.0")
-        value_to = portfolio.Amount("BTC", "1.0")
-        trade = portfolio.Trade(value_from, value_to, "ETH", self.m)
+        with self.subTest(inverted=False):
+            value_from = portfolio.Amount("BTC", "0.5")
+            value_from.linked_to = portfolio.Amount("ETH", "10.0")
+            value_to = portfolio.Amount("BTC", "1.0")
+            trade = portfolio.Trade(value_from, value_to, "ETH", self.m)
 
-        order1 = mock.Mock()
-        order1.filled_amount.return_value = portfolio.Amount("BTC", "0.3")
+            order1 = mock.Mock()
+            order1.filled_amount.return_value = portfolio.Amount("BTC", "0.3")
 
-        order2 = mock.Mock()
-        order2.filled_amount.return_value = portfolio.Amount("BTC", "0.01")
-        trade.orders.append(order1)
-        trade.orders.append(order2)
+            order2 = mock.Mock()
+            order2.filled_amount.return_value = portfolio.Amount("BTC", "0.01")
+            trade.orders.append(order1)
+            trade.orders.append(order2)
+
+            self.assertFalse(trade.is_fullfiled)
+
+            order3 = mock.Mock()
+            order3.filled_amount.return_value = portfolio.Amount("BTC", "0.19")
+            trade.orders.append(order3)
+
+            self.assertTrue(trade.is_fullfiled)
 
-        self.assertFalse(trade.is_fullfiled)
+            order1.filled_amount.assert_called_with(in_base_currency=True)
+            order2.filled_amount.assert_called_with(in_base_currency=True)
+            order3.filled_amount.assert_called_with(in_base_currency=True)
 
-        order3 = mock.Mock()
-        order3.filled_amount.return_value = portfolio.Amount("BTC", "0.19")
-        trade.orders.append(order3)
+        with self.subTest(inverted=True):
+            value_from = portfolio.Amount("BTC", "0.5")
+            value_from.linked_to = portfolio.Amount("USDT", "1000.0")
+            value_to = portfolio.Amount("BTC", "1.0")
+            trade = portfolio.Trade(value_from, value_to, "USDT", self.m)
+            trade.inverted = True
+
+            order1 = mock.Mock()
+            order1.filled_amount.return_value = portfolio.Amount("BTC", "0.3")
+
+            order2 = mock.Mock()
+            order2.filled_amount.return_value = portfolio.Amount("BTC", "0.01")
+            trade.orders.append(order1)
+            trade.orders.append(order2)
+
+            self.assertFalse(trade.is_fullfiled)
+
+            order3 = mock.Mock()
+            order3.filled_amount.return_value = portfolio.Amount("BTC", "0.19")
+            trade.orders.append(order3)
+
+            self.assertTrue(trade.is_fullfiled)
+
+            order1.filled_amount.assert_called_with(in_base_currency=False)
+            order2.filled_amount.assert_called_with(in_base_currency=False)
+            order3.filled_amount.assert_called_with(in_base_currency=False)
 
-        self.assertTrue(trade.is_fullfiled)
 
     def test_filled_amount(self):
         value_from = portfolio.Amount("BTC", "0.5")
@@ -2061,6 +2630,21 @@ class TradeTest(WebMockTestCase):
                     D("125"), "FOO", "long", self.m,
                     trade, close_if_possible=False)
 
+    def test_tick_actions_recreate(self):
+        value_from = portfolio.Amount("BTC", "0.5")
+        value_from.linked_to = portfolio.Amount("ETH", "10.0")
+        value_to = portfolio.Amount("BTC", "1.0")
+        trade = portfolio.Trade(value_from, value_to, "ETH", self.m)
+
+        self.assertEqual("average", trade.tick_actions_recreate(0))
+        self.assertEqual("foo", trade.tick_actions_recreate(0, default="foo"))
+        self.assertEqual("average", trade.tick_actions_recreate(1))
+        self.assertEqual(trade.tick_actions[2][1], trade.tick_actions_recreate(2))
+        self.assertEqual(trade.tick_actions[2][1], trade.tick_actions_recreate(3))
+        self.assertEqual(trade.tick_actions[5][1], trade.tick_actions_recreate(5))
+        self.assertEqual(trade.tick_actions[5][1], trade.tick_actions_recreate(6))
+        self.assertEqual("default", trade.tick_actions_recreate(7))
+        self.assertEqual("default", trade.tick_actions_recreate(8))
 
     @mock.patch.object(portfolio.Trade, "prepare_order")
     def test_update_order(self, prepare_order):
@@ -2463,12 +3047,12 @@ class OrderTest(WebMockTestCase):
         self.m.ccxt.privatePostReturnOrderTrades.return_value = [
                 {
                     "tradeID": 42, "type": "buy", "fee": "0.0015",
-                    "date": "2017-12-30 12:00:12", "rate": "0.1",
+                    "date": "2017-12-30 13:00:12", "rate": "0.1",
                     "amount": "3", "total": "0.3"
                     },
                 {
                     "tradeID": 43, "type": "buy", "fee": "0.0015",
-                    "date": "2017-12-30 13:00:12", "rate": "0.2",
+                    "date": "2017-12-30 12:00:12", "rate": "0.2",
                     "amount": "2", "total": "0.4"
                     }
             ]
@@ -2481,8 +3065,8 @@ class OrderTest(WebMockTestCase):
 
         self.m.ccxt.privatePostReturnOrderTrades.assert_called_with({"orderNumber": 12})
         self.assertEqual(2, len(order.mouvements))
-        self.assertEqual(42, order.mouvements[0].id)
-        self.assertEqual(43, order.mouvements[1].id)
+        self.assertEqual(43, order.mouvements[0].id)
+        self.assertEqual(42, order.mouvements[1].id)
 
         self.m.ccxt.privatePostReturnOrderTrades.side_effect = portfolio.ExchangeError
         order = portfolio.Order("buy", portfolio.Amount("ETH", 10),
@@ -2538,7 +3122,9 @@ class OrderTest(WebMockTestCase):
         self.m.report.log_debug_action.assert_called_once()
 
     @mock.patch.object(portfolio.Order, "fetch_mouvements")
-    def test_fetch(self, fetch_mouvements):
+    @mock.patch.object(portfolio.Order, "mark_disappeared_order")
+    @mock.patch.object(portfolio.Order, "mark_finished_order")
+    def test_fetch(self, mark_finished_order, mark_disappeared_order, fetch_mouvements):
         order = portfolio.Order("buy", portfolio.Amount("ETH", 10),
                 D("0.1"), "BTC", "long", self.m, "trade")
         order.id = 45
@@ -2548,6 +3134,8 @@ class OrderTest(WebMockTestCase):
             self.m.report.log_debug_action.assert_called_once()
             self.m.report.log_debug_action.reset_mock()
             self.m.ccxt.fetch_order.assert_not_called()
+            mark_finished_order.assert_not_called()
+            mark_disappeared_order.assert_not_called()
             fetch_mouvements.assert_not_called()
 
         with self.subTest(debug=False):
@@ -2564,17 +3152,102 @@ class OrderTest(WebMockTestCase):
             self.assertEqual("timestamp", order.timestamp)
             self.assertEqual(1, len(order.results))
             self.m.report.log_debug_action.assert_not_called()
+            mark_finished_order.assert_called_once()
+            mark_disappeared_order.assert_called_once()
 
+            mark_finished_order.reset_mock()
             with self.subTest(missing_order=True):
                 self.m.ccxt.fetch_order.side_effect = [
                         portfolio.OrderNotCached,
                         ]
                 order.fetch()
                 self.assertEqual("closed_unknown", order.status)
+                mark_finished_order.assert_called_once()
+
+    def test_mark_disappeared_order(self):
+        with self.subTest("Open order"):
+            order = portfolio.Order("buy", portfolio.Amount("ETH", 10),
+                    D("0.1"), "BTC", "long", self.m, "trade")
+            order.id = 45
+            order.mouvements.append(portfolio.Mouvement("XRP", "BTC", {
+                "tradeID":21336541,
+                "currencyPair":"BTC_XRP",
+                "type":"sell",
+                "rate":"0.00007013",
+                "amount":"0.00000222",
+                "total":"0.00000000",
+                "fee":"0.00150000",
+                "date":"2018-04-02 00:09:13"
+                }))
+            order.mark_disappeared_order()
+            self.assertEqual("pending", order.status)
+
+        with self.subTest("Non-zero amount"):
+            order = portfolio.Order("buy", portfolio.Amount("ETH", 10),
+                    D("0.1"), "BTC", "long", self.m, "trade")
+            order.id = 45
+            order.status = "closed"
+            order.mouvements.append(portfolio.Mouvement("XRP", "BTC", {
+                "tradeID":21336541,
+                "currencyPair":"BTC_XRP",
+                "type":"sell",
+                "rate":"0.00007013",
+                "amount":"0.00000222",
+                "total":"0.00000010",
+                "fee":"0.00150000",
+                "date":"2018-04-02 00:09:13"
+                }))
+            order.mark_disappeared_order()
+            self.assertEqual("closed", order.status)
+
+        with self.subTest("Other mouvements"):
+            order = portfolio.Order("buy", portfolio.Amount("ETH", 10),
+                    D("0.1"), "BTC", "long", self.m, "trade")
+            order.id = 45
+            order.status = "closed"
+            order.mouvements.append(portfolio.Mouvement("XRP", "BTC", {
+                "tradeID":21336541,
+                "currencyPair":"BTC_XRP",
+                "type":"sell",
+                "rate":"0.00007013",
+                "amount":"0.00000222",
+                "total":"0.00000001",
+                "fee":"0.00150000",
+                "date":"2018-04-02 00:09:13"
+                }))
+            order.mouvements.append(portfolio.Mouvement("XRP", "BTC", {
+                "tradeID":21336541,
+                "currencyPair":"BTC_XRP",
+                "type":"sell",
+                "rate":"0.00007013",
+                "amount":"0.00000222",
+                "total":"0.00000000",
+                "fee":"0.00150000",
+                "date":"2018-04-02 00:09:13"
+                }))
+            order.mark_disappeared_order()
+            self.assertEqual("error_disappeared", order.status)
+
+        with self.subTest("Order disappeared"):
+            order = portfolio.Order("buy", portfolio.Amount("ETH", 10),
+                    D("0.1"), "BTC", "long", self.m, "trade")
+            order.id = 45
+            order.status = "closed"
+            order.mouvements.append(portfolio.Mouvement("XRP", "BTC", {
+                "tradeID":21336541,
+                "currencyPair":"BTC_XRP",
+                "type":"sell",
+                "rate":"0.00007013",
+                "amount":"0.00000222",
+                "total":"0.00000000",
+                "fee":"0.00150000",
+                "date":"2018-04-02 00:09:13"
+                }))
+            order.mark_disappeared_order()
+            self.assertEqual("error_disappeared", order.status)
 
     @mock.patch.object(portfolio.Order, "fetch")
-    @mock.patch.object(portfolio.Order, "mark_finished_order")
-    def test_get_status(self, mark_finished_order, fetch):
+    def test_get_status(self, fetch):
         with self.subTest(debug=True):
             self.m.debug = True
             order = portfolio.Order("buy", portfolio.Amount("ETH", 10),
@@ -2593,10 +3266,8 @@ class OrderTest(WebMockTestCase):
                 return update_status
             fetch.side_effect = _fetch(order)
             self.assertEqual("open", order.get_status())
-            mark_finished_order.assert_not_called()
             fetch.assert_called_once()
 
-        mark_finished_order.reset_mock()
         fetch.reset_mock()
         with self.subTest(debug=False, finished=True):
             self.m.debug = False
@@ -2608,7 +3279,6 @@ class OrderTest(WebMockTestCase):
                 return update_status
             fetch.side_effect = _fetch(order)
             self.assertEqual("closed", order.get_status())
-            mark_finished_order.assert_called_once()
             fetch.assert_called_once()
 
     def test_run(self):
@@ -2714,6 +3384,549 @@ class OrderTest(WebMockTestCase):
             self.assertEqual(5, self.m.report.log_error.call_count)
             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)
 
+        self.m.reset_mock()
+        with self.subTest(invalid_nonce=True):
+            with self.subTest(retry_success=True):
+                order = portfolio.Order("buy", portfolio.Amount("ETH", "0.001"),
+                        D("0.1"), "BTC", "long", self.m, "trade")
+                self.m.ccxt.create_order.side_effect = [
+                        portfolio.InvalidNonce,
+                        portfolio.InvalidNonce,
+                        { "id": 123 },
+                        ]
+                order.run()
+                self.m.ccxt.create_order.assert_has_calls([
+                    mock.call('ETH/BTC', 'limit', 'buy', D('0.0010'), account='exchange', price=D('0.1')),
+                    mock.call('ETH/BTC', 'limit', 'buy', D('0.0010'), account='exchange', price=D('0.1')),
+                    mock.call('ETH/BTC', 'limit', 'buy', D('0.0010'), account='exchange', price=D('0.1')),
+                ])
+                self.assertEqual(3, self.m.ccxt.create_order.call_count)
+                self.assertEqual(3, order.tries)
+                self.m.report.log_error.assert_called()
+                self.assertEqual(2, self.m.report.log_error.call_count)
+                self.m.report.log_error.assert_called_with(mock.ANY, message="Retrying after invalid nonce", exception=mock.ANY)
+                self.assertEqual(123, order.id)
+
+            self.m.reset_mock()
+            with self.subTest(retry_success=False):
+                order = portfolio.Order("buy", portfolio.Amount("ETH", "0.001"),
+                        D("0.1"), "BTC", "long", self.m, "trade")
+                self.m.ccxt.create_order.side_effect = [
+                        portfolio.InvalidNonce,
+                        portfolio.InvalidNonce,
+                        portfolio.InvalidNonce,
+                        portfolio.InvalidNonce,
+                        portfolio.InvalidNonce,
+                        ]
+                order.run()
+                self.assertEqual(5, self.m.ccxt.create_order.call_count)
+                self.assertEqual(5, order.tries)
+                self.m.report.log_error.assert_called()
+                self.assertEqual(5, self.m.report.log_error.call_count)
+                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 invalid nonce", exception=mock.ANY)
+                self.assertEqual("error", order.status)
+
+        self.m.reset_mock()
+        with self.subTest(request_timeout=True):
+            order = portfolio.Order("buy", portfolio.Amount("ETH", "0.001"),
+                    D("0.1"), "BTC", "long", self.m, "trade")
+            with self.subTest(retrieved=False), \
+                    mock.patch.object(order, "retrieve_order") as retrieve:
+                self.m.ccxt.create_order.side_effect = [
+                        portfolio.RequestTimeout,
+                        portfolio.RequestTimeout,
+                        { "id": 123 },
+                        ]
+                retrieve.return_value = False
+                order.run()
+                self.m.ccxt.create_order.assert_has_calls([
+                    mock.call('ETH/BTC', 'limit', 'buy', D('0.0010'), account='exchange', price=D('0.1')),
+                    mock.call('ETH/BTC', 'limit', 'buy', D('0.0010'), account='exchange', price=D('0.1')),
+                    mock.call('ETH/BTC', 'limit', 'buy', D('0.0010'), account='exchange', price=D('0.1')),
+                ])
+                self.assertEqual(3, self.m.ccxt.create_order.call_count)
+                self.assertEqual(3, order.tries)
+                self.m.report.log_error.assert_called()
+                self.assertEqual(2, self.m.report.log_error.call_count)
+                self.m.report.log_error.assert_called_with(mock.ANY, message="Retrying after timeout", exception=mock.ANY)
+                self.assertEqual(123, order.id)
+
+            self.m.reset_mock()
+            order = portfolio.Order("buy", portfolio.Amount("ETH", "0.001"),
+                    D("0.1"), "BTC", "long", self.m, "trade")
+            with self.subTest(retrieved=True), \
+                    mock.patch.object(order, "retrieve_order") as retrieve:
+                self.m.ccxt.create_order.side_effect = [
+                        portfolio.RequestTimeout,
+                        ]
+                def _retrieve():
+                    order.results.append({"id": 123})
+                    return True
+                retrieve.side_effect = _retrieve
+                order.run()
+                self.m.ccxt.create_order.assert_has_calls([
+                    mock.call('ETH/BTC', 'limit', 'buy', D('0.0010'), account='exchange', price=D('0.1')),
+                ])
+                self.assertEqual(1, self.m.ccxt.create_order.call_count)
+                self.assertEqual(1, order.tries)
+                self.m.report.log_error.assert_called()
+                self.assertEqual(1, self.m.report.log_error.call_count)
+                self.m.report.log_error.assert_called_with(mock.ANY, message="Timeout, found the order")
+                self.assertEqual(123, order.id)
+
+            self.m.reset_mock()
+            order = portfolio.Order("buy", portfolio.Amount("ETH", "0.001"),
+                    D("0.1"), "BTC", "long", self.m, "trade")
+            with self.subTest(retrieved=False), \
+                    mock.patch.object(order, "retrieve_order") as retrieve:
+                self.m.ccxt.create_order.side_effect = [
+                        portfolio.RequestTimeout,
+                        portfolio.RequestTimeout,
+                        portfolio.RequestTimeout,
+                        portfolio.RequestTimeout,
+                        portfolio.RequestTimeout,
+                        ]
+                retrieve.return_value = False
+                order.run()
+                self.m.ccxt.create_order.assert_has_calls([
+                    mock.call('ETH/BTC', 'limit', 'buy', D('0.0010'), account='exchange', price=D('0.1')),
+                    mock.call('ETH/BTC', 'limit', 'buy', D('0.0010'), account='exchange', price=D('0.1')),
+                    mock.call('ETH/BTC', 'limit', 'buy', D('0.0010'), account='exchange', price=D('0.1')),
+                    mock.call('ETH/BTC', 'limit', 'buy', D('0.0010'), account='exchange', price=D('0.1')),
+                    mock.call('ETH/BTC', 'limit', 'buy', D('0.0010'), account='exchange', price=D('0.1')),
+                ])
+                self.assertEqual(5, self.m.ccxt.create_order.call_count)
+                self.assertEqual(5, order.tries)
+                self.m.report.log_error.assert_called()
+                self.assertEqual(5, self.m.report.log_error.call_count)
+                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)
+                self.assertEqual("error", order.status)
+
+    def test_retrieve_order(self):
+        with self.subTest(similar_open_order=True):
+            order = portfolio.Order("buy", portfolio.Amount("ETH", "0.001"),
+                    D("0.1"), "BTC", "long", self.m, "trade")
+            order.start_date = datetime.datetime(2018, 3, 25, 15, 15, 55)
+
+            self.m.ccxt.order_precision.return_value = 8
+            self.m.ccxt.fetch_orders.return_value = [
+                    { # Wrong amount
+                        'amount': 0.002, 'cost': 0.1,
+                        'datetime': '2018-03-25T15:15:51.000Z',
+                        'fee': None, 'filled': 0.0,
+                        'id': '1',
+                        'info': {
+                            'amount': '0.002',
+                            'date': '2018-03-25 15:15:51',
+                            'margin': 0, 'orderNumber': '1',
+                            'price': '0.1', 'rate': '0.1',
+                            'side': 'buy', 'startingAmount': '0.002',
+                            'status': 'open', 'total': '0.0002',
+                            'type': 'limit'
+                            },
+                        'price': 0.1, 'remaining': 0.002, 'side': 'buy',
+                        'status': 'open', 'symbol': 'ETH/BTC',
+                        'timestamp': 1521990951000, 'trades': None,
+                        'type': 'limit'
+                        },
+                    { # Margin
+                        'amount': 0.001, 'cost': 0.1,
+                        'datetime': '2018-03-25T15:15:51.000Z',
+                        'fee': None, 'filled': 0.0,
+                        'id': '2',
+                        'info': {
+                            'amount': '0.001',
+                            'date': '2018-03-25 15:15:51',
+                            'margin': 1, 'orderNumber': '2',
+                            'price': '0.1', 'rate': '0.1',
+                            'side': 'buy', 'startingAmount': '0.001',
+                            'status': 'open', 'total': '0.0001',
+                            'type': 'limit'
+                            },
+                        'price': 0.1, 'remaining': 0.001, 'side': 'buy',
+                        'status': 'open', 'symbol': 'ETH/BTC',
+                        'timestamp': 1521990951000, 'trades': None,
+                        'type': 'limit'
+                        },
+                    { # selling
+                        'amount': 0.001, 'cost': 0.1,
+                        'datetime': '2018-03-25T15:15:51.000Z',
+                        'fee': None, 'filled': 0.0,
+                        'id': '3',
+                        'info': {
+                            'amount': '0.001',
+                            'date': '2018-03-25 15:15:51',
+                            'margin': 0, 'orderNumber': '3',
+                            'price': '0.1', 'rate': '0.1',
+                            'side': 'sell', 'startingAmount': '0.001',
+                            'status': 'open', 'total': '0.0001',
+                            'type': 'limit'
+                            },
+                        'price': 0.1, 'remaining': 0.001, 'side': 'sell',
+                        'status': 'open', 'symbol': 'ETH/BTC',
+                        'timestamp': 1521990951000, 'trades': None,
+                        'type': 'limit'
+                        },
+                    { # Wrong rate
+                        'amount': 0.001, 'cost': 0.15,
+                        'datetime': '2018-03-25T15:15:51.000Z',
+                        'fee': None, 'filled': 0.0,
+                        'id': '4',
+                        'info': {
+                            'amount': '0.001',
+                            'date': '2018-03-25 15:15:51',
+                            'margin': 0, 'orderNumber': '4',
+                            'price': '0.15', 'rate': '0.15',
+                            'side': 'buy', 'startingAmount': '0.001',
+                            'status': 'open', 'total': '0.0001',
+                            'type': 'limit'
+                            },
+                        'price': 0.15, 'remaining': 0.001, 'side': 'buy',
+                        'status': 'open', 'symbol': 'ETH/BTC',
+                        'timestamp': 1521990951000, 'trades': None,
+                        'type': 'limit'
+                        },
+                    { # All good
+                        'amount': 0.001, 'cost': 0.1,
+                        'datetime': '2018-03-25T15:15:51.000Z',
+                        'fee': None, 'filled': 0.0,
+                        'id': '5',
+                        'info': {
+                            'amount': '0.001',
+                            'date': '2018-03-25 15:15:51',
+                            'margin': 0, 'orderNumber': '1',
+                            'price': '0.1', 'rate': '0.1',
+                            'side': 'buy', 'startingAmount': '0.001',
+                            'status': 'open', 'total': '0.0001',
+                            'type': 'limit'
+                            },
+                        'price': 0.1, 'remaining': 0.001, 'side': 'buy',
+                        'status': 'open', 'symbol': 'ETH/BTC',
+                        'timestamp': 1521990951000, 'trades': None,
+                        'type': 'limit'
+                        }
+                    ]
+            result = order.retrieve_order()
+            self.assertTrue(result)
+            self.assertEqual('5', order.results[0]["id"])
+            self.m.ccxt.fetch_my_trades.assert_not_called()
+            self.m.ccxt.fetch_orders.assert_called_once_with(symbol="ETH/BTC", since=1521983750)
+
+        self.m.reset_mock()
+        with self.subTest(similar_open_order=False, past_trades=True):
+            order = portfolio.Order("buy", portfolio.Amount("ETH", "0.001"),
+                    D("0.1"), "BTC", "long", self.m, "trade")
+            order.start_date = datetime.datetime(2018, 3, 25, 15, 15, 55)
+
+            self.m.ccxt.order_precision.return_value = 8
+            self.m.ccxt.fetch_orders.return_value = []
+            self.m.ccxt.fetch_my_trades.return_value = [
+                    { # Wrong timestamp 1
+                        'amount': 0.0006,
+                        'cost': 0.00006,
+                        'datetime': '2018-03-25T15:15:14.000Z',
+                        'id': '1-1',
+                        'info': {
+                            'amount': '0.0006',
+                            'category': 'exchange',
+                            'date': '2018-03-25 15:15:14',
+                            'fee': '0.00150000',
+                            'globalTradeID': 1,
+                            'orderNumber': '1',
+                            'rate': '0.1',
+                            'total': '0.00006',
+                            'tradeID': '1-1',
+                            'type': 'buy'
+                            },
+                        'order': '1',
+                        'price': 0.1,
+                        'side': 'buy',
+                        'symbol': 'ETH/BTC',
+                        'timestamp': 1521983714,
+                        'type': 'limit'
+                        },
+                    { # Wrong timestamp 2
+                        'amount': 0.0004,
+                        'cost': 0.00004,
+                        'datetime': '2018-03-25T15:16:54.000Z',
+                        'id': '1-2',
+                        'info': {
+                            'amount': '0.0004',
+                            'category': 'exchange',
+                            'date': '2018-03-25 15:16:54',
+                            'fee': '0.00150000',
+                            'globalTradeID': 2,
+                            'orderNumber': '1',
+                            'rate': '0.1',
+                            'total': '0.00004',
+                            'tradeID': '1-2',
+                            'type': 'buy'
+                            },
+                        'order': '1',
+                        'price': 0.1,
+                        'side': 'buy',
+                        'symbol': 'ETH/BTC',
+                        'timestamp': 1521983814,
+                        'type': 'limit'
+                        },
+                    { # Wrong side 1
+                        'amount': 0.0006,
+                        'cost': 0.00006,
+                        'datetime': '2018-03-25T15:15:54.000Z',
+                        'id': '2-1',
+                        'info': {
+                            'amount': '0.0006',
+                            'category': 'exchange',
+                            'date': '2018-03-25 15:15:54',
+                            'fee': '0.00150000',
+                            'globalTradeID': 1,
+                            'orderNumber': '2',
+                            'rate': '0.1',
+                            'total': '0.00006',
+                            'tradeID': '2-1',
+                            'type': 'sell'
+                            },
+                        'order': '2',
+                        'price': 0.1,
+                        'side': 'sell',
+                        'symbol': 'ETH/BTC',
+                        'timestamp': 1521983754,
+                        'type': 'limit'
+                        },
+                    { # Wrong side 2
+                        'amount': 0.0004,
+                        'cost': 0.00004,
+                        'datetime': '2018-03-25T15:16:54.000Z',
+                        'id': '2-2',
+                        'info': {
+                            'amount': '0.0004',
+                            'category': 'exchange',
+                            'date': '2018-03-25 15:16:54',
+                            'fee': '0.00150000',
+                            'globalTradeID': 2,
+                            'orderNumber': '2',
+                            'rate': '0.1',
+                            'total': '0.00004',
+                            'tradeID': '2-2',
+                            'type': 'buy'
+                            },
+                        'order': '2',
+                        'price': 0.1,
+                        'side': 'buy',
+                        'symbol': 'ETH/BTC',
+                        'timestamp': 1521983814,
+                        'type': 'limit'
+                        },
+                    { # Margin trade 1
+                        'amount': 0.0006,
+                        'cost': 0.00006,
+                        'datetime': '2018-03-25T15:15:54.000Z',
+                        'id': '3-1',
+                        'info': {
+                            'amount': '0.0006',
+                            'category': 'marginTrade',
+                            'date': '2018-03-25 15:15:54',
+                            'fee': '0.00150000',
+                            'globalTradeID': 1,
+                            'orderNumber': '3',
+                            'rate': '0.1',
+                            'total': '0.00006',
+                            'tradeID': '3-1',
+                            'type': 'buy'
+                            },
+                        'order': '3',
+                        'price': 0.1,
+                        'side': 'buy',
+                        'symbol': 'ETH/BTC',
+                        'timestamp': 1521983754,
+                        'type': 'limit'
+                        },
+                    { # Margin trade 2
+                        'amount': 0.0004,
+                        'cost': 0.00004,
+                        'datetime': '2018-03-25T15:16:54.000Z',
+                        'id': '3-2',
+                        'info': {
+                            'amount': '0.0004',
+                            'category': 'marginTrade',
+                            'date': '2018-03-25 15:16:54',
+                            'fee': '0.00150000',
+                            'globalTradeID': 2,
+                            'orderNumber': '3',
+                            'rate': '0.1',
+                            'total': '0.00004',
+                            'tradeID': '3-2',
+                            'type': 'buy'
+                            },
+                        'order': '3',
+                        'price': 0.1,
+                        'side': 'buy',
+                        'symbol': 'ETH/BTC',
+                        'timestamp': 1521983814,
+                        'type': 'limit'
+                        },
+                    { # Wrong amount 1
+                        'amount': 0.0005,
+                        'cost': 0.00005,
+                        'datetime': '2018-03-25T15:15:54.000Z',
+                        'id': '4-1',
+                        'info': {
+                            'amount': '0.0005',
+                            'category': 'exchange',
+                            'date': '2018-03-25 15:15:54',
+                            'fee': '0.00150000',
+                            'globalTradeID': 1,
+                            'orderNumber': '4',
+                            'rate': '0.1',
+                            'total': '0.00005',
+                            'tradeID': '4-1',
+                            'type': 'buy'
+                            },
+                        'order': '4',
+                        'price': 0.1,
+                        'side': 'buy',
+                        'symbol': 'ETH/BTC',
+                        'timestamp': 1521983754,
+                        'type': 'limit'
+                        },
+                    { # Wrong amount 2
+                        'amount': 0.0004,
+                        'cost': 0.00004,
+                        'datetime': '2018-03-25T15:16:54.000Z',
+                        'id': '4-2',
+                        'info': {
+                            'amount': '0.0004',
+                            'category': 'exchange',
+                            'date': '2018-03-25 15:16:54',
+                            'fee': '0.00150000',
+                            'globalTradeID': 2,
+                            'orderNumber': '4',
+                            'rate': '0.1',
+                            'total': '0.00004',
+                            'tradeID': '4-2',
+                            'type': 'buy'
+                            },
+                        'order': '4',
+                        'price': 0.1,
+                        'side': 'buy',
+                        'symbol': 'ETH/BTC',
+                        'timestamp': 1521983814,
+                        'type': 'limit'
+                        },
+                    { # Wrong price 1
+                        'amount': 0.0006,
+                        'cost': 0.000066,
+                        'datetime': '2018-03-25T15:15:54.000Z',
+                        'id': '5-1',
+                        'info': {
+                            'amount': '0.0006',
+                            'category': 'exchange',
+                            'date': '2018-03-25 15:15:54',
+                            'fee': '0.00150000',
+                            'globalTradeID': 1,
+                            'orderNumber': '5',
+                            'rate': '0.11',
+                            'total': '0.000066',
+                            'tradeID': '5-1',
+                            'type': 'buy'
+                            },
+                        'order': '5',
+                        'price': 0.11,
+                        'side': 'buy',
+                        'symbol': 'ETH/BTC',
+                        'timestamp': 1521983754,
+                        'type': 'limit'
+                        },
+                    { # Wrong price 2
+                        'amount': 0.0004,
+                        'cost': 0.00004,
+                        'datetime': '2018-03-25T15:16:54.000Z',
+                        'id': '5-2',
+                        'info': {
+                            'amount': '0.0004',
+                            'category': 'exchange',
+                            'date': '2018-03-25 15:16:54',
+                            'fee': '0.00150000',
+                            'globalTradeID': 2,
+                            'orderNumber': '5',
+                            'rate': '0.1',
+                            'total': '0.00004',
+                            'tradeID': '5-2',
+                            'type': 'buy'
+                            },
+                        'order': '5',
+                        'price': 0.1,
+                        'side': 'buy',
+                        'symbol': 'ETH/BTC',
+                        'timestamp': 1521983814,
+                        'type': 'limit'
+                        },
+                    { # All good 1
+                        'amount': 0.0006,
+                        'cost': 0.00006,
+                        'datetime': '2018-03-25T15:15:54.000Z',
+                        'id': '7-1',
+                        'info': {
+                            'amount': '0.0006',
+                            'category': 'exchange',
+                            'date': '2018-03-25 15:15:54',
+                            'fee': '0.00150000',
+                            'globalTradeID': 1,
+                            'orderNumber': '7',
+                            'rate': '0.1',
+                            'total': '0.00006',
+                            'tradeID': '7-1',
+                            'type': 'buy'
+                            },
+                        'order': '7',
+                        'price': 0.1,
+                        'side': 'buy',
+                        'symbol': 'ETH/BTC',
+                        'timestamp': 1521983754,
+                        'type': 'limit'
+                        },
+                    { # All good 2
+                        'amount': 0.0004,
+                        'cost': 0.000036,
+                        'datetime': '2018-03-25T15:16:54.000Z',
+                        'id': '7-2',
+                        'info': {
+                            'amount': '0.0004',
+                            'category': 'exchange',
+                            'date': '2018-03-25 15:16:54',
+                            'fee': '0.00150000',
+                            'globalTradeID': 2,
+                            'orderNumber': '7',
+                            'rate': '0.09',
+                            'total': '0.000036',
+                            'tradeID': '7-2',
+                            'type': 'buy'
+                            },
+                        'order': '7',
+                        'price': 0.09,
+                        'side': 'buy',
+                        'symbol': 'ETH/BTC',
+                        'timestamp': 1521983814,
+                        'type': 'limit'
+                        },
+                    ]
+
+            result = order.retrieve_order()
+            self.assertTrue(result)
+            self.assertEqual('7', order.results[0]["id"])
+            self.m.ccxt.fetch_orders.assert_called_once_with(symbol="ETH/BTC", since=1521983750)
+
+        self.m.reset_mock()
+        with self.subTest(similar_open_order=False, past_trades=False):
+            order = portfolio.Order("buy", portfolio.Amount("ETH", "0.001"),
+                    D("0.1"), "BTC", "long", self.m, "trade")
+            order.start_date = datetime.datetime(2018, 3, 25, 15, 15, 55)
+
+            self.m.ccxt.order_precision.return_value = 8
+            self.m.ccxt.fetch_orders.return_value = []
+            self.m.ccxt.fetch_my_trades.return_value = []
+            result = order.retrieve_order()
+            self.assertFalse(result)
 
 @unittest.skipUnless("unit" in limits, "Unit skipped")
 class MouvementTest(WebMockTestCase):
@@ -2803,15 +4016,18 @@ class ReportStoreTest(WebMockTestCase):
 
         self.assertEqual(3, len(report_store1.logs))
         self.assertEqual(["1", "2", "3"], list(map(lambda x: x["stage"], report_store1.logs)))
+        self.assertEqual(6, len(report_store1.print_logs))
 
     def test_print_log(self):
         report_store = market.ReportStore(self.m)
         with self.subTest(verbose=True),\
+                mock.patch.object(store, "datetime") as time_mock,\
                 mock.patch('sys.stdout', new_callable=StringIO) as stdout_mock:
+            time_mock.now.return_value = datetime.datetime(2018, 2, 25, 2, 20, 10)
             report_store.set_verbose(True)
             report_store.print_log("Coucou")
             report_store.print_log(portfolio.Amount("BTC", 1))
-            self.assertEqual(stdout_mock.getvalue(), "Coucou\n1.00000000 BTC\n")
+            self.assertEqual(stdout_mock.getvalue(), "2018-02-25 02:20:10: Coucou\n2018-02-25 02:20:10: 1.00000000 BTC\n")
 
         with self.subTest(verbose=False),\
                 mock.patch('sys.stdout', new_callable=StringIO) as stdout_mock:
@@ -2820,6 +4036,14 @@ class ReportStoreTest(WebMockTestCase):
             report_store.print_log(portfolio.Amount("BTC", 1))
             self.assertEqual(stdout_mock.getvalue(), "")
 
+    def test_default_json_serial(self):
+        report_store = market.ReportStore(self.m)
+
+        self.assertEqual("2018-02-24T00:00:00",
+                report_store.default_json_serial(portfolio.datetime(2018, 2, 24)))
+        self.assertEqual("1.00000000 BTC",
+                report_store.default_json_serial(portfolio.Amount("BTC", 1)))
+
     def test_to_json(self):
         report_store = market.ReportStore(self.m)
         report_store.logs.append({"foo": "bar"})
@@ -2829,6 +4053,20 @@ class ReportStoreTest(WebMockTestCase):
         report_store.logs.append({"amount": portfolio.Amount("BTC", 1)})
         self.assertEqual('[\n  {\n    "foo": "bar"\n  },\n  {\n    "date": "2018-02-24T00:00:00"\n  },\n  {\n    "amount": "1.00000000 BTC"\n  }\n]', report_store.to_json())
 
+    def test_to_json_array(self):
+        report_store = market.ReportStore(self.m)
+        report_store.logs.append({
+            "date": "date1", "type": "type1", "foo": "bar", "bla": "bla"
+            })
+        report_store.logs.append({
+            "date": "date2", "type": "type2", "foo": "bar", "bla": "bla"
+            })
+        logs = list(report_store.to_json_array())
+
+        self.assertEqual(2, len(logs))
+        self.assertEqual(("date1", "type1", '{\n  "foo": "bar",\n  "bla": "bla"\n}'), logs[0])
+        self.assertEqual(("date2", "type2", '{\n  "foo": "bar",\n  "bla": "bla"\n}'), logs[1])
+
     @mock.patch.object(market.ReportStore, "print_log")
     @mock.patch.object(market.ReportStore, "add_log")
     def test_log_stage(self, add_log, print_log):
@@ -3145,6 +4383,34 @@ class ReportStoreTest(WebMockTestCase):
             'response': 'Hey'
             })
 
+        add_log.reset_mock()
+        report_store.log_http_request("method", "url", "body",
+                "headers", ValueError("Foo"))
+        add_log.assert_called_once_with({
+            'type': 'http_request',
+            'method': 'method',
+            'url': 'url',
+            'body': 'body',
+            'headers': 'headers',
+            'status': -1,
+            'response': None,
+            'error': 'ValueError',
+            'error_message': 'Foo',
+            })
+
+    @mock.patch.object(market.ReportStore, "add_log")
+    def test_log_market(self, add_log):
+        report_store = market.ReportStore(self.m)
+
+        report_store.log_market(self.market_args(debug=True, quiet=False), 4, 1)
+        add_log.assert_called_once_with({
+            "type": "market",
+            "commit": "$Format:%H$",
+            "args": { "report_path": None, "debug": True, "quiet": False },
+            "user_id": 4,
+            "market_id": 1,
+            })
+
     @mock.patch.object(market.ReportStore, "print_log")
     @mock.patch.object(market.ReportStore, "add_log")
     def test_log_error(self, add_log, print_log):
@@ -3322,7 +4588,7 @@ class MainTest(WebMockTestCase):
                 mock.patch("main.parse_config") as main_parse_config:
             with self.subTest(debug=False):
                 main_parse_config.return_value = ["pg_config", "report_path"]
-                main_fetch_markets.return_value = [({"key": "market_config"},)]
+                main_fetch_markets.return_value = [(1, {"key": "market_config"}, 3)]
                 m = main.get_user_market("config_path.ini", 1)
 
                 self.assertIsInstance(m, market.Market)
@@ -3330,7 +4596,7 @@ class MainTest(WebMockTestCase):
 
             with self.subTest(debug=True):
                 main_parse_config.return_value = ["pg_config", "report_path"]
-                main_fetch_markets.return_value = [({"key": "market_config"},)]
+                main_fetch_markets.return_value = [(1, {"key": "market_config"}, 3)]
                 m = main.get_user_market("config_path.ini", 1, debug=True)
 
                 self.assertIsInstance(m, market.Market)
@@ -3349,95 +4615,114 @@ class MainTest(WebMockTestCase):
             args_mock.after = "after"
             self.assertEqual("", stdout_mock.getvalue())
 
-            main.process("config", 1, "report_path", args_mock)
+            main.process("config", 3, 1, args_mock, "pg_config")
 
             market_mock.from_config.assert_has_calls([
-                mock.call("config", debug="debug", user_id=1, report_path="report_path"),
+                mock.call("config", args_mock, pg_config="pg_config", market_id=3, user_id=1),
                 mock.call().process("action", before="before", after="after"),
                 ])
 
             with self.subTest(exception=True):
                 market_mock.from_config.side_effect = Exception("boo")
-                main.process("config", 1, "report_path", args_mock)
+                main.process(3, "config", 1, args_mock, "pg_config")
                 self.assertEqual("Exception: boo\n", stdout_mock.getvalue())
 
     def test_main(self):
-        with mock.patch("main.parse_args") as parse_args,\
-                mock.patch("main.parse_config") as parse_config,\
-                mock.patch("main.fetch_markets") as fetch_markets,\
-                mock.patch("main.process") as process:
+        with self.subTest(parallel=False):
+            with mock.patch("main.parse_args") as parse_args,\
+                    mock.patch("main.parse_config") as parse_config,\
+                    mock.patch("main.fetch_markets") as fetch_markets,\
+                    mock.patch("main.process") as process:
 
-            args_mock = mock.Mock()
-            args_mock.config = "config"
-            args_mock.user = "user"
-            parse_args.return_value = args_mock
+                args_mock = mock.Mock()
+                args_mock.parallel = False
+                args_mock.user = "user"
+                parse_args.return_value = args_mock
 
-            parse_config.return_value = ["pg_config", "report_path"]
+                parse_config.return_value = "pg_config"
 
-            fetch_markets.return_value = [["config1", 1], ["config2", 2]]
+                fetch_markets.return_value = [[3, "config1", 1], [1, "config2", 2]]
 
-            main.main(["Foo", "Bar"])
+                main.main(["Foo", "Bar"])
 
-            parse_args.assert_called_with(["Foo", "Bar"])
-            parse_config.assert_called_with("config")
-            fetch_markets.assert_called_with("pg_config", "user")
+                parse_args.assert_called_with(["Foo", "Bar"])
+                parse_config.assert_called_with(args_mock)
+                fetch_markets.assert_called_with("pg_config", "user")
 
-            self.assertEqual(2, process.call_count)
-            process.assert_has_calls([
-                mock.call("config1", 1, "report_path", args_mock),
-                mock.call("config2", 2, "report_path", args_mock),
-                ])
+                self.assertEqual(2, process.call_count)
+                process.assert_has_calls([
+                    mock.call("config1", 3, 1, args_mock, "pg_config"),
+                    mock.call("config2", 1, 2, args_mock, "pg_config"),
+                    ])
+        with self.subTest(parallel=True):
+            with mock.patch("main.parse_args") as parse_args,\
+                    mock.patch("main.parse_config") as parse_config,\
+                    mock.patch("main.fetch_markets") as fetch_markets,\
+                    mock.patch("main.process") as process,\
+                    mock.patch("store.Portfolio.start_worker") as start:
+
+                args_mock = mock.Mock()
+                args_mock.parallel = True
+                args_mock.user = "user"
+                parse_args.return_value = args_mock
+
+                parse_config.return_value = "pg_config"
+
+                fetch_markets.return_value = [[3, "config1", 1], [1, "config2", 2]]
+
+                main.main(["Foo", "Bar"])
+
+                parse_args.assert_called_with(["Foo", "Bar"])
+                parse_config.assert_called_with(args_mock)
+                fetch_markets.assert_called_with("pg_config", "user")
+
+                start.assert_called_once_with()
+                self.assertEqual(2, process.call_count)
+                process.assert_has_calls([
+                    mock.call.__bool__(),
+                    mock.call("config1", 3, 1, args_mock, "pg_config"),
+                    mock.call.__bool__(),
+                    mock.call("config2", 1, 2, args_mock, "pg_config"),
+                    ])
 
     @mock.patch.object(main.sys, "exit")
-    @mock.patch("main.configparser")
     @mock.patch("main.os")
-    def test_parse_config(self, os, configparser, exit):
-        with self.subTest(pg_config=True, report_path=None):
-            config_mock = mock.MagicMock()
-            configparser.ConfigParser.return_value = config_mock
-            def config(element):
-                return element == "postgresql"
-
-            config_mock.__contains__.side_effect = config
-            config_mock.__getitem__.return_value = "pg_config"
-
-            result = main.parse_config("configfile")
-
-            config_mock.read.assert_called_with("configfile")
-
-            self.assertEqual(["pg_config", None], result)
-
-        with self.subTest(pg_config=True, report_path="present"):
-            config_mock = mock.MagicMock()
-            configparser.ConfigParser.return_value = config_mock
+    def test_parse_config(self, os, exit):
+        with self.subTest(report_path=None):
+            args = main.configargparse.Namespace(**{
+                "db_host": "host",
+                "db_port": "port",
+                "db_user": "user",
+                "db_password": "password",
+                "db_database": "database",
+                "report_path": None,
+                })
 
-            config_mock.__contains__.return_value = True
-            config_mock.__getitem__.side_effect = [
-                    {"report_path": "report_path"},
-                    {"report_path": "report_path"},
-                    "pg_config",
-                    ]
+            result = main.parse_config(args)
+            self.assertEqual({ "host": "host", "port": "port", "user":
+                "user", "password": "password", "database": "database"
+                }, result)
+            with self.assertRaises(AttributeError):
+                args.db_password
+
+        with self.subTest(report_path="present"):
+            args = main.configargparse.Namespace(**{
+                "db_host": "host",
+                "db_port": "port",
+                "db_user": "user",
+                "db_password": "password",
+                "db_database": "database",
+                "report_path": "report_path",
+                })
 
             os.path.exists.return_value = False
-            result = main.parse_config("configfile")
 
-            config_mock.read.assert_called_with("configfile")
-            self.assertEqual(["pg_config", "report_path"], result)
+            result = main.parse_config(args)
+
             os.path.exists.assert_called_once_with("report_path")
             os.makedirs.assert_called_once_with("report_path")
 
-        with self.subTest(pg_config=False),\
-                mock.patch('sys.stdout', new_callable=StringIO) as stdout_mock:
-            config_mock = mock.MagicMock()
-            configparser.ConfigParser.return_value = config_mock
-            result = main.parse_config("configfile")
-
-            config_mock.read.assert_called_with("configfile")
-            exit.assert_called_once_with(1)
-            self.assertEqual("no configuration for postgresql in config file\n", stdout_mock.getvalue())
-
-    @mock.patch.object(main.sys, "exit")
-    def test_parse_args(self, exit):
+    def test_parse_args(self):
         with self.subTest(config="config.ini"):
             args = main.parse_args([])
             self.assertEqual("config.ini", args.config)
@@ -3450,13 +4735,10 @@ class MainTest(WebMockTestCase):
             self.assertTrue(args.after)
             self.assertTrue(args.debug)
 
-            exit.assert_not_called()
-
-        with self.subTest(config="inexistant"),\
-                mock.patch('sys.stdout', new_callable=StringIO) as stdout_mock:
+        with self.subTest(config="inexistant"), \
+                self.assertRaises(SystemExit), \
+                mock.patch('sys.stderr', new_callable=StringIO) as stdout_mock:
             args = main.parse_args(["--config", "foo.bar"])
-            exit.assert_called_once_with(1)
-            self.assertEqual("no config file found, exiting\n", stdout_mock.getvalue())
 
     @mock.patch.object(main, "psycopg2")
     def test_fetch_markets(self, psycopg2):
@@ -3471,7 +4753,7 @@ class MainTest(WebMockTestCase):
             rows = list(main.fetch_markets({"foo": "bar"}, None))
 
             psycopg2.connect.assert_called_once_with(foo="bar")
-            cursor_mock.execute.assert_called_once_with("SELECT config,user_id FROM market_configs")
+            cursor_mock.execute.assert_called_once_with("SELECT id,config,user_id FROM market_configs")
 
             self.assertEqual(["row_1", "row_2"], rows)
 
@@ -3481,7 +4763,7 @@ class MainTest(WebMockTestCase):
             rows = list(main.fetch_markets({"foo": "bar"}, 1))
 
             psycopg2.connect.assert_called_once_with(foo="bar")
-            cursor_mock.execute.assert_called_once_with("SELECT config,user_id FROM market_configs WHERE user_id = %s", 1)
+            cursor_mock.execute.assert_called_once_with("SELECT id,config,user_id FROM market_configs WHERE user_id = %s", 1)
 
             self.assertEqual(["row_1", "row_2"], rows)
 
@@ -3545,13 +4827,13 @@ class ProcessorTest(WebMockTestCase):
 
     def test_method_arguments(self):
         ccxt = mock.Mock(spec=market.ccxt.poloniexE)
-        m = market.Market(ccxt)
+        m = market.Market(ccxt, self.market_args())
 
         processor = market.Processor(m)
 
         method, arguments = processor.method_arguments("wait_for_recent")
         self.assertEqual(market.Portfolio.wait_for_recent, method)
-        self.assertEqual(["delta"], arguments)
+        self.assertEqual(["delta", "poll"], arguments)
 
         method, arguments = processor.method_arguments("prepare_trades")
         self.assertEqual(m.prepare_trades, method)