]> git.immae.eu Git - perso/Immae/Projets/Cryptomonnaies/Cryptoportfolio/Trader.git/commitdiff
Add Makefile and test coverage
authorIsmaël Bouya <ismael.bouya@normalesup.org>
Sun, 11 Feb 2018 12:36:54 +0000 (13:36 +0100)
committerIsmaël Bouya <ismael.bouya@normalesup.org>
Sun, 11 Feb 2018 12:36:54 +0000 (13:36 +0100)
Fix order preparation and add tests for the step
Separate tests between acceptance and unit
Add more tests

.gitignore
Makefile [new file with mode: 0644]
portfolio.py
store.py
test.py

index bee8a64b79a99590d5303307144172cfe824fbf7..bd500b32d4def6521a649b811bc11aa3114ffdab 100644 (file)
@@ -1 +1,3 @@
 __pycache__
+.coverage
+/htmlcov
diff --git a/Makefile b/Makefile
new file mode 100644 (file)
index 0000000..1464886
--- /dev/null
+++ b/Makefile
@@ -0,0 +1,32 @@
+test:
+       python test.py
+
+run:
+       python portfolio.py
+
+test_coverage_unit:
+       coverage run --source=. --omit=test.py test.py --onlyunit
+       coverage report -m
+
+test_coverage_unit_html: test_coverage_unit
+       coverage html
+       rm ~/hosts/www.immae.eu/htmlcov -rf && cp -r htmlcov ~/hosts/www.immae.eu
+       @echo "coverage in https://www.immae.eu/htmlcov"
+
+test_coverage_acceptance:
+       coverage run --source=. --omit=test.py test.py --onlyacceptance
+       coverage report -m
+
+test_coverage_acceptance_html: test_coverage_acceptance
+       coverage html
+       rm ~/hosts/www.immae.eu/htmlcov -rf && cp -r htmlcov ~/hosts/www.immae.eu
+       @echo "coverage in https://www.immae.eu/htmlcov"
+
+test_coverage_all:
+       coverage run --source=. --omit=test.py test.py
+       coverage report -m
+
+test_coverage_all_html: test_coverage_all
+       coverage html
+       rm ~/hosts/www.immae.eu/htmlcov -rf && cp -r htmlcov ~/hosts/www.immae.eu
+       @echo "coverage in https://www.immae.eu/htmlcov"
index efd9b84e498e20f39f0eb5a7e35d464d2b81614a..b629966b3e057cd53e50220be244e686d5418ad9 100644 (file)
@@ -149,7 +149,7 @@ class Amount:
 
     def __floordiv__(self, value):
         if not isinstance(value, (int, float, D)):
-            raise TypeError("Amount may only be multiplied by integers")
+            raise TypeError("Amount may only be divided by numbers")
         return Amount(self.currency, self.value / value)
 
     def __truediv__(self, value):
@@ -290,11 +290,10 @@ class Trade:
         else:
             return "long"
 
-    @property
-    def filled_amount(self):
+    def filled_amount(self, in_base_currency=False):
         filled_amount = 0
         for order in self.orders:
-            filled_amount += order.filled_amount
+            filled_amount += order.filled_amount(in_base_currency=in_base_currency)
         return filled_amount
 
     def update_order(self, order, tick):
@@ -329,54 +328,69 @@ class Trade:
         if inverted:
             ticker = ticker["original"]
         rate = Computation.compute_value(ticker, self.order_action(inverted), compute_value=compute_value)
-        # 0.1
 
         delta_in_base = abs(self.value_from - self.value_to)
         # 9 BTC's worth of move (10 - 1 or 1 - 10 depending on case)
 
         if not inverted:
-            currency = self.base_currency
+            base_currency = self.base_currency
             # BTC
             if self.action == "dispose":
-                # I have 10 BTC worth of FOO, and I want to sell 9 BTC worth of it
-                # At rate 1 Foo = 0.1 BTC
-                value_from = self.value_from.linked_to
-                # value_from = 100 FOO
-                value_to = self.value_to.in_currency(self.currency, self.market, rate=1/self.value_from.rate)
-                # value_to   = 10 FOO (1 BTC * 1/0.1)
-                delta = abs(value_to - value_from)
-                # delta      = 90 FOO
-                # Action: "sell" "90 FOO" at rate "0.1" "BTC" on "market"
-
-                # Note: no rounding error possible: if we have value_to == 0, then delta == value_from
+                filled = self.filled_amount(in_base_currency=False)
+                delta = delta_in_base.in_currency(self.currency, self.market, rate=1/self.value_from.rate)
+                # I have 10 BTC worth of FOO, and I want to sell 9 BTC
+                # worth of it, computed first with rate 10 FOO = 1 BTC.
+                # -> I "sell" "90" FOO at proposed rate "rate".
+
+                delta = delta - filled
+                # I already sold 60 FOO, 30 left
             else:
