From cfab619d9223fc824649a6fe16863931f5e43891 Mon Sep 17 00:00:00 2001 From: =?utf8?q?Isma=C3=ABl=20Bouya?= Date: Thu, 18 Jan 2018 01:43:19 +0100 Subject: [PATCH] Move ticker to Trade class and add tests --- portfolio.py | 96 ++++++++++++++++++------------------ test.py | 136 +++++++++++++++++++++++++++++++++++++++++++++++---- 2 files changed, 175 insertions(+), 57 deletions(-) diff --git a/portfolio.py b/portfolio.py index 1d8bfd5..576a228 100644 --- a/portfolio.py +++ b/portfolio.py @@ -102,7 +102,7 @@ class Amount: def in_currency(self, other_currency, market, action="average"): if other_currency == self.currency: return self - asset_ticker = self.get_ticker(other_currency, market) + asset_ticker = Trade.get_ticker(self.currency, other_currency, market) if asset_ticker is not None: return Amount( other_currency, @@ -113,41 +113,6 @@ class Amount: else: raise Exception("This asset is not available in the chosen market") - def get_ticker(self, c2, market, refresh=False): - c1 = self.currency - - def invert(ticker): - return { - "inverted": True, - "average": (float(1/ticker["bid"]) + float(1/ticker["ask"]) ) / 2, - "notInverted": ticker, - } - def augment_ticker(ticker): - ticker.update({ - "inverted": False, - "average": (ticker["bid"] + ticker["ask"] ) / 2, - }) - - if time.time() - self.ticker_cache_timestamp > 5: - self.ticker_cache = {} - self.ticker_cache_timestamp = time.time() - elif not refresh: - if (c1, c2, market.__class__) in self.ticker_cache: - return self.ticker_cache[(c1, c2, market.__class__)] - if (c2, c1, market.__class__) in self.ticker_cache: - return invert(self.ticker_cache[(c2, c1, market.__class__)]) - - try: - self.ticker_cache[(c1, c2, market.__class__)] = market.fetch_ticker("{}/{}".format(c1, c2)) - augment_ticker(self.ticker_cache[(c1, c2, market.__class__)]) - except ccxt.ExchangeError: - try: - self.ticker_cache[(c2, c1, market.__class__)] = market.fetch_ticker("{}/{}".format(c2, c1)) - augment_ticker(self.ticker_cache[(c2, c1, market.__class__)]) - except ccxt.ExchangeError: - self.ticker_cache[(c1, c2, market.__class__)] = None - return self.get_ticker(c2, market) - def __abs__(self): return Amount(self.currency, 0, int_val=abs(self._value)) @@ -212,7 +177,6 @@ class Amount: class Balance: known_balances = {} - trades = {} def __init__(self, currency, total_value, free_value, used_value): self.currency = currency @@ -288,8 +252,52 @@ class Trade: self.base_currency = self.value_from.currency if market is not None: + self.market = market self.prepare_order(market) + fees_cache = {} + @classmethod + def fetch_fees(cls, market): + if market.__class__ not in cls.fees_cache: + cls.fees_cache[market.__class__] = market.fetch_fees() + return cls.fees_cache[market.__class__] + + ticker_cache = {} + ticker_cache_timestamp = time.time() + @classmethod + def get_ticker(cls, c1, c2, market, refresh=False): + def invert(ticker): + return { + "inverted": True, + "average": (float(1/ticker["bid"]) + float(1/ticker["ask"]) ) / 2, + "original": ticker, + } + def augment_ticker(ticker): + ticker.update({ + "inverted": False, + "average": (ticker["bid"] + ticker["ask"] ) / 2, + }) + + if time.time() - cls.ticker_cache_timestamp > 5: + cls.ticker_cache = {} + cls.ticker_cache_timestamp = time.time() + elif not refresh: + if (c1, c2, market.__class__) in cls.ticker_cache: + return cls.ticker_cache[(c1, c2, market.__class__)] + if (c2, c1, market.__class__) in cls.ticker_cache: + return invert(cls.ticker_cache[(c2, c1, market.__class__)]) + + try: + cls.ticker_cache[(c1, c2, market.__class__)] = market.fetch_ticker("{}/{}".format(c1, c2)) + augment_ticker(cls.ticker_cache[(c1, c2, market.__class__)]) + except ccxt.ExchangeError: + try: + cls.ticker_cache[(c2, c1, market.__class__)] = market.fetch_ticker("{}/{}".format(c2, c1)) + augment_ticker(cls.ticker_cache[(c2, c1, market.__class__)]) + except ccxt.ExchangeError: + cls.ticker_cache[(c1, c2, market.__class__)] = None + return cls.get_ticker(c1, c2, market) + @classmethod def compute_trades(cls, values_in_base, new_repartition, market=None): base_currency = sum(values_in_base.values()).currency @@ -316,7 +324,7 @@ class Trade: else: return "sell" - def ticker_action(self, inverted): + def order_action(self, inverted): if self.value_from < self.value_to: return "ask" if not inverted else "bid" else: @@ -334,13 +342,13 @@ class Trade: delta = abs(value_to - value_from) currency = self.base_currency else: - ticker = ticker["notInverted"] + ticker = ticker["original"] delta = abs(self.value_to - self.value_from) currency = self.currency - rate = ticker[self.ticker_action(inverted)] + rate = ticker[self.order_action(inverted)] - self.orders.append(Order(self.ticker_action(inverted), delta, rate, currency)) + self.orders.append(Order(self.order_action(inverted), delta, rate, currency)) @classmethod def all_orders(cls): @@ -408,12 +416,6 @@ class Order: self.status = result["status"] return self.status -@static_var("cache", {}) -def fetch_fees(market): - if market.__class__ not in fetch_fees.cache: - fetch_fees.cache[market.__class__] = market.fetch_fees() - return fetch_fees.cache[market.__class__] - def print_orders(market, base_currency="BTC"): Balance.prepare_trades(market, base_currency=base_currency) for currency, trade in Trade.trades.items(): diff --git a/test.py b/test.py index d10bfe4..7608061 100644 --- a/test.py +++ b/test.py @@ -17,13 +17,12 @@ class AmountTest(unittest.TestCase): self.assertEqual(amount, amount.in_currency("ETC", None)) ticker_mock = unittest.mock.Mock() - with mock.patch.object(portfolio.Amount, 'get_ticker', new=ticker_mock): + with mock.patch.object(portfolio.Trade, 'get_ticker', new=ticker_mock): ticker_mock.return_value = None - portfolio.Amount.get_ticker = ticker_mock self.assertRaises(Exception, amount.in_currency, "ETH", None) - with mock.patch.object(portfolio.Amount, 'get_ticker', new=ticker_mock): + with mock.patch.object(portfolio.Trade, 'get_ticker', new=ticker_mock): ticker_mock.return_value = { "average": 0.3, "foo": "bar", @@ -35,10 +34,6 @@ class AmountTest(unittest.TestCase): self.assertEqual(amount, converted_amount.linked_to) self.assertEqual("bar", converted_amount.ticker["foo"]) - @unittest.skip("TODO") - def test_get_ticker(self): - pass - def test__abs(self): amount = portfolio.Amount("SC", -120) self.assertEqual(120, abs(amount).value) @@ -276,7 +271,7 @@ class BalanceTest(unittest.TestCase): "total": 0.0 }, } - self.patcher = mock.patch.multiple(portfolio.Balance, known_balances={}, trades={}) + self.patcher = mock.patch.multiple(portfolio.Balance, known_balances={}) self.patcher.start() def test_values(self): @@ -292,7 +287,7 @@ class BalanceTest(unittest.TestCase): self.assertEqual(0.30, balance.used.value) self.assertEqual("BTC", balance.currency) - @mock.patch.object(portfolio.Amount, "get_ticker") + @mock.patch.object(portfolio.Trade, "get_ticker") def test_in_currency(self, get_ticker): portfolio.Balance.known_balances = { "BTC": portfolio.Balance("BTC", 0.65, 0.35, 0.30), @@ -352,7 +347,7 @@ class BalanceTest(unittest.TestCase): self.assertEqual(7.5, amounts["XEM"].value) @mock.patch.object(portfolio.Portfolio, "repartition_pertenthousand") - @mock.patch.object(portfolio.Amount, "get_ticker") + @mock.patch.object(portfolio.Trade, "get_ticker") @mock.patch.object(portfolio.Trade, "compute_trades") def test_prepare_trades(self, compute_trades, get_ticker, repartition): repartition.return_value = { @@ -393,5 +388,126 @@ class BalanceTest(unittest.TestCase): def tearDown(self): self.patcher.stop() +class TradeTest(unittest.TestCase): + import time + + def setUp(self): + super(TradeTest, self).setUp() + + self.patcher = mock.patch.multiple(portfolio.Trade, + ticker_cache={}, + ticker_cache_timestamp=self.time.time(), + fees_cache={}, + trades={}) + self.patcher.start() + + def test_get_ticker(self): + market = mock.Mock() + market.fetch_ticker.side_effect = [ + { "bid": 1, "ask": 3 }, + portfolio.ccxt.ExchangeError("foo"), + { "bid": 10, "ask": 40 }, + portfolio.ccxt.ExchangeError("foo"), + portfolio.ccxt.ExchangeError("foo"), + ] + + ticker = portfolio.Trade.get_ticker("ETH", "ETC", market) + market.fetch_ticker.assert_called_with("ETH/ETC") + self.assertEqual(1, ticker["bid"]) + self.assertEqual(3, ticker["ask"]) + self.assertEqual(2, ticker["average"]) + self.assertFalse(ticker["inverted"]) + + ticker = portfolio.Trade.get_ticker("ETH", "XVG", market) + self.assertEqual(0.0625, ticker["average"]) + self.assertTrue(ticker["inverted"]) + self.assertIn("original", ticker) + self.assertEqual(10, ticker["original"]["bid"]) + + ticker = portfolio.Trade.get_ticker("XVG", "XMR", market) + self.assertIsNone(ticker) + + market.fetch_ticker.assert_has_calls([ + mock.call("ETH/ETC"), + mock.call("ETH/XVG"), + mock.call("XVG/ETH"), + mock.call("XVG/XMR"), + mock.call("XMR/XVG"), + ]) + + market2 = mock.Mock() + market2.fetch_ticker.side_effect = [ + { "bid": 1, "ask": 3 }, + { "bid": 1.2, "ask": 3.5 }, + ] + ticker1 = portfolio.Trade.get_ticker("ETH", "ETC", market2) + ticker2 = portfolio.Trade.get_ticker("ETH", "ETC", market2) + ticker3 = portfolio.Trade.get_ticker("ETC", "ETH", market2) + market2.fetch_ticker.assert_called_once_with("ETH/ETC") + self.assertEqual(1, ticker1["bid"]) + self.assertDictEqual(ticker1, ticker2) + self.assertDictEqual(ticker1, ticker3["original"]) + + ticker4 = portfolio.Trade.get_ticker("ETH", "ETC", market2, refresh=True) + ticker5 = portfolio.Trade.get_ticker("ETH", "ETC", market2) + self.assertEqual(1.2, ticker4["bid"]) + self.assertDictEqual(ticker4, ticker5) + + market3 = mock.Mock() + market3.fetch_ticker.side_effect = [ + { "bid": 1, "ask": 3 }, + { "bid": 1.2, "ask": 3.5 }, + ] + ticker6 = portfolio.Trade.get_ticker("ETH", "ETC", market3) + portfolio.Trade.ticker_cache_timestamp -= 4 + ticker7 = portfolio.Trade.get_ticker("ETH", "ETC", market3) + portfolio.Trade.ticker_cache_timestamp -= 2 + ticker8 = portfolio.Trade.get_ticker("ETH", "ETC", market3) + self.assertDictEqual(ticker6, ticker7) + self.assertEqual(1.2, ticker8["bid"]) + + @unittest.skip("TODO") + def test_values_assertion(self): + pass + + @unittest.skip("TODO") + def test_fetch_fees(self): + pass + + @unittest.skip("TODO") + def test_compute_trades(self): + pass + + @unittest.skip("TODO") + def test_action(self): + pass + + @unittest.skip("TODO") + def test_action(self): + pass + + @unittest.skip("TODO") + def test_order_action(self): + pass + + @unittest.skip("TODO") + def test_prepare_order(self): + pass + + @unittest.skip("TODO") + def test_all_orders(self): + pass + + @unittest.skip("TODO") + def test_follow_orders(self): + pass + + @unittest.skip("TODO") + def test__repr(self): + pass + + def tearDown(self): + self.patcher.stop() + if __name__ == '__main__': unittest.main() -- 2.41.0