]> git.immae.eu Git - perso/Immae/Projets/Cryptomonnaies/Cryptoportfolio/Trader.git/commitdiff
Merge branch 'night_fixes' into dev
authorIsmaël Bouya <ismael.bouya@normalesup.org>
Mon, 12 Mar 2018 01:10:08 +0000 (02:10 +0100)
committerIsmaël Bouya <ismael.bouya@normalesup.org>
Mon, 12 Mar 2018 01:10:08 +0000 (02:10 +0100)
1  2 
portfolio.py
test.py

diff --combined portfolio.py
index 554b34f090b8d179fead2742c8280f2100ded3bd,ed50b570ea2c31d310c6a3d271f37f7f413c43c0..69e37557fd9cd355fbdccabcd1fb993fbf9adef1
@@@ -1,10 -1,87 +1,10 @@@
 -import time
 -from datetime import datetime, timedelta
 +from datetime import datetime
  from decimal import Decimal as D, ROUND_DOWN
 -from json import JSONDecodeError
 -from simplejson.errors import JSONDecodeError as SimpleJSONDecodeError
  from ccxt import ExchangeError, InsufficientFunds, ExchangeNotAvailable, InvalidOrder, OrderNotCached, OrderNotFound
  from retry import retry
 -import requests
  
  # FIXME: correctly handle web call timeouts
  
 -class Portfolio:
 -    URL = "https://cryptoportfolio.io/wp-content/uploads/portfolio/json/cryptoportfolio.json"
 -    liquidities = {}
 -    data = None
 -    last_date = None
 -
 -    @classmethod
 -    def wait_for_recent(cls, market, delta=4):
 -        cls.repartition(market, refetch=True)
 -        while cls.last_date is None or datetime.now() - cls.last_date > timedelta(delta):
 -            time.sleep(30)
 -            market.report.print_log("Attempt to fetch up-to-date cryptoportfolio")
 -            cls.repartition(market, refetch=True)
 -
 -    @classmethod
 -    def repartition(cls, market, liquidity="medium", refetch=False):
 -        cls.parse_cryptoportfolio(market, refetch=refetch)
 -        liquidities = cls.liquidities[liquidity]
 -        return liquidities[cls.last_date]
 -
 -    @classmethod
 -    def get_cryptoportfolio(cls, market):
 -        try:
 -            r = requests.get(cls.URL)
 -            market.report.log_http_request(r.request.method,
 -                    r.request.url, r.request.body, r.request.headers, r)
 -        except Exception as e:
 -            market.report.log_error("get_cryptoportfolio", exception=e)
 -            return
 -        try:
 -            cls.data = r.json(parse_int=D, parse_float=D)
 -        except (JSONDecodeError, SimpleJSONDecodeError):
 -            cls.data = None
 -
 -    @classmethod
 -    def parse_cryptoportfolio(cls, market, refetch=False):
 -        if refetch or cls.data is None:
 -            cls.get_cryptoportfolio(market)
 -
 -        def filter_weights(weight_hash):
 -            if weight_hash[1][0] == 0:
 -                return False
 -            if weight_hash[0] == "_row":
 -                return False
 -            return True
 -
 -        def clean_weights(i):
 -            def clean_weights_(h):
 -                if h[0].endswith("s"):
 -                    return [h[0][0:-1], (h[1][i], "short")]
 -                else:
 -                    return [h[0], (h[1][i], "long")]
 -            return clean_weights_
 -
 -        def parse_weights(portfolio_hash):
 -            weights_hash = portfolio_hash["weights"]
 -            weights = {}
 -            for i in range(len(weights_hash["_row"])):
 -                date = datetime.strptime(weights_hash["_row"][i], "%Y-%m-%d")
 -                weights[date] = dict(filter(
 -                        filter_weights,
 -                        map(clean_weights(i), weights_hash.items())))
 -            return weights
 -
 -        high_liquidity = parse_weights(cls.data["portfolio_1"])
 -        medium_liquidity = parse_weights(cls.data["portfolio_2"])
 -
 -        cls.liquidities = {
 -                "medium": medium_liquidity,
 -                "high":   high_liquidity,
 -                }
 -        cls.last_date = max(max(medium_liquidity.keys()), max(high_liquidity.keys()))
 -
  class Computation:
      computations = {
              "default": lambda x, y: x[y],
@@@ -214,6 -291,7 +214,7 @@@ class Trade
          self.orders = []
          self.market = market
          self.closed = False
+         self.inverted = None
          assert self.value_from.value * self.value_to.value >= 0
          assert self.value_from.currency == self.value_to.currency
          if self.value_from != 0:
          else:
              return "dispose"
  
-     def order_action(self, inverted):
-         if (self.value_from < self.value_to) != inverted:
+     def order_action(self):
+         if (self.value_from < self.value_to) != self.inverted:
              return "buy"
          else:
              return "sell"
  
      @property
      def is_fullfiled(self):
-         return abs(self.filled_amount(in_base_currency=True)) >= abs(self.delta)
+         return abs(self.filled_amount(in_base_currency=(not self.inverted))) >= abs(self.delta)
  
      def filled_amount(self, in_base_currency=False):
          filled_amount = 0
          if self.action is None:
              return None
          ticker = self.market.get_ticker(self.currency, self.base_currency)
-         inverted = ticker["inverted"]
-         if inverted:
+         self.inverted = ticker["inverted"]
+         if self.inverted:
              ticker = ticker["original"]
-         rate = Computation.compute_value(ticker, self.order_action(inverted), compute_value=compute_value)
+         rate = Computation.compute_value(ticker, self.order_action(), compute_value=compute_value)
  
          # FIXME: Dust amount should be removed from there if they werent
          # honored in other sales
          delta_in_base = abs(self.delta)
          # 9 BTC's worth of move (10 - 1 or 1 - 10 depending on case)
  
-         if not inverted:
+         if not self.inverted:
              base_currency = self.base_currency
              # BTC
              if self.action == "dispose":
              self.market.report.log_error("prepare_order", message="Less to do than already filled: {}".format(delta))
              return None
  
-         order = Order(self.order_action(inverted),
+         order = Order(self.order_action(),
              delta, rate, base_currency, self.trade_type,
              self.market, self, close_if_possible=close_if_possible)
          self.orders.append(order)
@@@ -472,7 -550,7 +473,7 @@@ class Order
  
      @property
      def finished(self):
-         return self.status == "closed" or self.status == "canceled" or self.status == "error"
+         return self.status.startswith("closed") or self.status == "canceled" or self.status == "error"
  
      @retry(InsufficientFunds)
      def run(self):
          # other states are "closed" and "canceled"
          if not self.finished:
              self.fetch()
-             if self.finished:
-                 self.mark_finished_order()
          return self.status
  
      def mark_finished_order(self):
-         if self.market.debug:
+         if self.status.startswith("closed") and self.market.debug:
              self.market.report.log_debug_action("Mark {} as finished".format(self))
              return
-         if self.status == "closed":
+         if self.status.startswith("closed"):
              if self.trade_type == "short" and self.action == "buy" and self.close_if_possible:
                  self.market.ccxt.close_margin_position(self.amount.currency, self.base_currency)
  
  
          self.fetch_mouvements()
  
+         self.mark_finished_order()
          # FIXME: consider open order with dust remaining as closed
  
      def dust_amount_remaining(self):
diff --combined test.py
index f61e739d9a3e664cd30bfca1d185d13846c46cee,921af9f1b451595cfc9fb08cd9affaebb3741a47..ac9a6cd55b3ccf81b14f926939b359e14586ca70
+++ b/test.py
@@@ -7,8 -7,7 +7,8 @@@ from unittest import moc
  import requests
  import requests_mock
  from io import StringIO
 -import portfolio, helper, market
 +import threading
 +import portfolio, market, main, store
  
  limits = ["acceptance", "unit"]
  for test_type in limits:
@@@ -33,15 -32,7 +33,15 @@@ class WebMockTestCase(unittest.TestCase
          self.m.debug = False
  
          self.patchers = [
 -                mock.patch.multiple(portfolio.Portfolio, last_date=None, data=None, liquidities={}),
 +                mock.patch.multiple(market.Portfolio,
 +                    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),
                  ]
@@@ -135,632 -126,174 +135,632 @@@ class poloniexETest(unittest.TestCase)
                      }
              self.assertEqual(expected, self.s.margin_summary())
  
 +    def test_create_order(self):
 +        with mock.patch.object(self.s, "create_exchange_order") as exchange,\
 +                mock.patch.object(self.s, "create_margin_order") as margin:
 +            with self.subTest(account="unspecified"):
 +                self.s.create_order("symbol", "type", "side", "amount", price="price", lending_rate="lending_rate", params="params")
 +                exchange.assert_called_once_with("symbol", "type", "side", "amount", price="price", params="params")
 +                margin.assert_not_called()
 +                exchange.reset_mock()
 +                margin.reset_mock()
 +
 +            with self.subTest(account="exchange"):
 +                self.s.create_order("symbol", "type", "side", "amount", account="exchange", price="price", lending_rate="lending_rate", params="params")
 +                exchange.assert_called_once_with("symbol", "type", "side", "amount", price="price", params="params")
 +                margin.assert_not_called()
 +                exchange.reset_mock()
 +                margin.reset_mock()
 +
 +            with self.subTest(account="margin"):
 +                self.s.create_order("symbol", "type", "side", "amount", account="margin", price="price", lending_rate="lending_rate", params="params")
 +                margin.assert_called_once_with("symbol", "type", "side", "amount", lending_rate="lending_rate", price="price", params="params")
 +                exchange.assert_not_called()
 +                exchange.reset_mock()
 +                margin.reset_mock()
 +
 +            with self.subTest(account="unknown"), self.assertRaises(NotImplementedError):
 +                self.s.create_order("symbol", "type", "side", "amount", account="unknown")
 +
 +    def test_parse_ticker(self):
 +        ticker = {
 +                "high24hr": "12",
 +                "low24hr": "10",
 +                "highestBid": "10.5",
 +                "lowestAsk": "11.5",
 +                "last": "11",
 +                "percentChange": "0.1",
 +                "quoteVolume": "10",
 +                "baseVolume": "20"
 +                }
 +        market = {
 +                "symbol": "BTC/ETC"
 +                }
 +        with mock.patch.object(self.s, "milliseconds") as ms:
 +            ms.return_value = 1520292715123
 +            result = self.s.parse_ticker(ticker, market)
 +
 +            expected = {
 +                    "symbol": "BTC/ETC",
 +                    "timestamp": 1520292715123,
 +                    "datetime": "2018-03-05T23:31:55.123Z",
 +                    "high": D("12"),
 +                    "low": D("10"),
 +                    "bid": D("10.5"),
 +                    "ask": D("11.5"),
 +                    "vwap": None,
 +                    "open": None,
 +                    "close": None,
 +                    "first": None,
 +                    "last": D("11"),
 +                    "change": D("0.1"),
 +                    "percentage": None,
 +                    "average": None,
 +                    "baseVolume": D("10"),
 +                    "quoteVolume": D("20"),
 +                    "info": ticker
 +                    }
 +            self.assertEqual(expected, result)
 +
 +    def test_fetch_margin_balance(self):
 +        with mock.patch.object(self.s, "privatePostGetMarginPosition") as get_margin_position:
 +            get_margin_position.return_value = {
 +                    "BTC_DASH": {
 +                        "amount": "-0.1",
 +                        "basePrice": "0.06818560",
 +                        "lendingFees": "0.00000001",
 +                        "liquidationPrice": "0.15107132",
 +                        "pl": "-0.00000371",
 +                        "total": "0.00681856",
 +                        "type": "short"
 +                        },
 +                    "BTC_ETC": {
 +                        "amount": "-0.6",
 +                        "basePrice": "0.1",
 +                        "lendingFees": "0.00000001",
 +                        "liquidationPrice": "0.6",
 +                        "pl": "0.00000371",
 +                        "total": "0.06",
 +                        "type": "short"
 +                        },
 +                    "BTC_ETH": {
 +                        "amount": "0",
 +                        "basePrice": "0",
 +                        "lendingFees": "0",
 +                        "liquidationPrice": "-1",
 +                        "pl": "0",
 +                        "total": "0",
 +                        "type": "none"
 +                        }
 +                    }
 +            balances = self.s.fetch_margin_balance()
 +            self.assertEqual(2, len(balances))
 +            expected = {
 +                "DASH": {
 +                    "amount": D("-0.1"),
 +                    "borrowedPrice": D("0.06818560"),
 +                    "lendingFees": D("1E-8"),
 +                    "pl": D("-0.00000371"),
 +                    "liquidationPrice": D("0.15107132"),
 +                    "type": "short",
 +                    "total": D("0.00681856"),
 +                    "baseCurrency": "BTC"
 +                    },
 +                "ETC": {
 +                    "amount": D("-0.6"),
 +                    "borrowedPrice": D("0.1"),
 +                    "lendingFees": D("1E-8"),
 +                    "pl": D("0.00000371"),
 +                    "liquidationPrice": D("0.6"),
 +                    "type": "short",
 +                    "total": D("0.06"),
 +                    "baseCurrency": "BTC"
 +                    }
 +                }
 +            self.assertEqual(expected, balances)
 +
 +    def test_sum(self):
 +        self.assertEqual(D("1.1"), self.s.sum(D("1"), D("0.1")))
 +
 +    def test_fetch_balance(self):
 +        with mock.patch.object(self.s, "load_markets") as load_markets,\
 +                mock.patch.object(self.s, "privatePostReturnCompleteBalances") as balances,\
 +                mock.patch.object(self.s, "common_currency_code") as ccc:
 +            ccc.side_effect = ["ETH", "BTC", "DASH"]
 +            balances.return_value = {
 +                    "ETH": {
 +                        "available": "10",
 +                        "onOrders": "1",
 +                        },
 +                    "BTC": {
 +                        "available": "1",
 +                        "onOrders": "0",
 +                        },
 +                    "DASH": {
 +                        "available": "0",
 +                        "onOrders": "3"
 +                        }
 +                    }
 +
 +            expected = {
 +                    "info": {
 +                        "ETH": {"available": "10", "onOrders": "1"},
 +                        "BTC": {"available": "1", "onOrders": "0"},
 +                        "DASH": {"available": "0", "onOrders": "3"}
 +                        },
 +                    "ETH": {"free": D("10"), "used": D("1"), "total": D("11")},
 +                    "BTC": {"free": D("1"), "used": D("0"), "total": D("1")},
 +                    "DASH": {"free": D("0"), "used": D("3"), "total": D("3")},
 +                    "free": {"ETH": D("10"), "BTC": D("1"), "DASH": D("0")},
 +                    "used": {"ETH": D("1"), "BTC": D("0"), "DASH": D("3")},
 +                    "total": {"ETH": D("11"), "BTC": D("1"), "DASH": D("3")}
 +                    }
 +            result = self.s.fetch_balance()
 +            load_markets.assert_called_once()
 +            self.assertEqual(expected, result)
 +
 +    def test_fetch_balance_per_type(self):
 +        with mock.patch.object(self.s, "privatePostReturnAvailableAccountBalances") as balances:
 +            balances.return_value = {
 +                "exchange": {
 +                    "BLK": "159.83673869",
 +                    "BTC": "0.00005959",
 +                    "USDT": "0.00002625",
 +                    "XMR": "0.18719303"
 +                    },
 +                "margin": {
 +                    "BTC": "0.03019227"
 +                    }
 +                }
 +            expected = {
 +                    "info": {
 +                        "exchange": {
 +                            "BLK": "159.83673869",
 +                            "BTC": "0.00005959",
 +                            "USDT": "0.00002625",
 +                            "XMR": "0.18719303"
 +                            },
 +                        "margin": {
 +                            "BTC": "0.03019227"
 +                            }
 +                        },
 +                    "exchange": {
 +                        "BLK": D("159.83673869"),
 +                        "BTC": D("0.00005959"),
 +                        "USDT": D("0.00002625"),
 +                        "XMR": D("0.18719303")
 +                        },
 +                    "margin": {"BTC": D("0.03019227")},
 +                    "BLK": {"exchange": D("159.83673869")},
 +                    "BTC": {"exchange": D("0.00005959"), "margin": D("0.03019227")},
 +                    "USDT": {"exchange": D("0.00002625")},
 +                    "XMR": {"exchange": D("0.18719303")}
 +                    } 
 +            result = self.s.fetch_balance_per_type()
 +            self.assertEqual(expected, result)
 +
 +    def test_fetch_all_balances(self):
 +        import json
 +        with mock.patch.object(self.s, "load_markets") as load_markets,\
 +                mock.patch.object(self.s, "privatePostGetMarginPosition") as margin_balance,\
 +                mock.patch.object(self.s, "privatePostReturnCompleteBalances") as balance,\
 +                mock.patch.object(self.s, "privatePostReturnAvailableAccountBalances") as balance_per_type:
 +
 +            with open("test_samples/poloniexETest.test_fetch_all_balances.1.json") as f:
 +                balance.return_value = json.load(f)
 +            with open("test_samples/poloniexETest.test_fetch_all_balances.2.json") as f:
 +                margin_balance.return_value = json.load(f)
 +            with open("test_samples/poloniexETest.test_fetch_all_balances.3.json") as f:
 +                balance_per_type.return_value = json.load(f)
 +
 +            result = self.s.fetch_all_balances()
 +            expected_doge = {
 +                    "total": D("-12779.79821852"),
 +                    "exchange_used": D("0E-8"),
 +                    "exchange_total": D("0E-8"),
 +                    "exchange_free": D("0E-8"),
 +                    "margin_available": 0,
 +                    "margin_in_position": 0,
 +                    "margin_borrowed": D("12779.79821852"),
 +                    "margin_total": D("-12779.79821852"),
 +                    "margin_pending_gain": 0,
 +                    "margin_lending_fees": D("-9E-8"),
 +                    "margin_pending_base_gain": D("0.00024059"),
 +                    "margin_position_type": "short",
 +                    "margin_liquidation_price": D("0.00000246"),
 +                    "margin_borrowed_base_price": D("0.00599149"),
 +                    "margin_borrowed_base_currency": "BTC"
 +                    } 
 +            expected_btc = {"total": D("0.05432165"),
 +                    "exchange_used": D("0E-8"),
 +                    "exchange_total": D("0.00005959"),
 +                    "exchange_free": D("0.00005959"),
 +                    "margin_available": D("0.03019227"),
 +                    "margin_in_position": D("0.02406979"),
 +                    "margin_borrowed": 0,
 +                    "margin_total": D("0.05426206"),
 +                    "margin_pending_gain": D("0.00093955"),
 +                    "margin_lending_fees": 0,
 +                    "margin_pending_base_gain": 0,
 +                    "margin_position_type": None,
 +                    "margin_liquidation_price": 0,
 +                    "margin_borrowed_base_price": 0,
 +                    "margin_borrowed_base_currency": None
 +                    }
 +            expected_xmr = {"total": D("0.18719303"),
 +                    "exchange_used": D("0E-8"),
 +                    "exchange_total": D("0.18719303"),
 +                    "exchange_free": D("0.18719303"),
 +                    "margin_available": 0,
 +                    "margin_in_position": 0,
 +                    "margin_borrowed": 0,
 +                    "margin_total": 0,
 +                    "margin_pending_gain": 0,
 +                    "margin_lending_fees": 0,
 +                    "margin_pending_base_gain": 0,
 +                    "margin_position_type": None,
 +                    "margin_liquidation_price": 0,
 +                    "margin_borrowed_base_price": 0,
 +                    "margin_borrowed_base_currency": None
 +                    } 
 +            self.assertEqual(expected_xmr, result["XMR"])
 +            self.assertEqual(expected_doge, result["DOGE"])
 +            self.assertEqual(expected_btc, result["BTC"])
 +
 +    def test_create_margin_order(self):
 +        with self.assertRaises(market.ExchangeError):
 +            self.s.create_margin_order("FOO", "market", "buy", "10")
 +
 +        with mock.patch.object(self.s, "load_markets") as load_markets,\
 +                mock.patch.object(self.s, "privatePostMarginBuy") as margin_buy,\
 +                mock.patch.object(self.s, "privatePostMarginSell") as margin_sell,\
 +                mock.patch.object(self.s, "market") as market_mock,\
 +                mock.patch.object(self.s, "price_to_precision") as ptp,\
 +                mock.patch.object(self.s, "amount_to_precision") as atp:
 +
 +            margin_buy.return_value = {
 +                    "orderNumber": 123
 +                    }
 +            margin_sell.return_value = {
 +                    "orderNumber": 456
 +                    }
 +            market_mock.return_value = { "id": "BTC_ETC", "symbol": "BTC_ETC" }
 +            ptp.return_value = D("0.1")
 +            atp.return_value = D("12")
 +
 +            order = self.s.create_margin_order("BTC_ETC", "margin", "buy", "12", price="0.1")
 +            self.assertEqual(123, order["id"])
 +            margin_buy.assert_called_once_with({"currencyPair": "BTC_ETC", "rate": D("0.1"), "amount": D("12")})
 +            margin_sell.assert_not_called()
 +            margin_buy.reset_mock()
 +            margin_sell.reset_mock()
 +
 +            order = self.s.create_margin_order("BTC_ETC", "margin", "sell", "12", lending_rate="0.01", price="0.1")
 +            self.assertEqual(456, order["id"])
 +            margin_sell.assert_called_once_with({"currencyPair": "BTC_ETC", "rate": D("0.1"), "amount": D("12"), "lendingRate": "0.01"})
 +            margin_buy.assert_not_called()
 +
 +    def test_create_exchange_order(self):
 +        with mock.patch.object(market.ccxt.poloniex, "create_order") as create_order:
 +            self.s.create_order("symbol", "type", "side", "amount", price="price", params="params")
 +
 +            create_order.assert_called_once_with("symbol", "type", "side", "amount", price="price", params="params")
 +
  @unittest.skipUnless("unit" in limits, "Unit skipped")
 -class PortfolioTest(WebMockTestCase):
 -    def fill_data(self):
 -        if self.json_response is not None:
 -            portfolio.Portfolio.data = self.json_response
 +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()
  
 -        with open("test_portfolio.json") as example:
 +        with open("test_samples/test_portfolio.json") as example:
              self.json_response = example.read()
  
 -        self.wm.get(portfolio.Portfolio.URL, text=self.json_response)
 +        self.wm.get(market.Portfolio.URL, text=self.json_response)
  
 -    def test_get_cryptoportfolio(self):
 -        self.wm.get(portfolio.Portfolio.URL, [
 -            {"text":'{ "foo": "bar" }', "status_code": 200},
 -            {"text": "System Error", "status_code": 500},
 -            {"exc": requests.exceptions.ConnectTimeout},
 -            ])
 -        portfolio.Portfolio.get_cryptoportfolio(self.m)
 -        self.assertIn("foo", portfolio.Portfolio.data)
 -        self.assertEqual("bar", portfolio.Portfolio.data["foo"])
 -        self.assertTrue(self.wm.called)
 -        self.assertEqual(1, self.wm.call_count)
 -        self.m.report.log_error.assert_not_called()
 -        self.m.report.log_http_request.assert_called_once()
 -        self.m.report.log_http_request.reset_mock()
 -
 -        portfolio.Portfolio.get_cryptoportfolio(self.m)
 -        self.assertIsNone(portfolio.Portfolio.data)
 -        self.assertEqual(2, self.wm.call_count)
 -        self.m.report.log_error.assert_not_called()
 -        self.m.report.log_http_request.assert_called_once()
 -        self.m.report.log_http_request.reset_mock()
 -
 -
 -        portfolio.Portfolio.data = "Foo"
 -        portfolio.Portfolio.get_cryptoportfolio(self.m)
 -        self.assertEqual("Foo", portfolio.Portfolio.data)
 -        self.assertEqual(3, self.wm.call_count)
 -        self.m.report.log_error.assert_called_once_with("get_cryptoportfolio",
 -                exception=mock.ANY)
 -        self.m.report.log_http_request.assert_not_called()
 +    @mock.patch.object(market.Portfolio, "parse_cryptoportfolio")
 +    def test_get_cryptoportfolio(self, parse_cryptoportfolio):
 +        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):
 -        portfolio.Portfolio.parse_cryptoportfolio(self.m)
 -
 -        self.assertListEqual(
 -                ["medium", "high"],
 -                list(portfolio.Portfolio.liquidities.keys()))
 -
 -        liquidities = portfolio.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), portfolio.Portfolio.last_date)
 -
 -        self.m.report.log_http_request.assert_called_once_with("GET",
 -                portfolio.Portfolio.URL, None, mock.ANY, mock.ANY)
 -        self.m.report.log_http_request.reset_mock()
 -
 -        # It doesn't refetch the data when available
 -        portfolio.Portfolio.parse_cryptoportfolio(self.m)
 -        self.m.report.log_http_request.assert_not_called()
 -
 -        self.assertEqual(1, self.wm.call_count)
 -
 -        portfolio.Portfolio.parse_cryptoportfolio(self.m, refetch=True)
 -        self.assertEqual(2, self.wm.call_count)
 -        self.m.report.log_http_request.assert_called_once()
 -
 -    def test_repartition(self):
 -        expected_medium = {
 -                'BTC':   (D("1.1102e-16"), "long"),
 -                'USDT':  (D("0.1"), "long"),
 -                'ETC':   (D("0.1"), "long"),
 -                'FCT':   (D("0.1"), "long"),
 -                'OMG':   (D("0.1"), "long"),
 -                'STEEM': (D("0.1"), "long"),
 -                'STRAT': (D("0.1"), "long"),
 -                'XEM':   (D("0.1"), "long"),
 -                'XMR':   (D("0.1"), "long"),
 -                'XVC':   (D("0.1"), "long"),
 -                'ZRX':   (D("0.1"), "long"),
 -                }
 -        expected_high = {
 -                'USDT': (D("0.1226"), "long"),
 -                'BTC':  (D("0.1429"), "long"),
 -                'ETC':  (D("0.1127"), "long"),
 -                'ETH':  (D("0.1569"), "long"),
 -                'FCT':  (D("0.3341"), "long"),
 -                'GAS':  (D("0.1308"), "long"),
 -                }
 +        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.assertEqual(expected_medium, portfolio.Portfolio.repartition(self.m))
 -        self.assertEqual(expected_medium, portfolio.Portfolio.repartition(self.m, liquidity="medium"))
 -        self.assertEqual(expected_high, portfolio.Portfolio.repartition(self.m, liquidity="high"))
 +            self.assertListEqual(
 +                    ["medium", "high"],
 +                    list(market.Portfolio.liquidities.get().keys()))
  
 -        self.assertEqual(1, self.wm.call_count)
 +            liquidities = market.Portfolio.liquidities.get()
 +            self.assertEqual(10, len(liquidities["medium"].keys()))
 +            self.assertEqual(10, len(liquidities["high"].keys()))
  
 -        portfolio.Portfolio.repartition(self.m)
 -        self.assertEqual(1, self.wm.call_count)
 +            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])
  
 -        portfolio.Portfolio.repartition(self.m, refetch=True)
 -        self.assertEqual(2, self.wm.call_count)
 -        self.m.report.log_http_request.assert_called()
 -        self.assertEqual(2, self.m.report.log_http_request.call_count)
 +            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 = store.LockedVar({
 +                "medium": {
 +                    "2018-03-01": "medium_2018-03-01",
 +                    "2018-03-08": "medium_2018-03-08",
 +                    },
 +                "high": {
 +                    "2018-03-01": "high_2018-03-01",
 +                    "2018-03-08": "high_2018-03-08",
 +                    }
 +                })
 +        market.Portfolio.last_date = store.LockedVar("2018-03-08")
  
 -    @mock.patch.object(portfolio.time, "sleep")
 -    @mock.patch.object(portfolio.Portfolio, "repartition")
 -    def test_wait_for_recent(self, repartition, sleep):
 +        self.assertEqual("medium_2018-03-08", market.Portfolio.repartition())
 +        get_cryptoportfolio.assert_called_once_with()
 +        self.assertEqual("medium_2018-03-08", market.Portfolio.repartition(liquidity="medium"))
 +        self.assertEqual("high_2018-03-08", market.Portfolio.repartition(liquidity="high"))
 +
 +    @mock.patch.object(market.time, "sleep")
 +    @mock.patch.object(market.Portfolio, "get_cryptoportfolio")
 +    def test_wait_for_recent(self, get_cryptoportfolio, sleep):
          self.call_count = 0
 -        def _repartition(market, refetch):
 -            self.assertEqual(self.m, market)
 -            self.assertTrue(refetch)
 +        def _get(refetch=False):
 +            if self.call_count != 0:
 +                self.assertTrue(refetch)
 +            else:
 +                self.assertFalse(refetch)
              self.call_count += 1
 -            portfolio.Portfolio.last_date = portfolio.datetime.now()\
 -                - portfolio.timedelta(10)\
 -                + portfolio.timedelta(self.call_count)
 -        repartition.side_effect = _repartition
 +            market.Portfolio.last_date = store.LockedVar(store.datetime.now()\
 +                - store.timedelta(10)\
 +                + store.timedelta(self.call_count))
 +        get_cryptoportfolio.side_effect = _get
  
 -        portfolio.Portfolio.wait_for_recent(self.m)
 +        market.Portfolio.wait_for_recent()
          sleep.assert_called_with(30)
          self.assertEqual(6, sleep.call_count)
 -        self.assertEqual(7, repartition.call_count)
 -        self.m.report.print_log.assert_called_with("Attempt to fetch up-to-date cryptoportfolio")
 +        self.assertEqual(7, get_cryptoportfolio.call_count)
 +        market.Portfolio.report.print_log.assert_called_with("Attempt to fetch up-to-date cryptoportfolio")
  
          sleep.reset_mock()
 -        repartition.reset_mock()
 -        portfolio.Portfolio.last_date = None
 +        get_cryptoportfolio.reset_mock()
 +        market.Portfolio.last_date = store.LockedVar(None)
          self.call_count = 0
 -        portfolio.Portfolio.wait_for_recent(self.m, delta=15)
 +        market.Portfolio.wait_for_recent(delta=15)
          sleep.assert_not_called()
 -        self.assertEqual(1, repartition.call_count)
 +        self.assertEqual(1, get_cryptoportfolio.call_count)
  
          sleep.reset_mock()
 -        repartition.reset_mock()
 -        portfolio.Portfolio.last_date = None
 +        get_cryptoportfolio.reset_mock()
 +        market.Portfolio.last_date = store.LockedVar(None)
          self.call_count = 0
 -        portfolio.Portfolio.wait_for_recent(self.m, delta=1)
 +        market.Portfolio.wait_for_recent(delta=1)
          sleep.assert_called_with(30)
          self.assertEqual(9, sleep.call_count)
 -        self.assertEqual(10, repartition.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):