-                delta = delta_in_base.in_currency(self.currency, self.market, rate=1/rate)
-                # I want to buy 9 / 0.1 FOO
-                # Action: "buy" "90 FOO" at rate "0.1" "BTC" on "market"
+                filled = self.filled_amount(in_base_currency=True)
+                delta = (delta_in_base - filled).in_currency(self.currency, self.market, rate=1/rate)
+                # I want to buy 9 BTC worth of FOO, computed with rate
+                # 10 FOO = 1 BTC
+                # -> I "buy" "9 / rate" FOO at proposed rate "rate"
+
+                # I already bought 3 / rate FOO, 6 / rate left
         else:
-            currency = self.currency
+            base_currency = self.currency
             # FOO
-            delta = delta_in_base
-            # sell: 
-            #   I have 10 BTC worth of FOO, and I want to sell 9 BTC worth of it
-            #   At rate 1 Foo = 0.1 BTC
-            #   Action: "buy" "9 BTC" at rate "1/0.1" "FOO" on market
-            # buy:
-            #   I want to buy 9 / 0.1 FOO
-            #   Action: "sell" "9 BTC" at rate "1/0.1" "FOO" on "market"
-            if self.value_to == 0:
-                rate = self.value_from.linked_to.value / self.value_from.value
-                # Recompute the rate to avoid any rounding error
+            if self.action == "dispose":
+                filled = self.filled_amount(in_base_currency=True)
+                # Base is FOO
+
+                delta = (delta_in_base.in_currency(self.currency, self.market, rate=1/self.value_from.rate)
+                        - filled).in_currency(self.base_currency, self.market, rate=1/rate)
+                # I have 10 BTC worth of FOO, and I want to sell 9 BTC worth of it
+                # computed at rate 1 Foo = 0.01 BTC
+                # Computation says I should sell it at 125 FOO / BTC
+                # -> delta_in_base = 9 BTC
+                # -> delta = (9 * 1/0.01 FOO) * 1/125 = 7.2 BTC
+                # Action: "buy" "7.2 BTC" at rate "125" "FOO" on market
+
+                # I already bought 300/125 BTC, only 600/125 left
+            else:
+                filled = self.filled_amount(in_base_currency=False)
+                # Base is FOO
+
+                delta = delta_in_base
+                # I have 1 BTC worth of FOO, and I want to buy 9 BTC worth of it
+                # At rate 100 Foo / BTC
+                # Computation says I should buy it at 125 FOO / BTC
+                # -> delta_in_base = 9 BTC
+                # Action: "sell" "9 BTC" at rate "125" "FOO" on market
+
+                delta = delta - filled
+                # I already sold 4 BTC, only 5 left
 
         close_if_possible = (self.value_to == 0)
 
-        if delta <= self.filled_amount:
-            print("Less to do than already filled: {} <= {}".format(delta,
-                self.filled_amount))
+        if delta <= 0:
+            print("Less to do than already filled: {}".format(delta))
             return
 
         self.orders.append(Order(self.order_action(inverted),
-            delta - self.filled_amount, rate, currency, self.trade_type,
+            delta, rate, base_currency, self.trade_type,
             self.market, self, close_if_possible=close_if_possible))
 
     def __repr__(self):
@@ -497,15 +511,17 @@ class Order:
     def remaining_amount(self):
         if self.status == "open":
             self.fetch()
-        return self.amount - self.filled_amount
+        return self.amount - self.filled_amount()
 
-    @property
-    def filled_amount(self):
+    def filled_amount(self, in_base_currency=False):
         if self.status == "open":
             self.fetch()
-        filled_amount = Amount(self.amount.currency, 0)
+        filled_amount = 0
         for mouvement in self.mouvements:
-            filled_amount += mouvement.total
+            if in_base_currency:
+                filled_amount += mouvement.total_in_base
+            else:
+                filled_amount += mouvement.total
         return filled_amount
 
     def fetch_mouvements(self):
index 4e46878928d940522beceae46c9d08f37f40c636..841a0fc9e4f581ad468a2c3e8664507b003b4232 100644 (file)
--- a/store.py
+++ b/store.py
@@ -53,22 +53,27 @@ class TradeStore:
                 continue
             value_from = values_in_base.get(currency, portfolio.Amount(base_currency, 0))
             value_to = new_repartition.get(currency, portfolio.Amount(base_currency, 0))
+
             if value_from.value * value_to.value < 0:
