+import sys
import portfolio
import unittest
from decimal import Decimal as D
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
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:
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")
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)
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", {
"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()
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()
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()
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 = {
}
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()
compute.assert_called_with("foo", "bid")
+@unittest.skipUnless("unit" in limits, "Unit skipped")
class TradeTest(WebMockTestCase):
def test_values_assertion(self):
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)
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):
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):