@@@ -1203,7 -736,7 +1203,7 @@@ class MarketTest(WebMockTestCase)
          self.assertEqual("Foo", m.fetch_fees())
          self.ccxt.fetch_fees.assert_not_called()
  
 -    @mock.patch.object(portfolio.Portfolio, "repartition")
 +    @mock.patch.object(market.Portfolio, "repartition")
      @mock.patch.object(market.Market, "get_ticker")
      @mock.patch.object(market.TradeStore, "compute_trades")
      def test_prepare_trades(self, compute_trades, get_ticker, repartition):
              m.report.log_balances.assert_called_once_with(tag="tag")
  
  
 -    @mock.patch.object(portfolio.time, "sleep")
 +    @mock.patch.object(market.time, "sleep")
      @mock.patch.object(market.TradeStore, "all_orders")
      def test_follow_orders(self, all_orders, time_mock):
          for debug, sleep in [
                      self.ccxt.transfer_balance.assert_any_call("BTC", 3, "exchange", "margin")
                      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):
 +
 +        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()
 +
 +        report.reset_mock()
 +        file_open = mock.mock_open()
 +        m = market.Market(self.ccxt, 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.to_json.return_value = "json_content"
 +
 +            m.store_report()
 +
 +            file_open.assert_any_call("present/2018-02-25T00:00:00_1.json", "w")
 +            file_open().write.assert_called_once_with("json_content")
 +            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)
 +        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_report()
 +
 +            report.merge.assert_called_with(store.Portfolio.report)
 +            self.assertRegex(stdout_mock.getvalue(), "impossible to store report file: FileNotFoundError;")
 +
 +    def test_print_orders(self):
 +        m = market.Market(self.ccxt)
 +        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,\
 +                mock.patch.object(m.trades, "prepare_orders") as prepare_orders:
 +            m.print_orders()
 +
 +            log_stage.assert_called_with("print_orders")
 +            fetch_balances.assert_called_with(tag="print_orders")
 +            prepare_trades.assert_called_with(base_currency="BTC",
 +                    compute_value="average")
 +            prepare_orders.assert_called_with(compute_value="average")
 +
 +    def test_print_balances(self):
 +        m = market.Market(self.ccxt)
 +
 +        with mock.patch.object(m.balances, "in_currency") as in_currency,\
 +                mock.patch.object(m.report, "log_stage") as log_stage,\
 +                mock.patch.object(m.balances, "fetch_balances") as fetch_balances,\
 +                mock.patch.object(m.report, "print_log") as print_log:
 +
 +            in_currency.return_value = {
 +                    "BTC": portfolio.Amount("BTC", "0.65"),
 +                    "ETH": portfolio.Amount("BTC", "0.3"),
 +                    }
 +
 +            m.print_balances()
 +
 +            log_stage.assert_called_once_with("print_balances")
 +            fetch_balances.assert_called_with()
 +            print_log.assert_has_calls([
 +                mock.call("total:"),
 +                mock.call(portfolio.Amount("BTC", "0.95")),
 +                ])
 +
 +    @mock.patch("market.Processor.process")
 +    @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)
 +        with self.subTest(before=False, after=False):
 +            m.process(None)
 +
 +            process.assert_not_called()
 +            store_report.assert_called_once()
 +            log_error.assert_not_called()
 +
 +        process.reset_mock()
 +        log_error.reset_mock()
 +        store_report.reset_mock()
 +        with self.subTest(before=True, after=False):
 +            m.process(None, before=True)
 +
 +            process.assert_called_once_with("sell_all", steps="before")
 +            store_report.assert_called_once()
 +            log_error.assert_not_called()
 +
 +        process.reset_mock()
 +        log_error.reset_mock()
 +        store_report.reset_mock()
 +        with self.subTest(before=False, after=True):
 +            m.process(None, after=True)
 +
 +            process.assert_called_once_with("sell_all", steps="after")
 +            store_report.assert_called_once()
 +            log_error.assert_not_called()
 +
 +        process.reset_mock()
 +        log_error.reset_mock()
 +        store_report.reset_mock()
 +        with self.subTest(before=True, after=True):
 +            m.process(None, before=True, after=True)
 +
 +            process.assert_has_calls([
 +                mock.call("sell_all", steps="before"),
 +                mock.call("sell_all", steps="after"),
 +                ])
 +            store_report.assert_called_once()
 +            log_error.assert_not_called()
 +
 +        process.reset_mock()
 +        log_error.reset_mock()
 +        store_report.reset_mock()
 +        with self.subTest(action="print_balances"),\
 +                mock.patch.object(m, "print_balances") as print_balances:
 +            m.process(["print_balances"])
 +
 +            process.assert_not_called()
 +            log_error.assert_not_called()
 +            store_report.assert_called_once()
 +            print_balances.assert_called_once_with()
 +
 +        log_error.reset_mock()
 +        store_report.reset_mock()
 +        with self.subTest(action="print_orders"),\
 +                mock.patch.object(m, "print_orders") as print_orders,\
 +                mock.patch.object(m, "print_balances") as print_balances:
 +            m.process(["print_orders", "print_balances"])
 +
 +            process.assert_not_called()
 +            log_error.assert_not_called()
 +            store_report.assert_called_once()
 +            print_orders.assert_called_once_with()
 +            print_balances.assert_called_once_with()
 +
 +        log_error.reset_mock()
 +        store_report.reset_mock()
 +        with self.subTest(action="unknown"):
 +            m.process(["unknown"])
 +            log_error.assert_called_once_with("market_process", message="Unknown action unknown")
 +            store_report.assert_called_once()
 +
 +        log_error.reset_mock()
 +        store_report.reset_mock()
 +        with self.subTest(unhandled_exception=True):
 +            process.side_effect = Exception("bouh")
 +
 +            m.process(None, before=True)
 +            log_error.assert_called_with("market_process", exception=mock.ANY)
 +            store_report.assert_called_once()
 + 
  @unittest.skipUnless("unit" in limits, "Unit skipped")
  class TradeStoreTest(WebMockTestCase):
      def test_compute_trades(self):