-                trade_1 = portfolio.Trade(value_from, portfolio.Amount(base_currency, 0), currency, market=market)
-                if only is None or trade_1.action == only:
-                    cls.all.append(trade_1)
-                trade_2 = portfolio.Trade(portfolio.Amount(base_currency, 0), value_to, currency, market=market)
-                if only is None or trade_2.action == only:
-                    cls.all.append(trade_2)
+                cls.add_trade_if_matching(
+                        value_from, portfolio.Amount(base_currency, 0),
+                        currency, only=only, market=market)
+                cls.add_trade_if_matching(
+                        portfolio.Amount(base_currency, 0), value_to,
+                        currency, only=only, market=market)
             else:
-                trade = portfolio.Trade(
-                    value_from,
-                    value_to,
-                    currency,
-                    market=market
-                    )
-                if only is None or trade.action == only:
-                    cls.all.append(trade)
+                cls.add_trade_if_matching(value_from, value_to,
+                        currency, only=only, market=market)
+
+    @classmethod
+    def add_trade_if_matching(cls, value_from, value_to, currency,
+            only=None, market=None):
+        trade = portfolio.Trade(value_from, value_to, currency,
+                market=market)
+        if only is None or trade.action == only:
+            cls.all.append(trade)
+            return True
+        return False
 
     @classmethod
     def prepare_orders(cls, only=None, compute_value="default"):
diff --git a/test.py b/test.py
index aae1dc85af02245f88ddd28d32a4fac9ecfe1cef..be3ad4a6d481f61e8e5e40056dc49dae20c7759e 100644 (file)
--- a/test.py
+++ b/test.py
@@ -1,3 +1,4 @@
+import sys
 import portfolio
 import unittest
 from decimal import Decimal as D
@@ -7,6 +8,16 @@ import requests_mock
 from io import StringIO
 import helper
 
+limits = ["acceptance", "unit"]
+for test_type in limits:
+    if "--no{}".format(test_type) in sys.argv:
+        sys.argv.remove("--no{}".format(test_type))
+        limits.remove(test_type)
+    if "--only{}".format(test_type) in sys.argv:
+        sys.argv.remove("--only{}".format(test_type))
+        limits = [test_type]
+        break
+
 class WebMockTestCase(unittest.TestCase):
     import time
 
@@ -39,6 +50,7 @@ class WebMockTestCase(unittest.TestCase):
         self.wm.stop()
         super(WebMockTestCase, self).tearDown()
 
+@unittest.skipUnless("unit" in limits, "Unit skipped")
 class PortfolioTest(WebMockTestCase):
     def fill_data(self):
         if self.json_response is not None:
@@ -140,6 +152,7 @@ class PortfolioTest(WebMockTestCase):
         self.assertEqual(expected_medium, portfolio.Portfolio.repartition(liquidity="medium"))
         self.assertEqual(expected_high, portfolio.Portfolio.repartition(liquidity="high"))
 
+@unittest.skipUnless("unit" in limits, "Unit skipped")
 class AmountTest(WebMockTestCase):
     def test_values(self):
         amount = portfolio.Amount("BTC", "0.65")
@@ -250,6 +263,9 @@ class AmountTest(WebMockTestCase):
         self.assertEqual(D("5.5"), (amount / 2).value)
         self.assertEqual(D("4.4"), (amount / D("2.5")).value)
 
+        with self.assertRaises(Exception):
+            amount / amount
+
     def test__truediv(self):
         amount = portfolio.Amount("XEM", 11)
 
@@ -363,6 +379,7 @@ class AmountTest(WebMockTestCase):
         amount2.linked_to = amount3
         self.assertEqual("Amount(32.00000000 BTX -> Amount(12000.00000000 USDT -> Amount(0.10000000 BTC)))", repr(amount1))
 
+@unittest.skipUnless("unit" in limits, "Unit skipped")
 class BalanceTest(WebMockTestCase):
     def test_values(self):
         balance = portfolio.Balance("BTC", {
@@ -405,16 +422,27 @@ class BalanceTest(WebMockTestCase):
             "exchange_used": 1, "exchange_free": 2 })
         self.assertEqual("Balance(BTX Exch: [✔2.00000000 BTX + ❌1.00000000 BTX = 3.00000000 BTX])", repr(balance))
 
+        balance = portfolio.Balance("BTX", { "exchange_total": 1, "exchange_used": 1})
+        self.assertEqual("Balance(BTX Exch: [❌1.00000000 BTX])", repr(balance))
+
         balance = portfolio.Balance("BTX", { "margin_total": 3,
             "margin_borrowed": 1, "margin_free": 2 })
         self.assertEqual("Balance(BTX Margin: [✔2.00000000 BTX + borrowed 1.00000000 BTX = 3.00000000 BTX])", repr(balance))
 
