From 1aa7d4fa2ec3c2b3268bef31a666ca6e1aaa6563 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Isma=C3=ABl=20Bouya?= Date: Sun, 11 Feb 2018 13:36:54 +0100 Subject: Add Makefile and test coverage Fix order preparation and add tests for the step Separate tests between acceptance and unit Add more tests --- .gitignore | 2 + Makefile | 32 +++++ portfolio.py | 98 +++++++------ store.py | 33 +++-- test.py | 440 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++--- 5 files changed, 530 insertions(+), 75 deletions(-) create mode 100644 Makefile diff --git a/.gitignore b/.gitignore index bee8a64..bd500b3 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,3 @@ __pycache__ +.coverage +/htmlcov diff --git a/Makefile b/Makefile new file mode 100644 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" diff --git a/portfolio.py b/portfolio.py index efd9b84..b629966 100644 --- a/portfolio.py +++ b/portfolio.py @@ -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): diff --git a/store.py b/store.py index 4e46878..841a0fc 100644 --- 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 aae1dc8..be3ad4a 100644 --- 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): -- cgit v1.2.3