@@@ -1858,7 -1226,7 +1858,7 @@@ class BalanceStoreTest(WebMockTestCase)
          self.assertListEqual(["USDT", "XVG", "XMR", "ETC"], list(balance_store.currencies()))
          self.m.report.log_balances.assert_called_with(tag="foo")
  
 -    @mock.patch.object(portfolio.Portfolio, "repartition")
 +    @mock.patch.object(market.Portfolio, "repartition")
      def test_dispatch_assets(self, repartition):
          self.m.ccxt.fetch_all_balances.return_value = self.fetch_balance
  
          repartition.return_value = repartition_hash
  
          amounts = balance_store.dispatch_assets(portfolio.Amount("BTC", "11.1"))
 -        repartition.assert_called_with(self.m, liquidity="medium")
 +        repartition.assert_called_with(liquidity="medium")
          self.assertIn("XEM", balance_store.currencies())
          self.assertEqual(D("2.6"), amounts["BTC"].value)
          self.assertEqual(D("7.5"), amounts["XEM"].value)
@@@ -2007,16 -1375,20 +2007,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")
          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)
+             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)
  
-         self.assertFalse(trade.is_fullfiled)
+         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
  
-         order3 = mock.Mock()
-         order3.filled_amount.return_value = portfolio.Amount("BTC", "0.19")
-         trade.orders.append(order3)
+             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")
@@@ -2713,7 -2118,8 +2750,8 @@@ 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_finished_order")
+     def test_fetch(self, mark_finished_order, fetch_mouvements):
          order = portfolio.Order("buy", portfolio.Amount("ETH", 10),
                  D("0.1"), "BTC", "long", self.m, "trade")
          order.id = 45
              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()
              fetch_mouvements.assert_not_called()
  
          with self.subTest(debug=False):
              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_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()
  
      @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),
                  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
                  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):