+        balance = portfolio.Balance("BTX", { "margin_total": 2, "margin_free": 2 })
+        self.assertEqual("Balance(BTX Margin: [✔2.00000000 BTX])", repr(balance))
+
         balance = portfolio.Balance("BTX", { "margin_total": -3,
             "margin_borrowed_base_price": D("0.1"),
             "margin_borrowed_base_currency": "BTC",
             "margin_lending_fees": D("0.002") })
         self.assertEqual("Balance(BTX Margin: [-3.00000000 BTX @@ 0.10000000 BTC/0.00200000 BTC])", repr(balance))
 
+        balance = portfolio.Balance("BTX", { "margin_total": 1,
+            "margin_borrowed": 1, "exchange_free": 2, "exchange_total": 2})
+        self.assertEqual("Balance(BTX Exch: [✔2.00000000 BTX] Margin: [borrowed 1.00000000 BTX] Total: [0.00000000 BTX])", repr(balance))
+
+@unittest.skipUnless("unit" in limits, "Unit skipped")
 class HelperTest(WebMockTestCase):
     def test_get_ticker(self):
         market = mock.Mock()
@@ -612,15 +640,153 @@ class HelperTest(WebMockTestCase):
         self.assertEqual(D("0.01"), call[0][0]["XVG"].value)
         self.assertEqual(D("1.01"), call[0][1]["BTC"].value)
 
-    @unittest.skip("TODO")
-    def test_follow_orders(self):
-        pass
-
-
+    @mock.patch.object(portfolio.time, "sleep")
+    @mock.patch.object(portfolio.TradeStore, "all_orders")
+    def test_follow_orders(self, all_orders, time_mock):
+        for verbose, debug, sleep in [
+                (True, False, None), (False, False, None),
+                (True, True, None), (True, False, 12),
+                (True, True, 12)]:
+            with self.subTest(sleep=sleep, debug=debug, verbose=verbose), \
+                    mock.patch('sys.stdout', new_callable=StringIO) as stdout_mock:
+                portfolio.TradeStore.debug = debug
+                order_mock1 = mock.Mock()
+                order_mock2 = mock.Mock()
+                order_mock3 = mock.Mock()
+                all_orders.side_effect = [
+                        [order_mock1, order_mock2],
+                        [order_mock1, order_mock2],
+
+                        [order_mock1, order_mock3],
+                        [order_mock1, order_mock3],
+
+                        [order_mock1, order_mock3],
+                        [order_mock1, order_mock3],
+
+                        []
+                        ]
+
+                order_mock1.get_status.side_effect = ["open", "open", "closed"]
+                order_mock2.get_status.side_effect = ["open"]
+                order_mock3.get_status.side_effect = ["open", "closed"]
+
+                order_mock1.trade = mock.Mock()
+                order_mock2.trade = mock.Mock()
+                order_mock3.trade = mock.Mock()
+
+                helper.follow_orders(verbose=verbose, sleep=sleep)
+
+                order_mock1.trade.update_order.assert_any_call(order_mock1, 1)
+                order_mock1.trade.update_order.assert_any_call(order_mock1, 2)
+                self.assertEqual(2, order_mock1.trade.update_order.call_count)
+                self.assertEqual(3, order_mock1.get_status.call_count)
+
+                order_mock2.trade.update_order.assert_any_call(order_mock2, 1)
+                self.assertEqual(1, order_mock2.trade.update_order.call_count)
+                self.assertEqual(1, order_mock2.get_status.call_count)
+
+                order_mock3.trade.update_order.assert_any_call(order_mock3, 2)
+                self.assertEqual(1, order_mock3.trade.update_order.call_count)
+                self.assertEqual(2, order_mock3.get_status.call_count)
+
+                if sleep is None:
+                    if debug:
+                        time_mock.assert_called_with(7)
+                    else:
+                        time_mock.assert_called_with(30)
+                else:
+                    time_mock.assert_called_with(sleep)
+
+                if verbose:
+                    self.assertNotEqual("", stdout_mock.getvalue())
+                else:
+                    self.assertEqual("", stdout_mock.getvalue())
+
+@unittest.skipUnless("unit" in limits, "Unit skipped")
 class TradeStoreTest(WebMockTestCase):
-    @unittest.skip("TODO")
-    def test_compute_trades(self):
-        pass
+    @mock.patch.object(portfolio.BalanceStore, "currencies")
+    @mock.patch.object(portfolio.TradeStore, "add_trade_if_matching")
+    def test_compute_trades(self, add_trade_if_matching, currencies):
+        currencies.return_value = ["XMR", "DASH", "XVG", "BTC", "ETH"]
+
+        values_in_base = {
+                "XMR": portfolio.Amount("BTC", D("0.9")),
+                "DASH": portfolio.Amount("BTC", D("0.4")),
+                "XVG": portfolio.Amount("BTC", D("-0.5")),
+                "BTC": portfolio.Amount("BTC", D("0.5")),
+                }
+        new_repartition = {
+                "DASH": portfolio.Amount("BTC", D("0.5")),
+                "XVG": portfolio.Amount("BTC", D("0.1")),
+                "BTC": portfolio.Amount("BTC", D("0.4")),
+                "ETH": portfolio.Amount("BTC", D("0.3")),
+                }
+
+        portfolio.TradeStore.compute_trades(values_in_base,
+                new_repartition, only="only", market="market")
+
+        self.assertEqual(5, add_trade_if_matching.call_count)
+        add_trade_if_matching.assert_any_call(
+                portfolio.Amount("BTC", D("0.9")),
+                portfolio.Amount("BTC", 0),
+                "XMR", only="only", market="market"
+                )
+        add_trade_if_matching.assert_any_call(
+                portfolio.Amount("BTC", D("0.4")),
+                portfolio.Amount("BTC", D("0.5")),
+                "DASH", only="only", market="market"
+                )
+        add_trade_if_matching.assert_any_call(
+                portfolio.Amount("BTC", D("-0.5")),
+                portfolio.Amount("BTC", D("0")),
+                "XVG", only="only", market="market"
+                )
+        add_trade_if_matching.assert_any_call(
+                portfolio.Amount("BTC", D("0")),
+                portfolio.Amount("BTC", D("0.1")),
+                "XVG", only="only", market="market"
+                )
+        add_trade_if_matching.assert_any_call(
+                portfolio.Amount("BTC", D("0")),
+                portfolio.Amount("BTC", D("0.3")),
+                "ETH", only="only", market="market"
+                )
+
+    def test_add_trade_if_matching(self):
+        result = portfolio.TradeStore.add_trade_if_matching(
+                portfolio.Amount("BTC", D("0")),
+                portfolio.Amount("BTC", D("0.3")),
+                "ETH", only="nope", market="market"
+                )
+        self.assertEqual(0, len(portfolio.TradeStore.all))
+        self.assertEqual(False, result)
+
+        portfolio.TradeStore.all = []
+        result = portfolio.TradeStore.add_trade_if_matching(
+                portfolio.Amount("BTC", D("0")),
+                portfolio.Amount("BTC", D("0.3")),
+                "ETH", only=None, market="market"
+                )
+        self.assertEqual(1, len(portfolio.TradeStore.all))
+        self.assertEqual(True, result)
+
+        portfolio.TradeStore.all = []
+        result = portfolio.TradeStore.add_trade_if_matching(
+                portfolio.Amount("BTC", D("0")),
+                portfolio.Amount("BTC", D("0.3")),
+                "ETH", only="acquire", market="market"
+                )
+        self.assertEqual(1, len(portfolio.TradeStore.all))
+        self.assertEqual(True, result)
+
+        portfolio.TradeStore.all = []
+        result = portfolio.TradeStore.add_trade_if_matching(
+                portfolio.Amount("BTC", D("0")),
+                portfolio.Amount("BTC", D("0.3")),
+                "ETH", only="dispose", market="market"
+                )
+        self.assertEqual(0, len(portfolio.TradeStore.all))
+        self.assertEqual(False, result)
 
     def test_prepare_orders(self):
         trade_mock1 = mock.Mock()
@@ -709,7 +875,7 @@ class TradeStoreTest(WebMockTestCase):
         order_mock2.get_status.assert_called()
         order_mock3.get_status.assert_called()
 
-
+@unittest.skipUnless("unit" in limits, "Unit skipped")
 class BalanceStoreTest(WebMockTestCase):
     def setUp(self):
         super(BalanceStoreTest, self).setUp()
@@ -802,12 +968,14 @@ class BalanceStoreTest(WebMockTestCase):
         repartition.return_value = {
                 "XEM": (D("0.75"), "long"),
                 "BTC": (D("0.26"), "long"),
+                "DASH": (D("0.10"), "short"),
                 }
 
-        amounts = portfolio.BalanceStore.dispatch_assets(portfolio.Amount("BTC", "10.1"))
+        amounts = portfolio.BalanceStore.dispatch_assets(portfolio.Amount("BTC", "11.1"))
         self.assertIn("XEM", portfolio.BalanceStore.currencies())
         self.assertEqual(D("2.6"), amounts["BTC"].value)
         self.assertEqual(D("7.5"), amounts["XEM"].value)