@@@ -2966,19 -2372,6 +3004,19 @@@ class ReportStoreTest(WebMockTestCase)
              report_store.set_verbose(False)
              self.assertFalse(report_store.verbose_print)
  
 +    def test_merge(self):
 +        report_store1 = market.ReportStore(self.m, verbose_print=False)
 +        report_store2 = market.ReportStore(None, verbose_print=False)
 +
 +        report_store2.log_stage("1")
 +        report_store1.log_stage("2")
 +        report_store2.log_stage("3")
 +
 +        report_store1.merge(report_store2)
 +
 +        self.assertEqual(3, len(report_store1.logs))
 +        self.assertEqual(["1", "2", "3"], list(map(lambda x: x["stage"], report_store1.logs)))
 +
      def test_print_log(self):
          report_store = market.ReportStore(self.m)
          with self.subTest(verbose=True),\
              })
  
  @unittest.skipUnless("unit" in limits, "Unit skipped")
 -class HelperTest(WebMockTestCase):
 +class MainTest(WebMockTestCase):
      def test_make_order(self):
          self.m.get_ticker.return_value = {
                  "inverted": False,
                  }
  
          with self.subTest(description="nominal case"):
 -            helper.make_order(self.m, 10, "ETH")
 +            main.make_order(self.m, 10, "ETH")
  
              self.m.report.log_stage.assert_has_calls([
                  mock.call("make_order_begin"),
  
              self.m.reset_mock()
              with self.subTest(compute_value="default"):
 -                helper.make_order(self.m, 10, "ETH", action="dispose",
 +                main.make_order(self.m, 10, "ETH", action="dispose",
                          compute_value="ask")
  
                  trade = self.m.trades.all.append.mock_calls[0][1][0]
  
          self.m.reset_mock()
          with self.subTest(follow=False):
 -            result = helper.make_order(self.m, 10, "ETH", follow=False)
 +            result = main.make_order(self.m, 10, "ETH", follow=False)
  
              self.m.report.log_stage.assert_has_calls([
                  mock.call("make_order_begin"),
  
          self.m.reset_mock()
          with self.subTest(base_currency="USDT"):
 -            helper.make_order(self.m, 1, "BTC", base_currency="USDT")
 +            main.make_order(self.m, 1, "BTC", base_currency="USDT")
  
              trade = self.m.trades.all.append.mock_calls[0][1][0]
              self.assertEqual("BTC", trade.currency)
  
          self.m.reset_mock()
          with self.subTest(close_if_possible=True):
 -            helper.make_order(self.m, 10, "ETH", close_if_possible=True)
 +            main.make_order(self.m, 10, "ETH", close_if_possible=True)
  
              trade = self.m.trades.all.append.mock_calls[0][1][0]
              self.assertEqual(True, trade.orders[0].close_if_possible)
  
          self.m.reset_mock()
          with self.subTest(action="dispose"):
 -            helper.make_order(self.m, 10, "ETH", action="dispose")
 +            main.make_order(self.m, 10, "ETH", action="dispose")
  
              trade = self.m.trades.all.append.mock_calls[0][1][0]
              self.assertEqual(0, trade.value_to)
  
              self.m.reset_mock()
              with self.subTest(compute_value="default"):
 -                helper.make_order(self.m, 10, "ETH", action="dispose",
 +                main.make_order(self.m, 10, "ETH", action="dispose",
                          compute_value="bid")
  
                  trade = self.m.trades.all.append.mock_calls[0][1][0]
                  self.assertEqual(D("0.9"), trade.value_from.value)
  
 -    def test_user_market(self):
 -        with mock.patch("helper.main_fetch_markets") as main_fetch_markets,\
 -                mock.patch("helper.main_parse_config") as main_parse_config:
 +    def test_get_user_market(self):
 +        with mock.patch("main.fetch_markets") as main_fetch_markets,\
 +                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"},)]
 -                m = helper.get_user_market("config_path.ini", 1)
 +                m = main.get_user_market("config_path.ini", 1)
  
                  self.assertIsInstance(m, market.Market)
                  self.assertFalse(m.debug)
              with self.subTest(debug=True):
                  main_parse_config.return_value = ["pg_config", "report_path"]
                  main_fetch_markets.return_value = [({"key": "market_config"},)]
 -                m = helper.get_user_market("config_path.ini", 1, debug=True)
 +                m = main.get_user_market("config_path.ini", 1, debug=True)
  
                  self.assertIsInstance(m, market.Market)
                  self.assertTrue(m.debug)
  
 -    def test_main_store_report(self):
 -        file_open = mock.mock_open()
 -        with self.subTest(file=None), mock.patch("__main__.open", file_open):
 -            helper.main_store_report(None, 1, self.m)
 -            file_open.assert_not_called()
 -
 -        file_open = mock.mock_open()
 -        with self.subTest(file="present"), mock.patch("helper.open", file_open),\
 -                mock.patch.object(helper, "datetime") as time_mock:
 -            time_mock.now.return_value = datetime.datetime(2018, 2, 25)
 -            self.m.report.to_json.return_value = "json_content"
 -
 -            helper.main_store_report("present", 1, self.m)
 -
 -            file_open.assert_any_call("present/2018-02-25T00:00:00_1.json", "w")
 -            file_open().write.assert_called_once_with("json_content")
 -            self.m.report.to_json.assert_called_once_with()
 -
 -        with self.subTest(file="error"),\
 -                mock.patch("helper.open") as file_open,\
 +    def test_process(self):
 +        with mock.patch("market.Market") as market_mock,\
                  mock.patch('sys.stdout', new_callable=StringIO) as stdout_mock:
 -            file_open.side_effect = FileNotFoundError
  
 -            helper.main_store_report("error", 1, self.m)
 -
 -            self.assertRegex(stdout_mock.getvalue(), "impossible to store report file: FileNotFoundError;")
 -
 -    @mock.patch("helper.Processor.process")
 -    def test_main_process_market(self, process):
 -        with self.subTest(before=False, after=False):
 -            m = mock.Mock()
 -            helper.main_process_market(m, None)
 -
 -            process.assert_not_called()
 -
 -        process.reset_mock()
 -        with self.subTest(before=True, after=False):
 -            helper.main_process_market(m, None, before=True)
 -
 -            process.assert_called_once_with("sell_all", steps="before")
 -
 -        process.reset_mock()
 -        with self.subTest(before=False, after=True):
 -            helper.main_process_market(m, None, after=True)
 -            
 -            process.assert_called_once_with("sell_all", steps="after")
 +            args_mock = mock.Mock()
 +            args_mock.action = "action"
 +            args_mock.config = "config"
 +            args_mock.user = "user"
 +            args_mock.debug = "debug"
 +            args_mock.before = "before"
 +            args_mock.after = "after"
 +            self.assertEqual("", stdout_mock.getvalue())
  
 -        process.reset_mock()
 -        with self.subTest(before=True, after=True):
 -            helper.main_process_market(m, None, before=True, after=True)
 +            main.process("config", 1, "report_path", args_mock)
  
 -            process.assert_has_calls([
 -                mock.call("sell_all", steps="before"),
 -                mock.call("sell_all", steps="after"),
 +            market_mock.from_config.assert_has_calls([
 +                mock.call("config", debug="debug", user_id=1, report_path="report_path"),
 +                mock.call().process("action", before="before", after="after"),
                  ])
  
 -        process.reset_mock()
 -        with self.subTest(action="print_balances"),\
 -                mock.patch("helper.print_balances") as print_balances:
 -            helper.main_process_market("user", ["print_balances"])
 -
 -            process.assert_not_called()
 -            print_balances.assert_called_once_with("user")
 -
 -        with self.subTest(action="print_orders"),\
 -                mock.patch("helper.print_orders") as print_orders,\
 -                mock.patch("helper.print_balances") as print_balances:
 -            helper.main_process_market("user", ["print_orders", "print_balances"])
 -
 -            process.assert_not_called()
 -            print_orders.assert_called_once_with("user")
 -            print_balances.assert_called_once_with("user")
 -
 -        with self.subTest(action="unknown"),\
 -                self.assertRaises(NotImplementedError):
 -            helper.main_process_market("user", ["unknown"])
 +            with self.subTest(exception=True):
 +                market_mock.from_config.side_effect = Exception("boo")
 +                main.process("config", 1, "report_path", args_mock)
 +                self.assertEqual("Exception: boo\n", stdout_mock.getvalue())
  
 -    @mock.patch.object(helper, "psycopg2")
 -    def test_fetch_markets(self, psycopg2):
 -        connect_mock = mock.Mock()
 -        cursor_mock = mock.MagicMock()
 -        cursor_mock.__iter__.return_value = ["row_1", "row_2"]
 -
 -        connect_mock.cursor.return_value = cursor_mock
 -        psycopg2.connect.return_value = connect_mock
 -
 -        with self.subTest(user=None):
 -            rows = list(helper.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")
 -
 -            self.assertEqual(["row_1", "row_2"], rows)
 -
 -        psycopg2.connect.reset_mock()
 -        cursor_mock.execute.reset_mock()
 -        with self.subTest(user=1):
 -            rows = list(helper.main_fetch_markets({"foo": "bar"}, 1))
 +    def test_main(self):
 +        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:
  
 -            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)
 +                args_mock = mock.Mock()
 +                args_mock.parallel = False
 +                args_mock.config = "config"
 +                args_mock.user = "user"
 +                parse_args.return_value = args_mock
  
 -            self.assertEqual(["row_1", "row_2"], rows)
 +                parse_config.return_value = ["pg_config", "report_path"]
  
 -    @mock.patch.object(helper.sys, "exit")
 -    def test_main_parse_args(self, exit):
 -        with self.subTest(config="config.ini"):
 -            args = helper.main_parse_args([])
 -            self.assertEqual("config.ini", args.config)
 -            self.assertFalse(args.before)
 -            self.assertFalse(args.after)
 -            self.assertFalse(args.debug)
 +                fetch_markets.return_value = [["config1", 1], ["config2", 2]]
  
 -            args = helper.main_parse_args(["--before", "--after", "--debug"])
 -            self.assertTrue(args.before)
 -            self.assertTrue(args.after)
 -            self.assertTrue(args.debug)
 +                main.main(["Foo", "Bar"])
  
 -            exit.assert_not_called()
 +                parse_args.assert_called_with(["Foo", "Bar"])
 +                parse_config.assert_called_with("config")
 +                fetch_markets.assert_called_with("pg_config", "user")
  
 -        with self.subTest(config="inexistant"),\
 -                mock.patch('sys.stdout', new_callable=StringIO) as stdout_mock:
 -            args = helper.main_parse_args(["--config", "foo.bar"])
 -            exit.assert_called_once_with(1)
 -            self.assertEqual("no config file found, exiting\n", stdout_mock.getvalue())
 +                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),
 +                    ])
 +        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.config = "config"
 +                args_mock.user = "user"
 +                parse_args.return_value = args_mock
 +
 +                parse_config.return_value = ["pg_config", "report_path"]
 +
 +                fetch_markets.return_value = [["config1", 1], ["config2", 2]]
 +
 +                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")
 +
 +                start.assert_called_once_with()
 +                self.assertEqual(2, process.call_count)
 +                process.assert_has_calls([
 +                    mock.call.__bool__(),
 +                    mock.call("config1", 1, "report_path", args_mock),
 +                    mock.call.__bool__(),
 +                    mock.call("config2", 2, "report_path", args_mock),
 +                    ])
  
 -    @mock.patch.object(helper.sys, "exit")
 -    @mock.patch("helper.configparser")
 -    @mock.patch("helper.os")
 -    def test_main_parse_config(self, os, configparser, exit):
 +    @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
              config_mock.__contains__.side_effect = config
              config_mock.__getitem__.return_value = "pg_config"
  
 -            result = helper.main_parse_config("configfile")
 +            result = main.parse_config("configfile")
  
              config_mock.read.assert_called_with("configfile")
  
                      ]
  
              os.path.exists.return_value = False
 -            result = helper.main_parse_config("configfile")
 +            result = main.parse_config("configfile")
  
              config_mock.read.assert_called_with("configfile")
              self.assertEqual(["pg_config", "report_path"], result)
                  mock.patch('sys.stdout', new_callable=StringIO) as stdout_mock:
              config_mock = mock.MagicMock()
              configparser.ConfigParser.return_value = config_mock
 -            result = helper.main_parse_config("configfile")
 +            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):
 +        with self.subTest(config="config.ini"):
 +            args = main.parse_args([])
 +            self.assertEqual("config.ini", args.config)
 +            self.assertFalse(args.before)
 +            self.assertFalse(args.after)
 +            self.assertFalse(args.debug)
  
 -    def test_print_orders(self):
 -        helper.print_orders(self.m)
 +            args = main.parse_args(["--before", "--after", "--debug"])
 +            self.assertTrue(args.before)
 +            self.assertTrue(args.after)
 +            self.assertTrue(args.debug)
  
 -        self.m.report.log_stage.assert_called_with("print_orders")
 -        self.m.balances.fetch_balances.assert_called_with(tag="print_orders")
 -        self.m.prepare_trades.assert_called_with(base_currency="BTC",
 -                compute_value="average")
 -        self.m.trades.prepare_orders.assert_called_with(compute_value="average")
 +            exit.assert_not_called()
 +
 +        with self.subTest(config="inexistant"),\
 +                mock.patch('sys.stdout', 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):
 +        connect_mock = mock.Mock()
 +        cursor_mock = mock.MagicMock()
 +        cursor_mock.__iter__.return_value = ["row_1", "row_2"]
 +
 +        connect_mock.cursor.return_value = cursor_mock
 +        psycopg2.connect.return_value = connect_mock
 +
 +        with self.subTest(user=None):
 +            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")
 +
 +            self.assertEqual(["row_1", "row_2"], rows)
 +
 +        psycopg2.connect.reset_mock()
 +        cursor_mock.execute.reset_mock()
 +        with self.subTest(user=1):
 +            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)
 +
 +            self.assertEqual(["row_1", "row_2"], rows)
  
 -    def test_print_balances(self):
 -        self.m.balances.in_currency.return_value = {
 -                "BTC": portfolio.Amount("BTC", "0.65"),
 -                "ETH": portfolio.Amount("BTC", "0.3"),
 -        }
 -
 -        helper.print_balances(self.m)
 -
 -        self.m.report.log_stage.assert_called_once_with("print_balances")
 -        self.m.balances.fetch_balances.assert_called_with()
 -        self.m.report.print_log.assert_has_calls([
 -            mock.call("total:"),
 -            mock.call(portfolio.Amount("BTC", "0.95")),
 -            ])
  
  @unittest.skipUnless("unit" in limits, "Unit skipped")
  class ProcessorTest(WebMockTestCase):
      def test_values(self):
 -        processor = helper.Processor(self.m)
 +        processor = market.Processor(self.m)
  
          self.assertEqual(self.m, processor.market)
  
      def test_run_action(self):
 -        processor = helper.Processor(self.m)
 +        processor = market.Processor(self.m)
  
          with mock.patch.object(processor, "parse_args") as parse_args:
              method_mock = mock.Mock()
  
              processor.run_action("wait_for_recent", "bar", "baz")
  
 -            method_mock.assert_called_with(self.m, foo="bar")
 +            method_mock.assert_called_with(foo="bar")
  
      def test_select_step(self):
 -        processor = helper.Processor(self.m)
 +        processor = market.Processor(self.m)
  
          scenario = processor.scenarios["sell_all"]
  
          with self.assertRaises(TypeError):
              processor.select_steps(scenario, ["wait"])
  
 -    @mock.patch("helper.Processor.process_step")
 +    @mock.patch("market.Processor.process_step")
      def test_process(self, process_step):
 -        processor = helper.Processor(self.m)
 +        processor = market.Processor(self.m)
  
          processor.process("sell_all", foo="bar")
          self.assertEqual(3, process_step.call_count)
          ccxt = mock.Mock(spec=market.ccxt.poloniexE)
          m = market.Market(ccxt)
  
 -        processor = helper.Processor(m)
 +        processor = market.Processor(m)
  
          method, arguments = processor.method_arguments("wait_for_recent")
 -        self.assertEqual(portfolio.Portfolio.wait_for_recent, method)
 -        self.assertEqual(["delta"], arguments)
 +        self.assertEqual(market.Portfolio.wait_for_recent, method)
 +        self.assertEqual(["delta", "poll"], arguments)
  
          method, arguments = processor.method_arguments("prepare_trades")
          self.assertEqual(m.prepare_trades, method)
          self.assertEqual(m.trades.close_trades, method)
  
      def test_process_step(self):
 -        processor = helper.Processor(self.m)
 +        processor = market.Processor(self.m)
  
          with mock.patch.object(processor, "run_action") as run_action:
              step = processor.scenarios["sell_needed"][1]
              self.m.balances.fetch_balances.assert_not_called()
  
      def test_parse_args(self):
 -        processor = helper.Processor(self.m)
 +        processor = market.Processor(self.m)
  
          with mock.patch.object(processor, "method_arguments") as method_arguments:
              method_mock = mock.Mock()
@@@ -3941,7 -3350,7 +3979,7 @@@ class AcceptanceTest(WebMockTestCase)
          market = mock.Mock()
          market.fetch_all_balances.return_value = fetch_balance
          market.fetch_ticker.side_effect = fetch_ticker
 -        with mock.patch.object(portfolio.Portfolio, "repartition", return_value=repartition):
 +        with mock.patch.object(market.Portfolio, "repartition", return_value=repartition):
              # Action 1
              helper.prepare_trades(market)
  
                      "amount": "10", "total": "1"
                      }
                  ]
 -        with mock.patch.object(portfolio.time, "sleep") as sleep:
 +        with mock.patch.object(market.time, "sleep") as sleep:
              # Action 4
              helper.follow_orders(verbose=False)
  
                  }
          market.fetch_all_balances.return_value = fetch_balance
  
 -        with mock.patch.object(portfolio.Portfolio, "repartition", return_value=repartition):
 +        with mock.patch.object(market.Portfolio, "repartition", return_value=repartition):
              # Action 5
              helper.prepare_trades(market, only="acquire", compute_value="average")
  
          # TODO
          # portfolio.TradeStore.run_orders()
  
 -        with mock.patch.object(portfolio.time, "sleep") as sleep:
 +        with mock.patch.object(market.time, "sleep") as sleep:
              # Action 8
              helper.follow_orders(verbose=False)