+        self.assertEqual(D("-1.0"), amounts["DASH"].value)
 
     def test_currencies(self):
         portfolio.BalanceStore.all = {
@@ -824,6 +992,7 @@ class BalanceStoreTest(WebMockTestCase):
                 }
         self.assertListEqual(["BTC", "ETH"], list(portfolio.BalanceStore.currencies()))
 
+@unittest.skipUnless("unit" in limits, "Unit skipped")
 class ComputationTest(WebMockTestCase):
     def test_compute_value(self):
         compute = mock.Mock()
@@ -848,6 +1017,7 @@ class ComputationTest(WebMockTestCase):
         compute.assert_called_with("foo", "bid")
 
 
+@unittest.skipUnless("unit" in limits, "Unit skipped")
 class TradeTest(WebMockTestCase):
 
     def test_values_assertion(self):
@@ -881,7 +1051,7 @@ class TradeTest(WebMockTestCase):
 
         value_from = portfolio.Amount("BTC", "1.0")
         value_from.linked_to = portfolio.Amount("BTC", "1.0")
-        value_to = portfolio.Amount("BTC", "1.0")
+        value_to = portfolio.Amount("BTC", "2.0")
         trade = portfolio.Trade(value_from, value_to, "BTC")
 
         self.assertIsNone(trade.action)
@@ -939,22 +1109,251 @@ class TradeTest(WebMockTestCase):
         trade = portfolio.Trade(value_from, value_to, "ETH")
 
         order1 = mock.Mock()
-        order1.filled_amount = portfolio.Amount("ETH", "0.3")
+        order1.filled_amount.return_value = portfolio.Amount("ETH", "0.3")
 
         order2 = mock.Mock()
-        order2.filled_amount = portfolio.Amount("ETH", "0.01")
+        order2.filled_amount.return_value = portfolio.Amount("ETH", "0.01")
         trade.orders.append(order1)
         trade.orders.append(order2)
 
-        self.assertEqual(portfolio.Amount("ETH", "0.31"), trade.filled_amount)
+        self.assertEqual(portfolio.Amount("ETH", "0.31"), trade.filled_amount())
+        order1.filled_amount.assert_called_with(in_base_currency=False)
+        order2.filled_amount.assert_called_with(in_base_currency=False)
 
-    @unittest.skip("TODO")
-    def test_prepare_order(self):
-        pass
+        self.assertEqual(portfolio.Amount("ETH", "0.31"), trade.filled_amount(in_base_currency=False))
+        order1.filled_amount.assert_called_with(in_base_currency=False)
+        order2.filled_amount.assert_called_with(in_base_currency=False)
+
+        self.assertEqual(portfolio.Amount("ETH", "0.31"), trade.filled_amount(in_base_currency=True))
+        order1.filled_amount.assert_called_with(in_base_currency=True)
+        order2.filled_amount.assert_called_with(in_base_currency=True)
+
+    @mock.patch.object(helper, "get_ticker")
+    @mock.patch.object(portfolio.Computation, "compute_value")
+    @mock.patch.object(portfolio.Trade, "filled_amount")
+    @mock.patch.object(portfolio, "Order")
+    def test_prepare_order(self, Order, filled_amount, compute_value, get_ticker):
+        Order.return_value = "Order"
+
+        with self.subTest(desc="Nothing to do"):
+            value_from = portfolio.Amount("BTC", "10")
+            value_from.rate = D("0.1")
+            value_from.linked_to = portfolio.Amount("FOO", "100")
+            value_to = portfolio.Amount("BTC", "10")
+            trade = portfolio.Trade(value_from, value_to, "FOO", market="market")
+
+            trade.prepare_order()
+
+            filled_amount.assert_not_called()
+            compute_value.assert_not_called()
+            self.assertEqual(0, len(trade.orders))
+            Order.assert_not_called()
+
+        get_ticker.return_value = { "inverted": False }
+        with self.subTest(desc="Already filled"), mock.patch('sys.stdout', new_callable=StringIO) as stdout_mock:
+            filled_amount.return_value = portfolio.Amount("FOO", "100")
+            compute_value.return_value = D("0.125")
+
+            value_from = portfolio.Amount("BTC", "10")
+            value_from.rate = D("0.1")
+            value_from.linked_to = portfolio.Amount("FOO", "100")
+            value_to = portfolio.Amount("BTC", "0")
+            trade = portfolio.Trade(value_from, value_to, "FOO", market="market")
+
+            trade.prepare_order()
+
+            filled_amount.assert_called_with(in_base_currency=False)
+            compute_value.assert_called_with(get_ticker.return_value, "sell", compute_value="default")
+            self.assertEqual(0, len(trade.orders))
+            self.assertRegex(stdout_mock.getvalue(), "Less to do than already filled: ")
+            Order.assert_not_called()
+
+        with self.subTest(action="dispose", inverted=False):
+            filled_amount.return_value = portfolio.Amount("FOO", "60")
+            compute_value.return_value = D("0.125")
+
+            value_from = portfolio.Amount("BTC", "10")
+            value_from.rate = D("0.1")
+            value_from.linked_to = portfolio.Amount("FOO", "100")
+            value_to = portfolio.Amount("BTC", "1")
+            trade = portfolio.Trade(value_from, value_to, "FOO", market="market")
+
+            trade.prepare_order()
+
+            filled_amount.assert_called_with(in_base_currency=False)
+            compute_value.assert_called_with(get_ticker.return_value, "sell", compute_value="default")
+            self.assertEqual(1, len(trade.orders))
+            Order.assert_called_with("sell", portfolio.Amount("FOO", 30),
+                    D("0.125"), "BTC", "long", "market",
+                    trade, close_if_possible=False)
+
+        with self.subTest(action="acquire", inverted=False):
+            filled_amount.return_value = portfolio.Amount("BTC", "3")
+            compute_value.return_value = D("0.125")
+
+            value_from = portfolio.Amount("BTC", "1")
+            value_from.rate = D("0.1")
+            value_from.linked_to = portfolio.Amount("FOO", "10")
+            value_to = portfolio.Amount("BTC", "10")
+            trade = portfolio.Trade(value_from, value_to, "FOO", market="market")
+
+            trade.prepare_order()
+
+            filled_amount.assert_called_with(in_base_currency=True)
+            compute_value.assert_called_with(get_ticker.return_value, "buy", compute_value="default")
+            self.assertEqual(1, len(trade.orders))
+
+            Order.assert_called_with("buy", portfolio.Amount("FOO", 48),
+                D("0.125"), "BTC", "long", "market",
+                trade, close_if_possible=False)
+
+        with self.subTest(close_if_possible=True):
+            filled_amount.return_value = portfolio.Amount("FOO", "0")
+            compute_value.return_value = D("0.125")
+
+            value_from = portfolio.Amount("BTC", "10")
+            value_from.rate = D("0.1")
+            value_from.linked_to = portfolio.Amount("FOO", "100")
+            value_to = portfolio.Amount("BTC", "0")
+            trade = portfolio.Trade(value_from, value_to, "FOO", market="market")
+
+            trade.prepare_order()
+
+            filled_amount.assert_called_with(in_base_currency=False)
+            compute_value.assert_called_with(get_ticker.return_value, "sell", compute_value="default")
+            self.assertEqual(1, len(trade.orders))
+            Order.assert_called_with("sell", portfolio.Amount("FOO", 100),
+                    D("0.125"), "BTC", "long", "market",
+                    trade, close_if_possible=True)
+
+        get_ticker.return_value = { "inverted": True, "original": {} }
+        with self.subTest(action="dispose", inverted=True):
+            filled_amount.return_value = portfolio.Amount("FOO", "300")
+            compute_value.return_value = D("125")
+
+            value_from = portfolio.Amount("BTC", "10")
+            value_from.rate = D("0.01")
+            value_from.linked_to = portfolio.Amount("FOO", "1000")
+            value_to = portfolio.Amount("BTC", "1")
+            trade = portfolio.Trade(value_from, value_to, "FOO", market="market")
+
+            trade.prepare_order(compute_value="foo")
+
+            filled_amount.assert_called_with(in_base_currency=True)
+            compute_value.assert_called_with(get_ticker.return_value["original"], "buy", compute_value="foo")
+            self.assertEqual(1, len(trade.orders))
+            Order.assert_called_with("buy", portfolio.Amount("BTC", D("4.8")),
+                    D("125"), "FOO", "long", "market",
+                    trade, close_if_possible=False)
+
+        with self.subTest(action="acquire", inverted=True):
+            filled_amount.return_value = portfolio.Amount("BTC", "4")
+            compute_value.return_value = D("125")
+
+            value_from = portfolio.Amount("BTC", "1")
+            value_from.rate = D("0.01")
+            value_from.linked_to = portfolio.Amount("FOO", "100")
+            value_to = portfolio.Amount("BTC", "10")
+            trade = portfolio.Trade(value_from, value_to, "FOO", market="market")
+
+            trade.prepare_order(compute_value="foo")
+
+            filled_amount.assert_called_with(in_base_currency=False)
+            compute_value.assert_called_with(get_ticker.return_value["original"], "sell", compute_value="foo")
+            self.assertEqual(1, len(trade.orders))
+            Order.assert_called_with("sell", portfolio.Amount("BTC", D("5")),
+                    D("125"), "FOO", "long", "market",
+                    trade, close_if_possible=False)
+
+
+    @mock.patch.object(portfolio.Trade, "prepare_order")
+    def test_update_order(self, prepare_order):
+        order_mock = mock.Mock()
+        new_order_mock = mock.Mock()
+
+        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")
+        def _prepare_order(compute_value=None):
+            trade.orders.append(new_order_mock)
+        prepare_order.side_effect = _prepare_order
+
+        for i in [0, 1, 3, 4, 6]:
+            with self.subTest(tick=i), mock.patch('sys.stdout', new_callable=StringIO) as stdout_mock:
+                trade.update_order(order_mock, i)
+                order_mock.cancel.assert_not_called()
+                new_order_mock.run.assert_not_called()
+                self.assertRegex(stdout_mock.getvalue(), "tick {}, waiting".format(i))
+                self.assertEqual(0, len(trade.orders))
+
+            order_mock.reset_mock()
+            new_order_mock.reset_mock()
+            trade.orders = []
+
+        with mock.patch('sys.stdout', new_callable=StringIO) as stdout_mock:
+            trade.update_order(order_mock, 2)
+            order_mock.cancel.assert_called()
+            new_order_mock.run.assert_called()
+            prepare_order.assert_called()
+            self.assertRegex(stdout_mock.getvalue(), "tick 2, cancelling and adjusting")
+            self.assertEqual(1, len(trade.orders))
+
+        order_mock.reset_mock()
+        new_order_mock.reset_mock()
+        trade.orders = []
+
+        with mock.patch('sys.stdout', new_callable=StringIO) as stdout_mock:
+            trade.update_order(order_mock, 5)
+            order_mock.cancel.assert_called()
+            new_order_mock.run.assert_called()
+            prepare_order.assert_called()
+            self.assertRegex(stdout_mock.getvalue(), "tick 5, cancelling and adjusting")
+            self.assertEqual(1, len(trade.orders))
+
+        order_mock.reset_mock()
+        new_order_mock.reset_mock()
+        trade.orders = []
+
+        with mock.patch('sys.stdout', new_callable=StringIO) as stdout_mock:
+            trade.update_order(order_mock, 7)
+            order_mock.cancel.assert_called()
+            new_order_mock.run.assert_called()
+            prepare_order.assert_called_with(compute_value="default")
+            self.assertRegex(stdout_mock.getvalue(), "tick 7, fallbacking to market value")
+            self.assertRegex(stdout_mock.getvalue(), "tick 7, market value, cancelling and adjusting to")
+            self.assertEqual(1, len(trade.orders))
+
+        order_mock.reset_mock()
+        new_order_mock.reset_mock()
+        trade.orders = []
+
+        for i in [10, 13, 16]:
+            with self.subTest(tick=i), mock.patch('sys.stdout', new_callable=StringIO) as stdout_mock:
+                trade.update_order(order_mock, i)
+                order_mock.cancel.assert_called()
+                new_order_mock.run.assert_called()
+                prepare_order.assert_called_with(compute_value="default")
+                self.assertNotRegex(stdout_mock.getvalue(), "tick {}, fallbacking to market value".format(i))
+                self.assertRegex(stdout_mock.getvalue(), "tick {}, market value, cancelling and adjusting to".format(i))
+                self.assertEqual(1, len(trade.orders))
+
+            order_mock.reset_mock()
+            new_order_mock.reset_mock()
+            trade.orders = []
+
+        for i in [8, 9, 11, 12]:
+            with self.subTest(tick=i), mock.patch('sys.stdout', new_callable=StringIO) as stdout_mock:
+                trade.update_order(order_mock, i)
+                order_mock.cancel.assert_not_called()
+                new_order_mock.run.assert_not_called()
+                self.assertEqual("", stdout_mock.getvalue())
+                self.assertEqual(0, len(trade.orders))
+
+            order_mock.reset_mock()
+            new_order_mock.reset_mock()
+            trade.orders = []
 
-    @unittest.skip("TODO")
-    def test_update_order(self):
-        pass
 
     @mock.patch('sys.stdout', new_callable=StringIO)
     def test_print_with_order(self, mock_stdout):
@@ -987,6 +1386,7 @@ class TradeTest(WebMockTestCase):
 
         self.assertEqual("Trade(0.50000000 BTC [10.00000000 ETH] -> 1.00000000 BTC in ETH, acquire)", str(trade))
 
+@unittest.skipUnless("acceptance" in limits, "Acceptance skipped")
 class AcceptanceTest(WebMockTestCase):
     @unittest.expectedFailure
     def test_success_sell_only_necessary(self):